Skip to content

Commit cc4f433

Browse files
authored
fix: add configuration tests and settings validation
fix: add configuration tests and settings validation
2 parents 768173b + 8a8d172 commit cc4f433

3 files changed

Lines changed: 222 additions & 1 deletion

File tree

.vscode/settings.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"python.terminal.useEnvFile": true
3+
}

src/python_response_time/core/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ class Settings(BaseSettings):
1212
LOG_TO_STDOUT: Annotated[
1313
bool, Field(description="Whether to log to stdout (console)", strict=False)
1414
] = True
15-
1615
TARGET_URL: Annotated[
1716
list[str], Field(description="List of target endpoints for benchmarking")
1817
] = ["https://httpbin.org/get", "https://httpbin.org/status/200"]

tests/test_config.py

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
"""Unit tests for config.py in python_response_time.core."""
2+
3+
from typing import Any, cast
4+
5+
import pytest
6+
from pydantic import ValidationError
7+
8+
from python_response_time.core import config
9+
10+
11+
def test_default_values(monkeypatch):
12+
"""Test default configuration values."""
13+
monkeypatch.delenv("TARGET_URL", raising=False)
14+
monkeypatch.delenv("NUM_REQUESTS", raising=False)
15+
monkeypatch.delenv("CONNECT_TIMEOUT", raising=False)
16+
monkeypatch.delenv("READ_TIMEOUT", raising=False)
17+
monkeypatch.delenv("REQUEST_DELAY", raising=False)
18+
monkeypatch.delenv("LOG_LEVEL", raising=False)
19+
monkeypatch.delenv("VERIFY_SSL", raising=False)
20+
monkeypatch.delenv("LOG_TO_STDOUT", raising=False)
21+
22+
class TestSettings(config.Settings):
23+
model_config = config.SettingsConfigDict(
24+
env_file=None,
25+
)
26+
27+
s = TestSettings()
28+
29+
assert s.LOG_TO_STDOUT is True
30+
assert s.TARGET_URL == [
31+
"https://httpbin.org/get",
32+
"https://httpbin.org/status/200",
33+
]
34+
assert s.NUM_REQUESTS == 10
35+
assert s.CONNECT_TIMEOUT == 1.0
36+
assert s.READ_TIMEOUT == 3.0
37+
assert s.REQUEST_DELAY == 2.0
38+
assert s.LOG_LEVEL == "INFO"
39+
assert s.VERIFY_SSL is True
40+
41+
42+
def test_app_settings_instance():
43+
"""Test that the module-level settings object exists."""
44+
assert isinstance(config.app_settings, config.Settings)
45+
46+
47+
def test_env_override_num_requests(monkeypatch):
48+
"""Test environment variable override for integer values."""
49+
monkeypatch.setenv("NUM_REQUESTS", "42")
50+
51+
s = config.Settings()
52+
53+
assert s.NUM_REQUESTS == 42
54+
55+
56+
@pytest.mark.parametrize(
57+
("env_value", "expected"),
58+
[
59+
("true", True),
60+
("false", False),
61+
("1", True),
62+
("0", False),
63+
("yes", True),
64+
("no", False),
65+
],
66+
)
67+
def test_bool_env_parsing(monkeypatch, env_value, expected):
68+
"""Test boolean parsing from environment variables."""
69+
monkeypatch.setenv("VERIFY_SSL", env_value)
70+
71+
s = config.Settings()
72+
73+
assert s.VERIFY_SSL is expected
74+
75+
76+
def test_log_to_stdout_env_parsing(monkeypatch):
77+
"""Test LOG_TO_STDOUT boolean parsing from environment variable."""
78+
monkeypatch.setenv("LOG_TO_STDOUT", "true")
79+
80+
s = config.Settings()
81+
82+
assert s.LOG_TO_STDOUT is True
83+
84+
85+
def test_target_url_env_override(monkeypatch):
86+
"""Test TARGET_URL parsing from environment variable."""
87+
monkeypatch.setenv(
88+
"TARGET_URL",
89+
'["https://example.com", "https://test.com"]',
90+
)
91+
92+
s = config.Settings()
93+
94+
assert s.TARGET_URL == [
95+
"https://example.com",
96+
"https://test.com",
97+
]
98+
99+
100+
@pytest.mark.parametrize(
101+
("field", "value"),
102+
[
103+
("NUM_REQUESTS", 0),
104+
("NUM_REQUESTS", -1),
105+
("NUM_REQUESTS", 1_000_001),
106+
("CONNECT_TIMEOUT", 0),
107+
("CONNECT_TIMEOUT", -1),
108+
("CONNECT_TIMEOUT", 121),
109+
("READ_TIMEOUT", 0),
110+
("READ_TIMEOUT", -5),
111+
("READ_TIMEOUT", 121),
112+
("REQUEST_DELAY", 0),
113+
("REQUEST_DELAY", -1),
114+
("REQUEST_DELAY", 61),
115+
],
116+
)
117+
def test_invalid_numeric_values(field, value):
118+
"""Test numeric validation boundaries."""
119+
with pytest.raises(ValidationError):
120+
config.Settings(**{field: value})
121+
122+
123+
@pytest.mark.parametrize(
124+
"value",
125+
[
126+
"debug",
127+
"trace",
128+
"INVALID",
129+
"",
130+
"info",
131+
"warn",
132+
],
133+
)
134+
def test_invalid_log_level(value):
135+
"""Test invalid LOG_LEVEL values."""
136+
with pytest.raises(ValidationError):
137+
config.Settings(LOG_LEVEL=value)
138+
139+
140+
@pytest.mark.parametrize(
141+
"value",
142+
[
143+
"DEBUG",
144+
"INFO",
145+
"WARNING",
146+
"ERROR",
147+
"CRITICAL",
148+
],
149+
)
150+
def test_valid_log_levels(value):
151+
"""Test valid LOG_LEVEL values."""
152+
s = config.Settings(LOG_LEVEL=value)
153+
154+
assert s.LOG_LEVEL == value
155+
156+
157+
def test_invalid_env_value(monkeypatch):
158+
"""Test invalid environment variable values raise errors."""
159+
monkeypatch.setenv("NUM_REQUESTS", "not-an-int")
160+
161+
with pytest.raises(ValidationError):
162+
config.Settings()
163+
164+
165+
def test_extra_fields_forbidden():
166+
"""Test that extra fields are rejected."""
167+
with pytest.raises(ValidationError):
168+
config.Settings(**cast(Any, {"UNKNOWN_FIELD": "bad"}))
169+
170+
171+
def test_invalid_target_url_type():
172+
"""Test invalid TARGET_URL type."""
173+
with pytest.raises(ValidationError):
174+
config.Settings(TARGET_URL=cast(Any, "not-a-list"))
175+
176+
177+
def test_invalid_target_url_item_type():
178+
"""Test invalid TARGET_URL list item types."""
179+
with pytest.raises(ValidationError):
180+
config.Settings(TARGET_URL=cast(Any, [123, 456]))
181+
182+
183+
@pytest.mark.parametrize(
184+
("field", "value"),
185+
[
186+
("NUM_REQUESTS", 1),
187+
("NUM_REQUESTS", 1_000_000),
188+
("CONNECT_TIMEOUT", 0.001),
189+
("CONNECT_TIMEOUT", 120),
190+
("READ_TIMEOUT", 0.001),
191+
("READ_TIMEOUT", 120),
192+
("REQUEST_DELAY", 0.001),
193+
("REQUEST_DELAY", 60),
194+
],
195+
)
196+
def test_valid_numeric_boundaries(field, value):
197+
"""Test valid numeric boundary values."""
198+
s = config.Settings(**{field: value})
199+
200+
assert getattr(s, field) == value
201+
202+
203+
def test_settings_are_isolated():
204+
"""Test separate Settings instances do not share state."""
205+
s1 = config.Settings(NUM_REQUESTS=10)
206+
s2 = config.Settings(NUM_REQUESTS=20)
207+
208+
assert s1.NUM_REQUESTS == 10
209+
assert s2.NUM_REQUESTS == 20
210+
211+
212+
def test_model_config_values():
213+
"""Test important model configuration values."""
214+
cfg = config.Settings.model_config
215+
216+
assert cfg.get("extra") == "forbid"
217+
assert cfg.get("validate_default") is True
218+
assert cfg.get("env_file") == ".env"
219+
assert cfg.get("env_file_encoding") == "utf-8"

0 commit comments

Comments
 (0)