From d1f649b32d5a9915e582dffb4bd543883642a47e Mon Sep 17 00:00:00 2001 From: riddim-developer-bot Date: Sun, 14 Jun 2026 15:07:53 -0400 Subject: [PATCH] [EPAC-2285]: Assert application-level 404 for unknown bill diff in staging smoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bills version diff route was exposed and route-reachability checked by EPAC-2288 via bills:diff-route, which drives the missing-parameter HTTP 400 path. The regression this issue names — distinguishing an API Gateway route-missing 404 ({"message":"Not Found"}) from the bills service's own application-level 404 for an unknown bill/version — was not exercised by any default-mode smoke check. Add a deterministic bills:diff-unknown check: GET an unknown bill id with from/to set so the bills Lambda returns its own 404 ("bill not found") before any version/diff lookup. The validator fails on the API Gateway 404, requires a service-owned error body, and tolerates a warming 503. It runs in default contract mode for the bills service in both staging and production, so no backfilled diff data is required. --- scripts/ci/backend_staging_smoke.py | 26 ++++++++++++++++++++++++ scripts/ci/test_backend_staging_smoke.py | 23 +++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/scripts/ci/backend_staging_smoke.py b/scripts/ci/backend_staging_smoke.py index 96644633..b342a12c 100755 --- a/scripts/ci/backend_staging_smoke.py +++ b/scripts/ci/backend_staging_smoke.py @@ -152,6 +152,21 @@ def validate_bill_diff_payload(status: int, payload: Any) -> None: require_non_empty_list(body, "bills:diff-full", "clauses") +def validate_bill_diff_unknown(status: int, payload: Any) -> None: + body = require_dict(payload, "bills:diff-unknown") + if is_api_gateway_not_found(status, body): + raise SmokeFailure("bills:diff-unknown: API Gateway returned Not Found; route is missing or unsynced") + if "error" not in body: + raise SmokeFailure( + "bills:diff-unknown: expected service-owned error body (key 'error'), got keys: " + + (", ".join(sorted(body)) or "none") + ) + if status == 404 and "not found" not in str(body["error"]).lower(): + raise SmokeFailure( + f"bills:diff-unknown: 404 body is not a documented not-found message: {body['error']}" + ) + + def validate_members(_: int, payload: Any) -> None: body = require_dict(payload, "members") require_keys(body, "members", {"members"}) @@ -344,6 +359,17 @@ def validate_hansard_search_manifest(status: int, payload: Any) -> None: deterministic_note="Route-reachability check omits from/to so the bills service returns its own HTTP 400 before diff data is required.", fixture_note="No backfilled diff fixture required; API Gateway 404 is treated as a route exposure failure.", ), + SmokeCheck( + name="bills:diff-unknown", + method="GET", + path="/api/v1/bills/ZZ-9999/diff", + query={"from": "v1", "to": "v2"}, + expected_statuses={404, 503}, + validator=validate_bill_diff_unknown, + service="bills", + deterministic_note="Negative check — an unknown bill id with from/to set drives the bills service's own application-level 404 ('bill not found'), proving the route reaches the Lambda and is distinguished from an API Gateway route-missing 404.", + fixture_note="No backfilled diff data required; an unknown bill returns 404 before any version/diff lookup. HTTP 503 is tolerated while the bills index warms.", + ), SmokeCheck( name="bills:diff-full", method="GET", diff --git a/scripts/ci/test_backend_staging_smoke.py b/scripts/ci/test_backend_staging_smoke.py index 2303ec76..172a0fa2 100644 --- a/scripts/ci/test_backend_staging_smoke.py +++ b/scripts/ci/test_backend_staging_smoke.py @@ -109,6 +109,28 @@ def test_bill_diff_full_validator_requires_seeded_payload(): ) +def test_bill_diff_unknown_validator_accepts_service_owned_404(): + smoke = load_smoke_module() + + smoke.validate_bill_diff_unknown(404, {"error": "bill not found"}) + smoke.validate_bill_diff_unknown(404, {"error": "version not found"}) + # The bills index can still be warming; a service-owned 503 proves route reachability. + smoke.validate_bill_diff_unknown(503, {"error": "bills index checksum mismatch"}) + + +def test_bill_diff_unknown_validator_rejects_api_gateway_404(): + smoke = load_smoke_module() + + with pytest.raises(smoke.SmokeFailure, match="API Gateway returned Not Found"): + smoke.validate_bill_diff_unknown(404, {"message": "Not Found"}) + + with pytest.raises(smoke.SmokeFailure, match="service-owned error body"): + smoke.validate_bill_diff_unknown(404, {}) + + with pytest.raises(smoke.SmokeFailure, match="not a documented not-found message"): + smoke.validate_bill_diff_unknown(404, {"error": "internal error"}) + + def test_full_only_bill_diff_check_is_skipped_in_contract_mode(): smoke = load_smoke_module() @@ -116,5 +138,6 @@ def test_full_only_bill_diff_check_is_skipped_in_contract_mode(): full_checks = [check.name for check in smoke.CHECKS] assert "bills:diff-route" in contract_checks + assert "bills:diff-unknown" in contract_checks assert "bills:diff-full" not in contract_checks assert "bills:diff-full" in full_checks