Coverage for app/backend/src/tests/test_config.py: 100%
87 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 13:46 +0000
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 13:46 +0000
1import typing
3import pytest
5from couchers.config import Config
6from couchers.constants import DB_POOL_SIZE
9def _complete_config(dev: bool) -> Config:
10 """Build a config object attribute populated with a valid, truthy value.
12 This mirrors what Config.load_from_env() produces when every env var is set,
13 so Config.check() should succeed against it.
14 """
15 cfg = Config()
16 for var_name, var_type in Config.__annotations__.items():
17 if var_type is bool:
18 setattr(cfg, var_name, True)
19 elif var_type is int:
20 setattr(cfg, var_name, 1)
21 elif var_type is bytes:
22 setattr(cfg, var_name, b"x")
23 elif typing.get_origin(var_type) is typing.Literal: # type: ignore[comparison-overlap]
24 setattr(cfg, var_name, typing.get_args(var_type)[0])
25 else:
26 setattr(cfg, var_name, "x")
28 cfg.DEV = dev
29 if not dev:
30 # production invariants that aren't satisfiable by a generic truthy value
31 cfg.BASE_URL = "https://example.com"
32 cfg.ENABLE_EMAIL = True
33 cfg.IN_TEST = False
34 cfg.FEATURE_FLAGS_FILE_OVERRIDE_PATH = ""
35 return cfg
38def test_load_from_env() -> None:
39 cfg = Config()
40 assert not hasattr(cfg, "BASE_URL")
41 cfg.load_from_env({"BASE_URL": "https://example.com"})
42 assert cfg.BASE_URL == "https://example.com"
45def test_load_from_env_types() -> None:
46 cfg = Config()
48 cfg.load_from_env({"IN_TEST": "1"})
49 assert cfg.IN_TEST is True
50 with pytest.raises(ValueError):
51 cfg.load_from_env({"IN_TEST": "not a bool"})
53 cfg.load_from_env({"BACKGROUND_WORKER_PROCESSES": "42"})
54 assert cfg.BACKGROUND_WORKER_PROCESSES == 42
55 with pytest.raises(ValueError):
56 cfg.load_from_env({"BACKGROUND_WORKER_PROCESSES": "not an int"})
58 cfg.load_from_env({"SECRET": bytes.hex(b"abc")})
59 assert cfg.SECRET == b"abc"
60 with pytest.raises(ValueError):
61 cfg.load_from_env({"SECRET": "not hex"})
63 cfg.load_from_env({"ROLE": "worker"})
64 assert cfg.ROLE == "worker"
65 with pytest.raises(ValueError):
66 cfg.load_from_env({"ROLE": "not a valid role"})
69def test_getitem() -> None:
70 cfg = Config()
71 cfg.BASE_URL = "https://example.com"
72 assert cfg.BASE_URL == "https://example.com"
73 assert cfg["BASE_URL"] == "https://example.com"
76def test_setitem() -> None:
77 cfg = Config()
79 cfg["BASE_URL"] = "https://example.com"
80 assert cfg.BASE_URL == "https://example.com"
81 assert cfg["BASE_URL"] == "https://example.com"
83 with pytest.raises(KeyError):
84 cfg["NOT_A_KEY"] = "value"
86 with pytest.raises(TypeError):
87 cfg["BASE_URL"] = 123
90def test_instances_state_are_independent() -> None:
91 # Default values are declared at the class level, but should be copied to each instance.
92 assert Config.IN_TEST is False
94 cfg1 = Config()
95 cfg2 = Config()
97 assert cfg1.IN_TEST is False
98 assert cfg2.IN_TEST is False
100 cfg1.IN_TEST = True
102 assert cfg1.IN_TEST is True
103 assert cfg2.IN_TEST is False
106def test_copy() -> None:
107 cfg = Config()
109 cfg.BACKGROUND_WORKER_PROCESSES = 1
110 copy1 = cfg.copy()
111 cfg.BACKGROUND_WORKER_PROCESSES = 2
112 copy2 = cfg.copy()
114 assert copy1.BACKGROUND_WORKER_PROCESSES == 1
115 assert copy2.BACKGROUND_WORKER_PROCESSES == 2
118@pytest.mark.parametrize("dev", [True, False])
119def test_check_config_only_references_known_keys(dev):
120 """Config.check() must only access config keys that are declared as attributes.
122 A reference to a key that was removed from attributes (e.g. a toggle migrated to a feature
123 flag) would raise KeyError at app boot but is invisible to the rest of the test suite, since
124 Config.check() only runs in app.py's startup path. Exercising it here catches that.
125 """
126 _complete_config(dev=dev).check()
129def test_check_rejects_too_many_worker_threads() -> None:
130 cfg = _complete_config(dev=True)
131 cfg.BACKGROUND_WORKER_THREADS_PER_PROCESS = DB_POOL_SIZE // 2
132 cfg.check()
134 cfg.BACKGROUND_WORKER_THREADS_PER_PROCESS = DB_POOL_SIZE // 2 + 1
135 with pytest.raises(Exception, match="BACKGROUND_WORKER_THREADS_PER_PROCESS"):
136 cfg.check()