diff --git a/test/test_api_v3_non_finite_numbers.py b/test/test_api_v3_non_finite_numbers.py index c0392912..4e6c217b 100644 --- a/test/test_api_v3_non_finite_numbers.py +++ b/test/test_api_v3_non_finite_numbers.py @@ -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]