"""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