Coverage for app/backend/src/tests/test_config.py: 100%

91 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-08 09:15 +0000

1import typing 

2 

3import pytest 

4 

5from couchers.config import Config 

6from couchers.constants import DB_POOL_SIZE 

7 

8 

9def _complete_config(dev: bool) -> Config: 

10 """Build a config object attribute populated with a valid, truthy value. 

11 

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]) # type: ignore[unreachable] 

25 else: 

26 setattr(cfg, var_name, "x") 

27 

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 

36 

37 

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" 

43 

44 

45def test_load_from_env_types() -> None: 

46 cfg = Config() 

47 

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

52 

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

57 

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

62 

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

67 

68 

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" 

74 

75 

76def test_setitem() -> None: 

77 cfg = Config() 

78 

79 cfg["BASE_URL"] = "https://example.com" 

80 assert cfg.BASE_URL == "https://example.com" 

81 assert cfg["BASE_URL"] == "https://example.com" 

82 

83 with pytest.raises(KeyError): 

84 cfg["NOT_A_KEY"] = "value" 

85 

86 with pytest.raises(TypeError): 

87 cfg["BASE_URL"] = 123 

88 

89 cfg["ROLE"] = "worker" 

90 assert cfg.ROLE == "worker" 

91 with pytest.raises(ValueError): 

92 cfg["ROLE"] = "not a valid role" 

93 

94 

95def test_instances_state_are_independent() -> None: 

96 # Default values are declared at the class level, but should be copied to each instance. 

97 assert Config.IN_TEST is False 

98 

99 cfg1 = Config() 

100 cfg2 = Config() 

101 

102 assert cfg1.IN_TEST is False 

103 assert cfg2.IN_TEST is False 

104 

105 cfg1.IN_TEST = True 

106 

107 assert cfg1.IN_TEST is True 

108 assert cfg2.IN_TEST is False 

109 

110 

111def test_copy() -> None: 

112 cfg = Config() 

113 

114 cfg.BACKGROUND_WORKER_PROCESSES = 1 

115 copy1 = cfg.copy() 

116 cfg.BACKGROUND_WORKER_PROCESSES = 2 

117 copy2 = cfg.copy() 

118 

119 assert copy1.BACKGROUND_WORKER_PROCESSES == 1 

120 assert copy2.BACKGROUND_WORKER_PROCESSES == 2 

121 

122 

123@pytest.mark.parametrize("dev", [True, False]) 

124def test_check_config_only_references_known_keys(dev): 

125 """Config.check() must only access config keys that are declared as attributes. 

126 

127 A reference to a key that was removed from attributes (e.g. a toggle migrated to a feature 

128 flag) would raise KeyError at app boot but is invisible to the rest of the test suite, since 

129 Config.check() only runs in app.py's startup path. Exercising it here catches that. 

130 """ 

131 _complete_config(dev=dev).check() 

132 

133 

134def test_check_rejects_too_many_worker_threads() -> None: 

135 cfg = _complete_config(dev=True) 

136 cfg.BACKGROUND_WORKER_THREADS_PER_PROCESS = DB_POOL_SIZE // 2 

137 cfg.check() 

138 

139 cfg.BACKGROUND_WORKER_THREADS_PER_PROCESS = DB_POOL_SIZE // 2 + 1 

140 with pytest.raises(Exception, match="BACKGROUND_WORKER_THREADS_PER_PROCESS"): 

141 cfg.check()