mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-06-08 14:49:44 +00:00
Finish off the testing coverage
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from paperless.settings.custom import parse_db_settings
|
||||
|
||||
|
||||
class TestParseDbSettings:
|
||||
"""Test suite for parse_db_settings function."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("env_vars", "expected_database_settings"),
|
||||
[
|
||||
pytest.param(
|
||||
{},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="default-sqlite",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "sqlite",
|
||||
"PAPERLESS_DB_OPTIONS": "timeout=30",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {
|
||||
"timeout": 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
id="sqlite-with-timeout-override",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "localhost",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "localhost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "prefer",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "paperless-db-host",
|
||||
"PAPERLESS_DBPORT": "1111",
|
||||
"PAPERLESS_DBNAME": "customdb",
|
||||
"PAPERLESS_DBUSER": "customuser",
|
||||
"PAPERLESS_DBPASS": "custompass",
|
||||
"PAPERLESS_DB_OPTIONS": "pool.max_size=50;pool.min_size=2;sslmode=require",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "paperless-db-host",
|
||||
"PORT": 1111,
|
||||
"NAME": "customdb",
|
||||
"USER": "customuser",
|
||||
"PASSWORD": "custompass",
|
||||
"OPTIONS": {
|
||||
"sslmode": "require",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"pool": {
|
||||
"min_size": 2,
|
||||
"max_size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-overrides",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "pghost",
|
||||
"PAPERLESS_DB_POOLSIZE": "10",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "pghost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "prefer",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"pool": {
|
||||
"min_size": 1,
|
||||
"max_size": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-legacy-poolsize",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "pghost",
|
||||
"PAPERLESS_DBSSLMODE": "require",
|
||||
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.crt",
|
||||
"PAPERLESS_DB_TIMEOUT": "30",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "pghost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "require",
|
||||
"sslrootcert": "/certs/ca.crt",
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"connect_timeout": 30,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-legacy-ssl-and-timeout",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "localhost",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "localhost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "PREFERRED",
|
||||
"ssl": {
|
||||
"ca": None,
|
||||
"cert": None,
|
||||
"key": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "paperless-mariadb-host",
|
||||
"PAPERLESS_DBPORT": "5555",
|
||||
"PAPERLESS_DBUSER": "my-cool-user",
|
||||
"PAPERLESS_DBPASS": "my-secure-password",
|
||||
"PAPERLESS_DB_OPTIONS": "ssl.ca=/path/to/ca.pem;ssl_mode=REQUIRED",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "paperless-mariadb-host",
|
||||
"PORT": 5555,
|
||||
"NAME": "paperless",
|
||||
"USER": "my-cool-user",
|
||||
"PASSWORD": "my-secure-password",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "REQUIRED",
|
||||
"ssl": {
|
||||
"ca": "/path/to/ca.pem",
|
||||
"cert": None,
|
||||
"key": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-overrides",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "mariahost",
|
||||
"PAPERLESS_DBSSLMODE": "REQUIRED",
|
||||
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.pem",
|
||||
"PAPERLESS_DBSSLCERT": "/certs/client.pem",
|
||||
"PAPERLESS_DBSSLKEY": "/certs/client.key",
|
||||
"PAPERLESS_DB_TIMEOUT": "25",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "mariahost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "REQUIRED",
|
||||
"ssl": {
|
||||
"ca": "/certs/ca.pem",
|
||||
"cert": "/certs/client.pem",
|
||||
"key": "/certs/client.key",
|
||||
},
|
||||
"connect_timeout": 25,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-legacy-ssl-and-timeout",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_db_settings(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
env_vars: dict[str, str],
|
||||
expected_database_settings: dict[str, dict],
|
||||
) -> None:
|
||||
"""Test various database configurations with defaults and overrides."""
|
||||
# Clear environment and set test vars
|
||||
mocker.patch.dict(os.environ, env_vars, clear=True)
|
||||
|
||||
# Update expected paths with actual tmp_path
|
||||
if (
|
||||
"default" in expected_database_settings
|
||||
and expected_database_settings["default"]["NAME"] is None
|
||||
):
|
||||
expected_database_settings["default"]["NAME"] = str(
|
||||
tmp_path / "db.sqlite3",
|
||||
)
|
||||
if "sqlite" in expected_database_settings:
|
||||
expected_database_settings["sqlite"]["NAME"] = str(
|
||||
tmp_path / "db.sqlite3",
|
||||
)
|
||||
|
||||
settings = parse_db_settings(tmp_path)
|
||||
|
||||
assert settings == expected_database_settings
|
||||
@@ -0,0 +1,414 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from paperless.settings.parsers import get_choice_from_env
|
||||
from paperless.settings.parsers import get_int_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):
|
||||
"""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):
|
||||
"""Test that various 'false' strings correctly evaluate to False."""
|
||||
assert str_to_bool(false_value) is False
|
||||
|
||||
def test_invalid_conversion(self):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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 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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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):
|
||||
"""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 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_raises_error_when_env_not_set_and_no_default(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
valid_choices: set[str],
|
||||
) -> None:
|
||||
"""Test that function raises ValueError when env var is missing and no default."""
|
||||
mocker.patch.dict("os.environ", {}, clear=True)
|
||||
|
||||
with pytest.raises(ValueError) as exc_info:
|
||||
get_choice_from_env("TEST_ENV", valid_choices)
|
||||
|
||||
assert "Environment variable 'TEST_ENV' is required but not set" in str(
|
||||
exc_info.value,
|
||||
)
|
||||
|
||||
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"
|
||||
|
||||
def test_different_env_keys(
|
||||
self,
|
||||
mocker: MockerFixture,
|
||||
valid_choices: set[str],
|
||||
) -> None:
|
||||
"""Test function works with different environment variable keys."""
|
||||
test_cases = [
|
||||
("DJANGO_ENV", "development"),
|
||||
("DATABASE_BACKEND", "staging"),
|
||||
("LOG_LEVEL", "production"),
|
||||
("APP_MODE", "development"),
|
||||
]
|
||||
|
||||
for env_key, env_value in test_cases:
|
||||
mocker.patch.dict("os.environ", {env_key: env_value})
|
||||
result = get_choice_from_env(env_key, valid_choices)
|
||||
assert result == env_value
|
||||
@@ -1,12 +1,10 @@
|
||||
import datetime
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest import TestCase
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from celery.schedules import crontab
|
||||
from pytest_mock import MockerFixture
|
||||
|
||||
from paperless.settings import _parse_base_paths
|
||||
from paperless.settings import _parse_beat_schedule
|
||||
@@ -15,7 +13,6 @@ from paperless.settings import _parse_ignore_dates
|
||||
from paperless.settings import _parse_paperless_url
|
||||
from paperless.settings import _parse_redis_url
|
||||
from paperless.settings import default_threads_per_worker
|
||||
from paperless.settings.custom import parse_db_settings
|
||||
|
||||
|
||||
class TestIgnoreDateParsing(TestCase):
|
||||
@@ -380,304 +377,6 @@ class TestCeleryScheduleParsing(TestCase):
|
||||
)
|
||||
|
||||
|
||||
class TestParseDbSettings:
|
||||
"""Test suite for parse_db_settings function."""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("env_vars", "expected_database_settings"),
|
||||
[
|
||||
pytest.param(
|
||||
{},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="default-sqlite",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "sqlite",
|
||||
"PAPERLESS_DB_OPTIONS": "timeout=30",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {
|
||||
"timeout": 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
id="sqlite-with-timeout-override",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "localhost",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "localhost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "prefer",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "paperless-db-host",
|
||||
"PAPERLESS_DBPORT": "1111",
|
||||
"PAPERLESS_DBNAME": "customdb",
|
||||
"PAPERLESS_DBUSER": "customuser",
|
||||
"PAPERLESS_DBPASS": "custompass",
|
||||
"PAPERLESS_DB_OPTIONS": "pool.max_size=50;pool.min_size=2;sslmode=require",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "paperless-db-host",
|
||||
"PORT": 1111,
|
||||
"NAME": "customdb",
|
||||
"USER": "customuser",
|
||||
"PASSWORD": "custompass",
|
||||
"OPTIONS": {
|
||||
"sslmode": "require",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"pool": {
|
||||
"min_size": 2,
|
||||
"max_size": 50,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-overrides",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "pghost",
|
||||
"PAPERLESS_DB_POOLSIZE": "10",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "pghost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "prefer",
|
||||
"sslrootcert": None,
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"pool": {
|
||||
"min_size": 1,
|
||||
"max_size": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-legacy-poolsize",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "postgresql",
|
||||
"PAPERLESS_DBHOST": "pghost",
|
||||
"PAPERLESS_DBSSLMODE": "require",
|
||||
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.crt",
|
||||
"PAPERLESS_DB_TIMEOUT": "30",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.postgresql",
|
||||
"HOST": "pghost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"sslmode": "require",
|
||||
"sslrootcert": "/certs/ca.crt",
|
||||
"sslcert": None,
|
||||
"sslkey": None,
|
||||
"connect_timeout": 30,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="postgresql-legacy-ssl-and-timeout",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "localhost",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "localhost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "PREFERRED",
|
||||
"ssl": {
|
||||
"ca": None,
|
||||
"cert": None,
|
||||
"key": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-defaults",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "paperless-mariadb-host",
|
||||
"PAPERLESS_DBPORT": "5555",
|
||||
"PAPERLESS_DBUSER": "my-cool-user",
|
||||
"PAPERLESS_DBPASS": "my-secure-password",
|
||||
"PAPERLESS_DB_OPTIONS": "ssl.ca=/path/to/ca.pem;ssl_mode=REQUIRED",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "paperless-mariadb-host",
|
||||
"PORT": 5555,
|
||||
"NAME": "paperless",
|
||||
"USER": "my-cool-user",
|
||||
"PASSWORD": "my-secure-password",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "REQUIRED",
|
||||
"ssl": {
|
||||
"ca": "/path/to/ca.pem",
|
||||
"cert": None,
|
||||
"key": None,
|
||||
},
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-overrides",
|
||||
),
|
||||
pytest.param(
|
||||
{
|
||||
"PAPERLESS_DBENGINE": "mariadb",
|
||||
"PAPERLESS_DBHOST": "mariahost",
|
||||
"PAPERLESS_DBSSLMODE": "REQUIRED",
|
||||
"PAPERLESS_DBSSLROOTCERT": "/certs/ca.pem",
|
||||
"PAPERLESS_DBSSLCERT": "/certs/client.pem",
|
||||
"PAPERLESS_DBSSLKEY": "/certs/client.key",
|
||||
"PAPERLESS_DB_TIMEOUT": "25",
|
||||
},
|
||||
{
|
||||
"default": {
|
||||
"ENGINE": "django.db.backends.mysql",
|
||||
"HOST": "mariahost",
|
||||
"NAME": "paperless",
|
||||
"USER": "paperless",
|
||||
"PASSWORD": "paperless",
|
||||
"OPTIONS": {
|
||||
"read_default_file": "/etc/mysql/my.cnf",
|
||||
"charset": "utf8mb4",
|
||||
"collation": "utf8mb4_unicode_ci",
|
||||
"ssl_mode": "REQUIRED",
|
||||
"ssl": {
|
||||
"ca": "/certs/ca.pem",
|
||||
"cert": "/certs/client.pem",
|
||||
"key": "/certs/client.key",
|
||||
},
|
||||
"connect_timeout": 25,
|
||||
},
|
||||
},
|
||||
"sqlite": {
|
||||
"ENGINE": "django.db.backends.sqlite3",
|
||||
"NAME": None, # Will be replaced with tmp_path
|
||||
"OPTIONS": {},
|
||||
},
|
||||
},
|
||||
id="mariadb-legacy-ssl-and-timeout",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_parse_db_settings(
|
||||
self,
|
||||
tmp_path: Path,
|
||||
mocker: MockerFixture,
|
||||
env_vars: dict[str, str],
|
||||
expected_database_settings: dict[str, dict],
|
||||
) -> None:
|
||||
"""Test various database configurations with defaults and overrides."""
|
||||
# Clear environment and set test vars
|
||||
mocker.patch.dict(os.environ, env_vars, clear=True)
|
||||
|
||||
# Update expected paths with actual tmp_path
|
||||
if (
|
||||
"default" in expected_database_settings
|
||||
and expected_database_settings["default"]["NAME"] is None
|
||||
):
|
||||
expected_database_settings["default"]["NAME"] = str(
|
||||
tmp_path / "db.sqlite3",
|
||||
)
|
||||
if "sqlite" in expected_database_settings:
|
||||
expected_database_settings["sqlite"]["NAME"] = str(
|
||||
tmp_path / "db.sqlite3",
|
||||
)
|
||||
|
||||
settings = parse_db_settings(tmp_path)
|
||||
|
||||
assert settings == expected_database_settings
|
||||
|
||||
|
||||
class TestPaperlessURLSettings(TestCase):
|
||||
def test_paperless_url(self) -> None:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user