Files
tim ca3d730129 Turn CWA off unless CWA_ENABLED is true
CWA_ENABLED now defaults to false. If a CWA variable is set while CWA is off, the start log has a warning that names it.

On the Settings page, the alert about INGEST_DIR moves to Book Downloads and shows with CWA on or off. The Uploads to CWA card moves below Book Downloads.
2026-10-06 16:59:15 +02:00

298 lines
12 KiB
Python

"""Reading the configuration from environment variables."""
import importlib
from pathlib import Path
import pytest
from bindery import config
@pytest.fixture
def reload_config(monkeypatch):
"""Load the config module again with a changed environment, and restore it afterwards."""
yield lambda: importlib.reload(config)
monkeypatch.undo()
importlib.reload(config)
@pytest.fixture
def folders(monkeypatch, tmp_path):
"""Put the folders of Bindery in a temporary folder. The ingest folder does not exist yet."""
for name, folder in [("CONFIG_DIR", "config"), ("UPLOAD_DIR", "uploads"), ("TMP_DIR", "tmp"), ("INGEST_DIR", "in")]:
monkeypatch.setattr(config, name, tmp_path / folder)
monkeypatch.setattr(config, "PERSONAL_INI", tmp_path / "config" / "personal.ini")
def test_bool(monkeypatch):
for value in ("true", "Yes", " 1 ", "on", "y"):
monkeypatch.setenv("X_TEST", value)
assert config._bool("X_TEST")
for value in ("false", "0", "no", ""):
monkeypatch.setenv("X_TEST", value)
assert not config._bool("X_TEST")
monkeypatch.delenv("X_TEST")
assert config._bool("X_TEST", True)
# A value that is not clear gives the default.
monkeypatch.setenv("X_TEST", "maybe")
assert config._bool("X_TEST", True) and not config._bool("X_TEST")
def test_list(monkeypatch):
monkeypatch.setenv("X_TEST", " title, tags ,,series ")
assert config._list("X_TEST") == ["title", "tags", "series"]
monkeypatch.delenv("X_TEST")
assert config._list("X_TEST") == []
assert config._list("X_TEST", "title") == ["title"]
def test_relative_paths_become_absolute(monkeypatch, tmp_path, reload_config):
monkeypatch.chdir(tmp_path)
for name, value in [("CONFIG_DIR", "dev/config"), ("INGEST_DIR", "dev/ingest"), ("TMP_DIR", "dev/tmp")]:
monkeypatch.setenv(name, value)
monkeypatch.setenv("CWA_DB_PATH", "cwa/app.db")
monkeypatch.setenv("CALIBRE_DB_PATH", "library/metadata.db")
reload_config()
assert config.CONFIG_DIR == Path.cwd() / "dev/config"
assert config.INGEST_DIR == Path.cwd() / "dev/ingest"
assert config.TMP_DIR == Path.cwd() / "dev/tmp"
assert config.CWA_DB_PATH == Path.cwd() / "cwa/app.db"
assert config.CALIBRE_DB_PATH == Path.cwd() / "library/metadata.db"
@pytest.mark.parametrize(
("value", "expected"),
[
("", ""),
("/", ""),
("bindery", "/bindery"),
("/bindery/", "/bindery"),
(" /a/b ", "/a/b"),
],
)
def test_base_path(value, expected):
assert config._base_path(value) == expected
def test_values_from_the_environment(monkeypatch, reload_config):
monkeypatch.setenv("URL_BASE", "bindery/")
monkeypatch.setenv("AUTH_METHOD", " CWA ")
monkeypatch.setenv("UPDATE_INTERVAL_HOURS", "1.5")
monkeypatch.setenv("DEFAULT_LOCKED_FIELDS", "title, authors")
monkeypatch.setenv("CWA_ALLOWED_USERS", "alice,bob")
monkeypatch.setenv("BINDERY_ADMIN_USERS", "alice")
monkeypatch.setenv("CONFIG_DIR", "/data/bindery")
monkeypatch.setenv("TRUST_PROXY", "yes")
monkeypatch.setenv("CWA_DB_PATH", " /cwa/app.db ")
monkeypatch.setenv("CALIBRE_DB_PATH", "/library/metadata.db")
monkeypatch.setenv("BINDERY_USERNAME", " reader ")
monkeypatch.setenv("BINDERY_PASSWORD_HASH", " hash ")
monkeypatch.setenv("ALLOWED_HOSTS", "Bindery.Example.com, .home.example")
reload_config()
assert config.ALLOWED_HOSTS == ["bindery.example.com", ".home.example"]
assert config.URL_BASE == "/bindery"
assert config.AUTH_METHOD == "cwa"
assert config.UPDATE_INTERVAL_HOURS == 1.5
assert config.DEFAULT_LOCKED_FIELDS == ["title", "authors"]
assert config.CWA_ALLOWED_USERS == ["alice", "bob"]
assert config.ADMIN_USERS == ["alice"]
assert str(config.DB_PATH) == "/data/bindery/bindery.db"
assert config.TRUST_PROXY is True
assert str(config.CWA_DB_PATH) == "/cwa/app.db"
assert str(config.CALIBRE_DB_PATH) == "/library/metadata.db"
assert config.LOCAL_USERNAME == "reader"
assert config.LOCAL_PASSWORD_HASH == "hash"
def test_defaults(monkeypatch, reload_config):
for name in (
"URL_BASE",
"AUTH_METHOD",
"UPDATE_INTERVAL_HOURS",
"DEFAULT_LOCKED_FIELDS",
"LOG_LEVEL",
"PROXY_AUTH_USER_HEADER",
"PROXY_AUTH_TRUSTED_IPS",
"ALLOWED_HOSTS",
"BINDERY_ADMIN_USERS",
"CWA_ENABLED",
"SESSION_COOKIE_SECURE",
"TRUST_PROXY",
"CWA_DB_PATH",
"CALIBRE_DB_PATH",
"INGEST_DIR",
"BINDERY_USERNAME",
"BINDERY_PASSWORD_HASH",
):
monkeypatch.delenv(name, raising=False)
reload_config()
assert config.URL_BASE == ""
assert config.AUTH_METHOD == "none"
assert config.UPDATE_INTERVAL_HOURS == 24
assert config.DEFAULT_LOCKED_FIELDS == ["title"]
assert config.LOG_LEVEL == "INFO"
assert config.PROXY_AUTH_USER_HEADER == "X-Remote-User"
assert config.PROXY_AUTH_TRUSTED_IPS == [
"127.0.0.0/8",
"::1",
"10.0.0.0/8",
"172.16.0.0/12",
"192.168.0.0/16",
"fc00::/7",
"fe80::/10",
]
assert config.ALLOWED_HOSTS == []
assert config.ADMIN_USERS == []
assert config.CWA_ENABLED is False
assert config.IGNORED_SETTINGS == []
assert config.SESSION_COOKIE_SECURE is False
assert config.TRUST_PROXY is False
assert config.CWA_DB_PATH is None
assert config.CALIBRE_DB_PATH is None
assert config.INGEST_DIR is None
assert config.LOCAL_USERNAME == ""
assert config.LOCAL_PASSWORD_HASH == ""
@pytest.mark.parametrize("value", ["", " "])
def test_empty_ingest_directory_disables_uploads(monkeypatch, reload_config, value):
monkeypatch.setenv("INGEST_DIR", value)
reload_config()
assert config.INGEST_DIR is None
@pytest.mark.parametrize("value", [None, "", "false", " No ", "0", "maybe"])
def test_cwa_off_ignores_the_cwa_settings_but_not_the_ingest_folder(monkeypatch, reload_config, value):
if value is None:
monkeypatch.delenv("CWA_ENABLED")
else:
monkeypatch.setenv("CWA_ENABLED", value)
monkeypatch.setenv("CWA_URL", "https://cwa.invalid")
monkeypatch.setenv("CWA_USERNAME", "bindery")
monkeypatch.setenv("CWA_PASSWORD", "secret")
monkeypatch.setenv("CWA_DB_PATH", "/cwa/app.db")
monkeypatch.setenv("CALIBRE_DB_PATH", "/library/metadata.db")
monkeypatch.setenv("INGEST_DIR", "/books")
reload_config()
assert config.CWA_ENABLED is False
assert (config.CWA_URL, config.CWA_USERNAME, config.CWA_PASSWORD) == ("", "", "")
assert (config.CWA_DB_PATH, config.CALIBRE_DB_PATH) == (None, None)
assert config.IGNORED_SETTINGS == ["CWA_URL", "CWA_USERNAME", "CWA_PASSWORD", "CWA_DB_PATH", "CALIBRE_DB_PATH"]
assert str(config.INGEST_DIR) == "/books"
@pytest.mark.parametrize("value", ["true", " Yes ", "1", "on"])
def test_only_a_clear_true_turns_cwa_on(monkeypatch, reload_config, value):
monkeypatch.setenv("CWA_ENABLED", value)
monkeypatch.setenv("CWA_URL", "https://cwa.invalid")
reload_config()
assert config.CWA_ENABLED is True
assert config.CWA_URL == "https://cwa.invalid"
@pytest.mark.parametrize("ignored", [[], ["CWA_URL", "CALIBRE_DB_PATH"]])
def test_prepare_with_cwa_off_logs_nothing_about_missing_cwa(folders, monkeypatch, tmp_path, caplog, ignored):
monkeypatch.setattr(config, "CWA_ENABLED", False)
monkeypatch.setattr(config, "IGNORED_SETTINGS", ignored)
monkeypatch.setattr(config, "INGEST_DIR", None)
monkeypatch.setattr(config, "CWA_URL", "")
monkeypatch.setattr(config, "CALIBRE_DB_PATH", None)
with caplog.at_level("INFO"):
config.prepare()
lines = [r.getMessage() for r in caplog.records]
ignores = "CWA_ENABLED is not true. Bindery ignores CWA_URL, CALIBRE_DB_PATH. To use CWA, set CWA_ENABLED=true."
expected = [ignores] if ignored else []
assert [r.levelname for r in caplog.records if "CWA" in r.getMessage()] == ["WARNING"] * len(expected)
assert [line for line in lines if "CWA" in line or "INGEST" in line] == expected
@pytest.mark.parametrize(
("ingest", "url", "database", "expected"),
[
(False, "", None, ["INGEST_DIR is not set", "cannot find the library book of a story.", "remove CWA_ENABLED"]),
(True, "", None, ["so each update adds a new book to CWA.", "remove CWA_ENABLED"]),
(True, "", Path("/library/metadata.db"), ["CWA_URL is not set. Bindery cannot search", "remove CWA_ENABLED"]),
(False, "https://cwa.invalid", None, ["INGEST_DIR is not set", "remove CWA_ENABLED"]),
(True, "https://cwa.invalid", None, []),
],
)
def test_prepare_warns_about_each_missing_cwa_function(
folders, monkeypatch, tmp_path, caplog, ingest, url, database, expected
):
monkeypatch.setattr(config, "CWA_URL", url)
monkeypatch.setattr(config, "CALIBRE_DB_PATH", database)
monkeypatch.setattr(config, "INGEST_DIR", tmp_path if ingest else None)
with caplog.at_level("WARNING"):
config.prepare()
warnings = [r.getMessage() for r in caplog.records]
assert len(warnings) == len(expected)
for warning, text in zip(warnings, expected, strict=True):
assert text in warning
def test_prepare_without_cwa_creates_only_bindery_folders(folders, monkeypatch, tmp_path, caplog):
monkeypatch.setattr(config, "CWA_ENABLED", False)
monkeypatch.setattr(config, "INGEST_DIR", None)
monkeypatch.setattr(config, "CWA_URL", "")
monkeypatch.setattr(config, "CALIBRE_DB_PATH", None)
config.prepare()
assert config.PERSONAL_INI.exists()
assert {p.name for p in tmp_path.iterdir()} == {"config", "uploads", "tmp"}
assert caplog.records == []
@pytest.mark.parametrize(
("url", "database", "warns"),
[("", None, True), ("https://cwa.invalid", None, False), ("", Path("/library/metadata.db"), False)],
)
def test_prepare_warns_about_an_ingest_folder_without_a_library(
folders, monkeypatch, tmp_path, caplog, url, database, warns
):
monkeypatch.setattr(config, "CWA_URL", url)
monkeypatch.setattr(config, "CALIBRE_DB_PATH", database)
with caplog.at_level("WARNING"):
config.prepare()
assert ("each update adds a new book" in caplog.text) is warns
@pytest.mark.parametrize(
("name", "value", "error"),
[
("UPDATE_INTERVAL_HOURS", "daily", "UPDATE_INTERVAL_HOURS must be a number, not 'daily'"),
("LOG_LEVEL", "loud", "LOG_LEVEL must be one of DEBUG, INFO, WARNING, ERROR, CRITICAL, not 'LOUD'"),
],
)
def test_a_bad_value_stops_the_start_with_a_clear_message(monkeypatch, reload_config, name, value, error):
monkeypatch.setenv(name, value)
with pytest.raises(SystemExit) as stop:
reload_config()
assert str(stop.value) == error
def test_log_level_in_lower_case(monkeypatch, reload_config):
monkeypatch.setenv("LOG_LEVEL", " debug ")
reload_config()
assert config.LOG_LEVEL == "DEBUG"
@pytest.mark.parametrize(("username", "warns"), [("", True), ("bindery", False)])
def test_prepare_warns_about_a_cwa_password_without_a_user_name(
folders, monkeypatch, tmp_path, caplog, username, warns
):
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
monkeypatch.setattr(config, "CWA_USERNAME", username)
monkeypatch.setattr(config, "CWA_PASSWORD", "secret")
with caplog.at_level("WARNING"):
config.prepare()
assert ("CWA_PASSWORD is set" in caplog.text) is warns
@pytest.mark.parametrize("exists", [False, True])
def test_prepare_does_not_make_the_ingest_folder(folders, monkeypatch, tmp_path, caplog, exists):
monkeypatch.setattr(config, "CWA_URL", "https://cwa.invalid")
if exists:
config.INGEST_DIR.mkdir()
config.prepare()
assert config.INGEST_DIR.exists() is exists
assert ("does not exist. Check INGEST_DIR and the volume mount" in caplog.text) is not exists