mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-10-13 11:37:13 +00:00
614 lines
25 KiB
Python
614 lines
25 KiB
Python
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pytest_mock import MockerFixture
|
|
|
|
from paperless.settings.parsers import get_bool_from_env
|
|
from paperless.settings.parsers import get_choice_from_env
|
|
from paperless.settings.parsers import get_float_from_env
|
|
from paperless.settings.parsers import get_int_from_env
|
|
from paperless.settings.parsers import get_list_from_env
|
|
from paperless.settings.parsers import get_path_from_env
|
|
from paperless.settings.parsers import parse_dict_from_str
|
|
from paperless.settings.parsers import str_to_bool
|
|
|
|
|
|
class TestStringToBool:
|
|
@pytest.mark.parametrize(
|
|
"true_value",
|
|
[
|
|
pytest.param("true", id="lowercase_true"),
|
|
pytest.param("1", id="digit_1"),
|
|
pytest.param("T", id="capital_T"),
|
|
pytest.param("y", id="lowercase_y"),
|
|
pytest.param("YES", id="uppercase_YES"),
|
|
pytest.param(" True ", id="whitespace_true"),
|
|
],
|
|
)
|
|
def test_true_conversion(self, true_value: str) -> None:
|
|
"""Test that various 'true' strings correctly evaluate to True."""
|
|
assert str_to_bool(true_value) is True
|
|
|
|
@pytest.mark.parametrize(
|
|
"false_value",
|
|
[
|
|
pytest.param("false", id="lowercase_false"),
|
|
pytest.param("0", id="digit_0"),
|
|
pytest.param("f", id="capital_f"),
|
|
pytest.param("N", id="capital_N"),
|
|
pytest.param("no", id="lowercase_no"),
|
|
pytest.param(" False ", id="whitespace_false"),
|
|
],
|
|
)
|
|
def test_false_conversion(self, false_value: str) -> None:
|
|
"""Test that various 'false' strings correctly evaluate to False."""
|
|
assert str_to_bool(false_value) is False
|
|
|
|
def test_invalid_conversion(self) -> None:
|
|
"""Test that an invalid string raises a ValueError."""
|
|
with pytest.raises(ValueError, match="Cannot convert 'maybe' to a boolean\\."):
|
|
str_to_bool("maybe")
|
|
|
|
|
|
class TestParseDictFromString:
|
|
def test_empty_and_none_input(self) -> None:
|
|
"""Test behavior with None or empty string input."""
|
|
assert parse_dict_from_str(None) == {}
|
|
assert parse_dict_from_str("") == {}
|
|
defaults = {"a": 1}
|
|
res = parse_dict_from_str(None, defaults=defaults)
|
|
assert res == defaults
|
|
# Ensure it returns a copy, not the original object
|
|
assert res is not defaults
|
|
|
|
def test_basic_parsing(self) -> None:
|
|
"""Test simple key-value parsing without defaults or types."""
|
|
env_str = "key1=val1, key2=val2"
|
|
expected = {"key1": "val1", "key2": "val2"}
|
|
assert parse_dict_from_str(env_str) == expected
|
|
|
|
def test_with_defaults(self) -> None:
|
|
"""Test that environment values override defaults correctly."""
|
|
defaults = {"host": "localhost", "port": 8000, "user": "default"}
|
|
env_str = "port=9090, host=db.example.com"
|
|
expected = {"host": "db.example.com", "port": "9090", "user": "default"}
|
|
result = parse_dict_from_str(env_str, defaults=defaults)
|
|
assert result == expected
|
|
|
|
def test_type_casting(self) -> None:
|
|
"""Test successful casting of values to specified types."""
|
|
env_str = "port=9090, debug=true, timeout=12.5, user=admin"
|
|
type_map = {"port": int, "debug": bool, "timeout": float}
|
|
expected = {"port": 9090, "debug": True, "timeout": 12.5, "user": "admin"}
|
|
result = parse_dict_from_str(env_str, type_map=type_map)
|
|
assert result == expected
|
|
|
|
def test_type_casting_with_defaults(self) -> None:
|
|
"""Test casting when values come from both defaults and env string."""
|
|
defaults = {"port": 8000, "debug": False, "retries": 3}
|
|
env_str = "port=9090, debug=true"
|
|
type_map = {"port": int, "debug": bool, "retries": int}
|
|
|
|
# The 'retries' value comes from defaults and is already an int,
|
|
# so it should not be processed by the caster.
|
|
expected = {"port": 9090, "debug": True, "retries": 3}
|
|
result = parse_dict_from_str(env_str, defaults=defaults, type_map=type_map)
|
|
assert result == expected
|
|
assert isinstance(result["retries"], int)
|
|
|
|
def test_path_casting(self, tmp_path: Path) -> None:
|
|
"""Test successful casting of a string to a resolved pathlib.Path object."""
|
|
# Create a dummy file to resolve against
|
|
test_file = tmp_path / "test_file.txt"
|
|
test_file.touch()
|
|
|
|
env_str = f"config_path={test_file}"
|
|
type_map = {"config_path": Path}
|
|
result = parse_dict_from_str(env_str, type_map=type_map)
|
|
|
|
# The result should be a resolved Path object
|
|
assert isinstance(result["config_path"], Path)
|
|
assert result["config_path"] == test_file.resolve()
|
|
|
|
def test_custom_separator(self) -> None:
|
|
"""Test parsing with a custom separator like a semicolon."""
|
|
env_str = "host=db; port=5432; user=test"
|
|
expected = {"host": "db", "port": "5432", "user": "test"}
|
|
result = parse_dict_from_str(env_str, separator=";")
|
|
assert result == expected
|
|
|
|
def test_edge_cases_in_string(self) -> None:
|
|
"""Test malformed strings to ensure robustness."""
|
|
# Malformed pair 'debug' is skipped, extra comma is ignored
|
|
env_str = "key=val,, debug, foo=bar"
|
|
expected = {"key": "val", "foo": "bar"}
|
|
assert parse_dict_from_str(env_str) == expected
|
|
|
|
# Value can contain the equals sign
|
|
env_str = "url=postgres://user:pass@host:5432/db"
|
|
expected = {"url": "postgres://user:pass@host:5432/db"}
|
|
assert parse_dict_from_str(env_str) == expected
|
|
|
|
def test_casting_error_handling(self) -> None:
|
|
"""Test that a ValueError is raised for invalid casting."""
|
|
env_str = "port=not-a-number"
|
|
type_map = {"port": int}
|
|
|
|
with pytest.raises(ValueError) as excinfo:
|
|
parse_dict_from_str(env_str, type_map=type_map)
|
|
|
|
assert "Error casting key 'port'" in str(excinfo.value)
|
|
assert "value 'not-a-number'" in str(excinfo.value)
|
|
assert "to type 'int'" in str(excinfo.value)
|
|
|
|
def test_bool_casting_error(self) -> None:
|
|
"""Test that an invalid boolean string raises a ValueError."""
|
|
env_str = "debug=maybe"
|
|
type_map = {"debug": bool}
|
|
with pytest.raises(ValueError, match="Error casting key 'debug'"):
|
|
parse_dict_from_str(env_str, type_map=type_map)
|
|
|
|
def test_nested_key_parsing_basic(self) -> None:
|
|
"""Basic nested key parsing using dot-notation."""
|
|
env_str = "database.host=db.example.com, database.port=5432, logging.level=INFO"
|
|
result = parse_dict_from_str(env_str)
|
|
assert result == {
|
|
"database": {"host": "db.example.com", "port": "5432"},
|
|
"logging": {"level": "INFO"},
|
|
}
|
|
|
|
def test_nested_overrides_defaults_and_deepcopy(self) -> None:
|
|
"""Nested env keys override defaults and defaults are deep-copied."""
|
|
defaults = {"database": {"host": "127.0.0.1", "port": 3306, "user": "default"}}
|
|
env_str = "database.host=db.example.com, debug=true"
|
|
result = parse_dict_from_str(
|
|
env_str,
|
|
defaults=defaults,
|
|
type_map={"debug": bool},
|
|
)
|
|
|
|
assert result["database"]["host"] == "db.example.com"
|
|
# Unchanged default preserved
|
|
assert result["database"]["port"] == 3306
|
|
assert result["database"]["user"] == "default"
|
|
# Default object was deep-copied (no same nested object identity)
|
|
assert result is not defaults
|
|
assert result["database"] is not defaults["database"]
|
|
|
|
def test_nested_type_casting(self) -> None:
|
|
"""Type casting for nested keys (dot-notation) should work."""
|
|
env_str = "database.host=db.example.com, database.port=5433, debug=false"
|
|
type_map = {"database.port": int, "debug": bool}
|
|
result = parse_dict_from_str(env_str, type_map=type_map)
|
|
|
|
assert result["database"]["host"] == "db.example.com"
|
|
assert result["database"]["port"] == 5433
|
|
assert isinstance(result["database"]["port"], int)
|
|
assert result["debug"] is False
|
|
assert isinstance(result["debug"], bool)
|
|
|
|
def test_nested_casting_error_message(self) -> None:
|
|
"""Error messages should include the full dotted key name on failure."""
|
|
env_str = "database.port=not-a-number"
|
|
type_map = {"database.port": int}
|
|
with pytest.raises(ValueError) as excinfo:
|
|
parse_dict_from_str(env_str, type_map=type_map)
|
|
|
|
msg = str(excinfo.value)
|
|
assert "Error casting key 'database.port'" in msg
|
|
assert "value 'not-a-number'" in msg
|
|
assert "to type 'int'" in msg
|
|
|
|
def test_type_map_does_not_recast_non_string_defaults(self) -> None:
|
|
"""If a default already provides a non-string value, the caster should skip it."""
|
|
defaults = {"database": {"port": 3306}}
|
|
type_map = {"database.port": int}
|
|
result = parse_dict_from_str(None, defaults=defaults, type_map=type_map)
|
|
assert result["database"]["port"] == 3306
|
|
assert isinstance(result["database"]["port"], int)
|
|
|
|
|
|
class TestGetBoolFromEnv:
|
|
def test_existing_env_var(self, mocker) -> None:
|
|
"""Test that an existing environment variable is read and converted."""
|
|
mocker.patch.dict(os.environ, {"TEST_VAR": "true"})
|
|
assert get_bool_from_env("TEST_VAR") is True
|
|
|
|
def test_missing_env_var_uses_default_no(self, mocker) -> None:
|
|
"""Test that a missing environment variable uses default 'NO' and returns False."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_bool_from_env("MISSING_VAR") is False
|
|
|
|
def test_missing_env_var_with_explicit_default(self, mocker) -> None:
|
|
"""Test that a missing environment variable uses the provided default."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_bool_from_env("MISSING_VAR", default="yes") is True
|
|
|
|
def test_invalid_value_raises_error(self, mocker) -> None:
|
|
"""Test that an invalid value raises ValueError (delegates to str_to_bool)."""
|
|
mocker.patch.dict(os.environ, {"INVALID_VAR": "maybe"})
|
|
with pytest.raises(ValueError):
|
|
get_bool_from_env("INVALID_VAR")
|
|
|
|
|
|
class TestGetIntFromEnv:
|
|
@pytest.mark.parametrize(
|
|
("env_value", "expected"),
|
|
[
|
|
pytest.param("42", 42, id="positive"),
|
|
pytest.param("-10", -10, id="negative"),
|
|
pytest.param("0", 0, id="zero"),
|
|
pytest.param("999", 999, id="large_positive"),
|
|
pytest.param("-999", -999, id="large_negative"),
|
|
],
|
|
)
|
|
def test_existing_env_var_valid_ints(self, mocker, env_value, expected) -> None:
|
|
"""Test that existing environment variables with valid integers return correct values."""
|
|
mocker.patch.dict(os.environ, {"INT_VAR": env_value})
|
|
assert get_int_from_env("INT_VAR") == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
("default", "expected"),
|
|
[
|
|
pytest.param(100, 100, id="positive_default"),
|
|
pytest.param(0, 0, id="zero_default"),
|
|
pytest.param(-50, -50, id="negative_default"),
|
|
pytest.param(None, None, id="none_default"),
|
|
],
|
|
)
|
|
def test_missing_env_var_with_defaults(self, mocker, default, expected) -> None:
|
|
"""Test that missing environment variables return provided defaults."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_int_from_env("MISSING_VAR", default=default) == expected
|
|
|
|
def test_missing_env_var_no_default(self, mocker) -> None:
|
|
"""Test that missing environment variable with no default returns None."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_int_from_env("MISSING_VAR") is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_value",
|
|
[
|
|
pytest.param("not_a_number", id="text"),
|
|
pytest.param("42.5", id="float"),
|
|
pytest.param("42a", id="alpha_suffix"),
|
|
pytest.param("", id="empty"),
|
|
pytest.param(" ", id="whitespace"),
|
|
pytest.param("true", id="boolean"),
|
|
pytest.param("1.0", id="decimal"),
|
|
],
|
|
)
|
|
def test_invalid_int_values_raise_error(self, mocker, invalid_value) -> None:
|
|
"""Test that invalid integer values raise ValueError."""
|
|
mocker.patch.dict(os.environ, {"INVALID_INT": invalid_value})
|
|
with pytest.raises(ValueError):
|
|
get_int_from_env("INVALID_INT")
|
|
|
|
|
|
class TestGetFloatFromEnv:
|
|
@pytest.mark.parametrize(
|
|
("env_value", "expected"),
|
|
[
|
|
pytest.param("3.14", 3.14, id="pi"),
|
|
pytest.param("42", 42.0, id="int_as_float"),
|
|
pytest.param("-2.5", -2.5, id="negative"),
|
|
pytest.param("0.0", 0.0, id="zero_float"),
|
|
pytest.param("0", 0.0, id="zero_int"),
|
|
pytest.param("1.5e2", 150.0, id="sci_positive"),
|
|
pytest.param("1e-3", 0.001, id="sci_negative"),
|
|
pytest.param("-1.23e4", -12300.0, id="sci_large"),
|
|
],
|
|
)
|
|
def test_existing_env_var_valid_floats(self, mocker, env_value, expected) -> None:
|
|
"""Test that existing environment variables with valid floats return correct values."""
|
|
mocker.patch.dict(os.environ, {"FLOAT_VAR": env_value})
|
|
assert get_float_from_env("FLOAT_VAR") == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
("default", "expected"),
|
|
[
|
|
pytest.param(3.14, 3.14, id="pi_default"),
|
|
pytest.param(0.0, 0.0, id="zero_default"),
|
|
pytest.param(-2.5, -2.5, id="negative_default"),
|
|
pytest.param(None, None, id="none_default"),
|
|
],
|
|
)
|
|
def test_missing_env_var_with_defaults(self, mocker, default, expected) -> None:
|
|
"""Test that missing environment variables return provided defaults."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_float_from_env("MISSING_VAR", default=default) == expected
|
|
|
|
def test_missing_env_var_no_default(self, mocker) -> None:
|
|
"""Test that missing environment variable with no default returns None."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_float_from_env("MISSING_VAR") is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"invalid_value",
|
|
[
|
|
pytest.param("not_a_number", id="text"),
|
|
pytest.param("42.5.0", id="double_decimal"),
|
|
pytest.param("42a", id="alpha_suffix"),
|
|
pytest.param("", id="empty"),
|
|
pytest.param(" ", id="whitespace"),
|
|
pytest.param("true", id="boolean"),
|
|
pytest.param("1.2.3", id="triple_decimal"),
|
|
],
|
|
)
|
|
def test_invalid_float_values_raise_error(self, mocker, invalid_value) -> None:
|
|
"""Test that invalid float values raise ValueError."""
|
|
mocker.patch.dict(os.environ, {"INVALID_FLOAT": invalid_value})
|
|
with pytest.raises(ValueError):
|
|
get_float_from_env("INVALID_FLOAT")
|
|
|
|
|
|
class TestGetPathFromEnv:
|
|
@pytest.mark.parametrize(
|
|
"env_value",
|
|
[
|
|
pytest.param("/tmp/test", id="absolute"),
|
|
pytest.param("relative/path", id="relative"),
|
|
pytest.param("/path/with spaces/file.txt", id="spaces"),
|
|
pytest.param(".", id="current_dir"),
|
|
pytest.param("..", id="parent_dir"),
|
|
pytest.param("/", id="root"),
|
|
],
|
|
)
|
|
def test_existing_env_var_paths(self, mocker, env_value) -> None:
|
|
"""Test that existing environment variables with paths return resolved Path objects."""
|
|
mocker.patch.dict(os.environ, {"PATH_VAR": env_value})
|
|
result = get_path_from_env("PATH_VAR")
|
|
assert isinstance(result, Path)
|
|
assert result == Path(env_value).resolve()
|
|
|
|
def test_missing_env_var_no_default(self, mocker) -> None:
|
|
"""Test that missing environment variable with no default returns None."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_path_from_env("MISSING_VAR") is None
|
|
|
|
def test_missing_env_var_with_none_default(self, mocker) -> None:
|
|
"""Test that missing environment variable with None default returns None."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
assert get_path_from_env("MISSING_VAR", default=None) is None
|
|
|
|
@pytest.mark.parametrize(
|
|
"default_path_str",
|
|
[
|
|
pytest.param("/default/path", id="absolute_default"),
|
|
pytest.param("relative/default", id="relative_default"),
|
|
pytest.param(".", id="current_default"),
|
|
],
|
|
)
|
|
def test_missing_env_var_with_path_defaults(self, mocker, default_path_str) -> None:
|
|
"""Test that missing environment variables return resolved default Path objects."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
default_path = Path(default_path_str)
|
|
result = get_path_from_env("MISSING_VAR", default=default_path)
|
|
assert isinstance(result, Path)
|
|
assert result == default_path.resolve()
|
|
|
|
def test_relative_paths_are_resolved(self, mocker) -> None:
|
|
"""Test that relative paths are properly resolved to absolute paths."""
|
|
mocker.patch.dict(os.environ, {"REL_PATH": "relative/path"})
|
|
result = get_path_from_env("REL_PATH")
|
|
assert result is not None
|
|
assert result.is_absolute()
|
|
|
|
|
|
class TestGetListFromEnv:
|
|
@pytest.mark.parametrize(
|
|
("env_value", "expected"),
|
|
[
|
|
pytest.param("a,b,c", ["a", "b", "c"], id="basic_comma_separated"),
|
|
pytest.param("single", ["single"], id="single_element"),
|
|
pytest.param("", [], id="empty_string"),
|
|
pytest.param("a, b , c", ["a", "b", "c"], id="whitespace_trimmed"),
|
|
pytest.param("a,,b,c", ["a", "b", "c"], id="empty_elements_removed"),
|
|
],
|
|
)
|
|
def test_existing_env_var_basic_parsing(self, mocker, env_value, expected) -> None:
|
|
"""Test that existing environment variables are parsed correctly."""
|
|
mocker.patch.dict(os.environ, {"LIST_VAR": env_value})
|
|
result = get_list_from_env("LIST_VAR")
|
|
assert result == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
("separator", "env_value", "expected"),
|
|
[
|
|
pytest.param("|", "a|b|c", ["a", "b", "c"], id="pipe_separator"),
|
|
pytest.param(":", "a:b:c", ["a", "b", "c"], id="colon_separator"),
|
|
pytest.param(";", "a;b;c", ["a", "b", "c"], id="semicolon_separator"),
|
|
],
|
|
)
|
|
def test_custom_separators(self, mocker, separator, env_value, expected) -> None:
|
|
"""Test that custom separators work correctly."""
|
|
mocker.patch.dict(os.environ, {"LIST_VAR": env_value})
|
|
result = get_list_from_env("LIST_VAR", separator=separator)
|
|
assert result == expected
|
|
|
|
@pytest.mark.parametrize(
|
|
("default", "expected"),
|
|
[
|
|
pytest.param(
|
|
["default1", "default2"],
|
|
["default1", "default2"],
|
|
id="string_list_default",
|
|
),
|
|
pytest.param([1, 2, 3], [1, 2, 3], id="int_list_default"),
|
|
pytest.param(None, [], id="none_default_returns_empty_list"),
|
|
],
|
|
)
|
|
def test_missing_env_var_with_defaults(self, mocker, default, expected) -> None:
|
|
"""Test that missing environment variables return provided defaults."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
result = get_list_from_env("MISSING_VAR", default=default)
|
|
assert result == expected
|
|
|
|
def test_missing_env_var_no_default(self, mocker) -> None:
|
|
"""Test that missing environment variable with no default returns empty list."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
result = get_list_from_env("MISSING_VAR")
|
|
assert result == []
|
|
|
|
def test_required_env_var_missing_raises_error(self, mocker) -> None:
|
|
"""Test that missing required environment variable raises ValueError."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
with pytest.raises(
|
|
ValueError,
|
|
match="Required environment variable 'REQUIRED_VAR' is not set",
|
|
):
|
|
get_list_from_env("REQUIRED_VAR", required=True)
|
|
|
|
def test_required_env_var_with_default_does_not_raise(self, mocker) -> None:
|
|
"""Test that required environment variable with default does not raise error."""
|
|
mocker.patch.dict(os.environ, {}, clear=True)
|
|
result = get_list_from_env("REQUIRED_VAR", default=["default"], required=True)
|
|
assert result == ["default"]
|
|
|
|
def test_strip_whitespace_false(self, mocker) -> None:
|
|
"""Test that whitespace is preserved when strip_whitespace=False."""
|
|
mocker.patch.dict(os.environ, {"LIST_VAR": " a , b , c "})
|
|
result = get_list_from_env("LIST_VAR", strip_whitespace=False)
|
|
assert result == [" a ", " b ", " c "]
|
|
|
|
def test_remove_empty_false(self, mocker) -> None:
|
|
"""Test that empty elements are preserved when remove_empty=False."""
|
|
mocker.patch.dict(os.environ, {"LIST_VAR": "a,,b,,c"})
|
|
result = get_list_from_env("LIST_VAR", remove_empty=False)
|
|
assert result == ["a", "", "b", "", "c"]
|
|
|
|
|
|
class TestGetEnvChoice:
|
|
@pytest.fixture
|
|
def valid_choices(self) -> set[str]:
|
|
"""Fixture providing a set of valid environment choices."""
|
|
return {"development", "staging", "production"}
|
|
|
|
def test_returns_valid_env_value(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that function returns the environment value when it's valid."""
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": "development"})
|
|
|
|
result = get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
assert result == "development"
|
|
|
|
def test_returns_default_when_env_not_set(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that function returns default value when env var is not set."""
|
|
mocker.patch.dict("os.environ", {}, clear=True)
|
|
|
|
result = get_choice_from_env("TEST_ENV", valid_choices, default="staging")
|
|
|
|
assert result == "staging"
|
|
|
|
def test_returns_none_when_env_not_set_and_no_default(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that function returns None when env var is missing and no default given."""
|
|
mocker.patch.dict("os.environ", {}, clear=True)
|
|
|
|
result = get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
assert result is None
|
|
|
|
def test_raises_error_when_env_value_invalid(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that function raises ValueError when env value is not in choices."""
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": "invalid_value"})
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert (
|
|
"Environment variable 'TEST_ENV' has invalid value 'invalid_value'"
|
|
in error_msg
|
|
)
|
|
assert "Valid choices are:" in error_msg
|
|
assert "development" in error_msg
|
|
assert "staging" in error_msg
|
|
assert "production" in error_msg
|
|
|
|
def test_raises_error_when_default_invalid(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that function raises ValueError when default value is not in choices."""
|
|
mocker.patch.dict("os.environ", {}, clear=True)
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
get_choice_from_env("TEST_ENV", valid_choices, default="invalid_default")
|
|
|
|
error_msg = str(exc_info.value)
|
|
assert (
|
|
"Environment variable 'TEST_ENV' has invalid value 'invalid_default'"
|
|
in error_msg
|
|
)
|
|
|
|
def test_case_sensitive_validation(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test that validation is case sensitive."""
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": "DEVELOPMENT"})
|
|
|
|
with pytest.raises(ValueError):
|
|
get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
def test_empty_string_env_value(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test behavior with empty string environment value."""
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": ""})
|
|
|
|
with pytest.raises(ValueError) as exc_info:
|
|
get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
assert "has invalid value ''" in str(exc_info.value)
|
|
|
|
def test_whitespace_env_value(
|
|
self,
|
|
mocker: MockerFixture,
|
|
valid_choices: set[str],
|
|
) -> None:
|
|
"""Test behavior with whitespace-only environment value."""
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": " development "})
|
|
|
|
with pytest.raises(ValueError):
|
|
get_choice_from_env("TEST_ENV", valid_choices)
|
|
|
|
def test_single_choice_set(self, mocker: MockerFixture) -> None:
|
|
"""Test function works correctly with single choice set."""
|
|
single_choice: set[str] = {"production"}
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": "production"})
|
|
|
|
result = get_choice_from_env("TEST_ENV", single_choice)
|
|
|
|
assert result == "production"
|
|
|
|
def test_large_choice_set(self, mocker: MockerFixture) -> None:
|
|
"""Test function works correctly with large choice set."""
|
|
large_choices: set[str] = {f"option_{i}" for i in range(100)}
|
|
mocker.patch.dict("os.environ", {"TEST_ENV": "option_50"})
|
|
|
|
result = get_choice_from_env("TEST_ENV", large_choices)
|
|
|
|
assert result == "option_50"
|