"""procurement-api handler tests (offline, fake Dynamo). Pins the auth seam both ways: docs routes fail closed on the shared token (including the browser ?token= shim), while data routes must NOT consult the token at all -- SigV4 authorization is API Gateway's job upstream, and a handler-side token check there would break every legitimate SigV4 caller. Also pins routing, 404/501/400 mapping, Decimal-safe serialization, and limit clamping. """ import json from decimal import Decimal import pytest from tests.support import load_lambda_module class _FakeTable: def __init__(self, items): self._items = items self.last_kwargs = None def scan(self, **kwargs): self.last_kwargs = kwargs return {"Items": list(self._items)} def query(self, **kwargs): self.last_kwargs = kwargs return {"Items": list(self._items)} def get_item(self, Key): # noqa: N803 (boto3 kwarg name) pk_attr, pk_val = next(iter(Key.items())) for item in self._items: if item.get(pk_attr) == pk_val: return {"Item": item} return {} class _FakeDynamo: def __init__(self, tables): self.tables = tables def Table(self, name): # noqa: N802 (boto3 method name) return self.tables[name] def _event(method, resource, path_params=None, qs=None, headers=None): return { "httpMethod": method, "resource": resource, "pathParameters": path_params or {}, "queryStringParameters": qs or {}, "headers": headers or {}, } @pytest.fixture() def api(monkeypatch): mod = load_lambda_module("api", "handler") wo_items = [ { "work_order_id": "11144580730", "wo_status": "assigned", "description": "Dock door 14", } ] comment_items = [ { "work_order_id": "11144580730", "comment_id": "11144580730#2026-01-01T00:00:00#abc123def456", "text": "Vendor dispatched", } ] po_items = [ { "po_number": "2D-22030794", "total_amount": Decimal("123.45"), "line_items": [{"quantity": Decimal("5"), "price": Decimal("24.69")}], } ] site_items = [{"siteCode": "JFK8", "state": "NY", "poCount": Decimal("7")}] tables = { mod.wo_repo.WORK_ORDERS_TABLE: _FakeTable(wo_items), mod.wo_repo.COMMENTS_TABLE: _FakeTable(comment_items), mod.po_repo.PO_TABLE: _FakeTable(po_items), mod.po_repo.VERIFIED_SITES_TABLE: _FakeTable(site_items), } fake = _FakeDynamo(tables) monkeypatch.setattr(mod.wo_repo, "dynamodb", fake) monkeypatch.setattr(mod.po_repo, "dynamodb", fake) return mod, fake def test_healthcheck_short_circuits(api): mod, _ = api assert mod.handler({"healthcheck": True}, None) == {"healthcheck": "ok"} def test_docs_fail_closed_without_token(api): # No WEB_UI_AUTH_TOKEN_SECRET_ARN configured -> web_ui_auth returns None # -> the docs routes must 401, not serve the spec. mod, _ = api for resource in ("/docs", "/openapi.json"): response = mod.handler(_event("GET", resource), None) assert response["statusCode"] == 401 def test_docs_served_when_authenticated(api, monkeypatch): mod, _ = api monkeypatch.setattr(mod, "is_authenticated", lambda event: True) docs = mod.handler(_event("GET", "/docs"), None) assert docs["statusCode"] == 200 assert docs["headers"]["Content-Type"].startswith("text/html") # The vendored Redoc bundle is inlined and the spec (carrying the title) # with it. assert "Redoc.init" in docs["body"] assert "Procurement Ingest API" in docs["body"] # Every placeholder must be substituted, or the page renders broken. for placeholder in ( mod._SPEC_PLACEHOLDER, mod._JS_PLACEHOLDER, mod._FONTS_PLACEHOLDER, ): assert placeholder not in docs["body"] spec = mod.handler(_event("GET", "/openapi.json"), None) assert spec["statusCode"] == 200 assert json.loads(spec["body"])["openapi"] == "3.1.0" def test_base_url_custom_domain_omits_stage(api): mod, _ = api # execute-api host keeps the /{stage} segment... execapi = { "requestContext": { "domainName": "mvul1efda2.execute-api.us-east-1.amazonaws.com", "stage": "prod", } } assert mod._base_url(execapi) == ( "https://mvul1efda2.execute-api.us-east-1.amazonaws.com/prod" ) # ...but a custom domain (base-path mapping) serves the stage at the root, # so no /{stage} is appended (it would advertise a broken server URL). custom = { "requestContext": { "domainName": "procurement-api.seahaven.com", "stage": "prod", } } assert mod._base_url(custom) == "https://procurement-api.seahaven.com" assert mod._base_url({}) is None def test_spec_injects_custom_domain_server(api): mod, _ = api event = { "requestContext": { "domainName": "procurement-api.seahaven.com", "stage": "prod", } } spec = json.loads(mod._spec_for_request(event)) urls = [s["url"] for s in spec["servers"]] # Accessed via the custom domain, every server URL resolves to the custom # domain (the static entry plus the injected one), and none carry a /prod # suffix. Exact-equality checks (no substring-in-URL) so this reads # cleanly and doesn't trip CodeQL's URL-sanitization heuristic. assert set(urls) == {"https://procurement-api.seahaven.com"} def test_token_query_shim_synthesizes_header(api, monkeypatch): mod, _ = api seen = {} def capture(event): seen.update(event.get("headers") or {}) return False monkeypatch.setattr(mod, "is_authenticated", capture) original = _event("GET", "/docs", qs={"token": "sekrit"}) mod.handler(original, None) assert seen.get("x-auth-token") == "sekrit" # The shim must not mutate the caller's event in place. assert "x-auth-token" not in original["headers"] def test_non_ascii_token_fails_closed_not_500(api): # A non-ASCII presented token must 401 (fail closed), not crash # hmac.compare_digest into a 500 that pages the 5xx alarm. Reach the # web_ui_auth module namespace through the imported is_authenticated's # globals and seed a valid cached token. mod, _ = api auth_globals = mod.is_authenticated.__globals__ auth_globals["_auth_token_cache"] = "correct-token" auth_globals["_auth_token_cached_at"] = float("inf") try: response = mod.handler( _event("GET", "/docs", headers={"x-auth-token": "é"}), None ) assert response["statusCode"] == 401 finally: auth_globals["_auth_token_cache"] = None auth_globals["_auth_token_cached_at"] = 0.0 def test_data_routes_never_consult_docs_token(api, monkeypatch): mod, _ = api def explode(event): raise AssertionError("data routes must not consult the docs token") monkeypatch.setattr(mod, "is_authenticated", explode) response = mod.handler(_event("GET", "/work-orders"), None) assert response["statusCode"] == 200 def test_list_and_get_routing(api): mod, _ = api listing = mod.handler(_event("GET", "/work-orders"), None) body = json.loads(listing["body"]) assert body["items"][0]["work_order_id"] == "11144580730" assert body["next_cursor"] is None hit = mod.handler( _event( "GET", "/work-orders/{workOrderId}", path_params={"workOrderId": "11144580730"}, ), None, ) assert json.loads(hit["body"])["wo_status"] == "assigned" miss = mod.handler( _event( "GET", "/work-orders/{workOrderId}", path_params={"workOrderId": "999"}, ), None, ) assert miss["statusCode"] == 404 comments = mod.handler( _event( "GET", "/work-orders/{workOrderId}/comments", path_params={"workOrderId": "11144580730"}, ), None, ) assert json.loads(comments["body"])["items"][0]["text"] == "Vendor dispatched" sites = mod.handler( _event( "GET", "/verified-sites/{siteCode}", path_params={"siteCode": "JFK8"}, ), None, ) assert json.loads(sites["body"])["state"] == "NY" def test_decimal_serialization_round_trips(api): mod, _ = api response = mod.handler( _event( "GET", "/purchase-orders/{poNumber}", path_params={"poNumber": "2D-22030794"}, ), None, ) body = json.loads(response["body"]) assert body["total_amount"] == 123.45 assert body["line_items"][0]["quantity"] == 5 assert isinstance(body["line_items"][0]["quantity"], int) assert "Decimal" not in response["body"] def test_limit_clamped_to_bounds(api): mod, fake = api mod.handler(_event("GET", "/work-orders", qs={"limit": "9999"}), None) assert fake.tables[mod.wo_repo.WORK_ORDERS_TABLE].last_kwargs["Limit"] == 500 mod.handler(_event("GET", "/work-orders", qs={"limit": "0"}), None) assert fake.tables[mod.wo_repo.WORK_ORDERS_TABLE].last_kwargs["Limit"] == 1 def test_non_integer_limit_is_400(api): mod, _ = api response = mod.handler(_event("GET", "/work-orders", qs={"limit": "abc"}), None) assert response["statusCode"] == 400 def test_planned_routes_return_501(api): mod, _ = api for method, resource in mod.PLANNED_ROUTES: response = mod.handler(_event(method, resource), None) assert response["statusCode"] == 501, (method, resource) def test_unknown_route_is_404(api): mod, _ = api response = mod.handler(_event("GET", "/nope"), None) assert response["statusCode"] == 404 def test_unexpected_error_maps_to_clean_500(api, monkeypatch): mod, fake = api def boom(**kwargs): raise RuntimeError("dynamo fell over") monkeypatch.setattr(fake.tables[mod.wo_repo.WORK_ORDERS_TABLE], "scan", boom) response = mod.handler(_event("GET", "/work-orders"), None) assert response["statusCode"] == 500 assert json.loads(response["body"]) == {"error": "internal error"}