test(web): assert 400 exactly, and prove valid input is accepted

Both review points were right, and the first is the failure mode this file
exists to catch.

Accepting any 4xx meant a 404 would have passed. Renaming one of these routes
would have left the test green while it tested nothing -- the same "looks like
coverage, points somewhere safe" shape that hid the composer injections. Now
asserts exactly 400.

Both infinity signs are exercised for every route. int() raises OverflowError
either way, but only +Infinity was in the original report, and a guard that
special-cased the sign would have passed a one-sided test.

The valid-input test previously asserted "not a 400", which did not show what
it claimed: the mocked save path fails for any input, so that assertion held
whether or not validation had accepted the value. It now gives load_config a
real dict and stubs _save_config_atomic, so the endpoint reaches its success
response and the test can assert 200 -- which only happens if the value passed
validation.

8 of the 11 checks fail with OverflowError removed from the except tuples.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01STMbQE4YctTacQXfbYqKuW
This commit is contained in:
ChuckBuilds
2026-08-22 16:47:33 -04:00
co-authored by Claude Opus 5
parent 0ca702bebf
commit 6eaa50fd94
+33 -21
View File
@@ -23,24 +23,34 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401,E402
#: (route, body) pairs that returned 500 before OverflowError was caught.
#: (route, field) that returned 500 before OverflowError was caught. Both
#: infinity signs are exercised: int() raises OverflowError for either, but
#: only one of them was in the original report, and a guard that special-cased
#: the sign would pass a one-sided test.
NON_FINITE_ROUTES = [
('/api/v3/config/dim-schedule', 'dim_brightness'),
('/api/v3/errors/clear', 'max_age_hours'),
('/api/v3/config/main', 'multiplexing'),
('/api/v3/config/main', 'row_address_type'),
]
NON_FINITE_CASES = [
('/api/v3/config/dim-schedule', '{"dim_brightness": Infinity}'),
('/api/v3/config/dim-schedule', '{"dim_brightness": -Infinity}'),
('/api/v3/errors/clear', '{"max_age_hours": Infinity}'),
('/api/v3/config/main', '{"multiplexing": Infinity}'),
('/api/v3/config/main', '{"row_address_type": Infinity}'),
(route, '{"%s": %s}' % (field, literal))
for route, field in NON_FINITE_ROUTES
for literal in ('Infinity', '-Infinity')
]
@pytest.mark.parametrize("route,body", NON_FINITE_CASES)
def test_infinity_is_a_client_error_not_a_server_error(api_v3_client, route, body):
"""Exactly 400, not merely "some 4xx".
Accepting any 4xx would let a 404 pass, so renaming one of these routes
would leave the test green while testing nothing -- the failure mode this
whole file exists to catch.
"""
response = api_v3_client.post(route, data=body, content_type='application/json')
assert response.status_code != 500, (
f"{route} with {body} raised instead of validating"
)
assert 400 <= response.status_code < 500, (
f"{route} answered {response.status_code}; expected a 4xx"
assert response.status_code == 400, (
f"{route} with {body} answered {response.status_code}; expected 400"
)
@@ -52,22 +62,24 @@ def test_nan_is_also_a_client_error(api_v3_client, route, body):
"""int(nan) raises ValueError so this path already worked -- pinned so a
refactor that narrows the except tuple cannot quietly break it."""
response = api_v3_client.post(route, data=body, content_type='application/json')
assert 400 <= response.status_code < 500
assert response.status_code == 400
def test_a_valid_number_is_not_rejected_by_the_guard(api_v3_client):
"""The widened except must not start swallowing ordinary input.
def test_a_valid_number_is_accepted(api_v3_client, api_v3_module, monkeypatch):
"""Prove the widened except did not start swallowing ordinary input.
Asserting on 2xx is not possible here: every manager is a MagicMock, so
the save path fails downstream whatever is posted. What this can show is
that a valid number gets past *validation* -- it is not answered with a
400, and nothing in the response mentions the coercion failing.
Asserting "not a 400" would not show that: the mocked save path fails for
any input, so the assertion would hold even if validation had rejected the
value. Give load_config a real dict and stub the atomic save, and the
endpoint reaches its success response -- which only happens if 30 passed
validation.
"""
api_v3_module.api_v3.config_manager.load_config.return_value = {}
monkeypatch.setattr(api_v3_module, '_save_config_atomic',
lambda *a, **k: (True, ''))
response = api_v3_client.post(
'/api/v3/config/dim-schedule',
data='{"dim_brightness": 30}',
content_type='application/json',
)
assert response.status_code != 400, "a valid brightness was rejected"
assert b'must be an integer' not in response.get_data()
assert b'OverflowError' not in response.get_data()
assert response.status_code == 200, response.get_data(as_text=True)[:200]