diff --git a/.redocly.yaml b/.redocly.yaml index 3a9fa49..b1ea20b 100644 --- a/.redocly.yaml +++ b/.redocly.yaml @@ -1,7 +1,29 @@ # Same Redocly recommended ruleset as internal-portal (DEV-223 / DEV-289). +# Login-style redirects succeed with 302. Recommended only counts 2XX. extends: - recommended +rules: + operation-2xx-response: off + rule/operation-2xx-or-3xx-response: + subject: + type: Responses + message: Operation must define a 2XX or 3XX response. + severity: warn + assertions: + requireAny: + - "200" + - "201" + - "202" + - "204" + - "301" + - "302" + - "303" + - "307" + - "308" + - "2XX" + - "3XX" + apis: meals@v1: root: openapi.yaml diff --git a/openapi.yaml b/openapi.yaml index a11cced..968bd26 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -46,6 +46,8 @@ paths: application/json: schema: $ref: "#/components/schemas/Health" + "400": + $ref: "#/components/responses/StringError" /api/menu/{week}: get: @@ -82,6 +84,8 @@ paths: application/json: schema: $ref: "#/components/schemas/FormStatus" + "400": + $ref: "#/components/responses/StringError" /api/submit-order: post: @@ -260,6 +264,8 @@ paths: application/json: schema: $ref: "#/components/schemas/FormRoster" + "400": + $ref: "#/components/responses/StringError" /api/publish/settings: get: diff --git a/src/server/http_api.py b/src/server/http_api.py index cc32671..5e96618 100644 --- a/src/server/http_api.py +++ b/src/server/http_api.py @@ -279,15 +279,20 @@ def lambda_handler(event, context): return response(405, {"error": "Method not allowed"}) -def handle_menu(event): - """Return the published menu in the portal's public MenuPayload shape.""" +def _week_from_path(event): requested_week = (event.get("pathParameters") or {}).get("week", "") if requested_week == "current": - week = current_week() - elif re.fullmatch(r"\d{4}-W\d{2}", requested_week): - week = requested_week - else: - return response(400, {"error": "week path param must be current or YYYY-WNN"}) + return current_week(), None + if re.fullmatch(r"\d{4}-W\d{2}", requested_week): + return requested_week, None + return None, response(400, {"error": "week path param must be current or YYYY-WNN"}) + + +def handle_menu(event): + """Return the published menu in the portal's public MenuPayload shape.""" + week, error = _week_from_path(event) + if error is not None: + return error menu = get_menu(week) if menu is None: @@ -623,7 +628,9 @@ def handle_my_orders(event): def handle_form_status(event): - week = event.get("pathParameters", {}).get("week", current_week()) + week, error = _week_from_path(event) + if error is not None: + return error status = get_form_status(week) result = {"week": week, "status": status} if status == "closed": diff --git a/tests/test_openapi_contract.py b/tests/test_openapi_contract.py index 6e96d44..a876099 100644 --- a/tests/test_openapi_contract.py +++ b/tests/test_openapi_contract.py @@ -12,6 +12,8 @@ def test_openapi_uses_redocly_recommended(): ci = (ROOT / ".github" / "workflows" / "ci.yml").read_text() assert "extends:" in redocly assert "- recommended" in redocly + assert "operation-2xx-response: off" in redocly + assert "rule/operation-2xx-or-3xx-response" in redocly assert "root: openapi.yaml" in redocly assert '"openapi:lint"' in package assert '"@redocly/cli": "2.52.1"' in package diff --git a/tests/test_submit_order.py b/tests/test_submit_order.py index c69fde9..d435798 100644 --- a/tests/test_submit_order.py +++ b/tests/test_submit_order.py @@ -1446,6 +1446,33 @@ def test_form_status_open(mock_week, mock_status): assert "reopen_at" not in body, "reopen_at should NOT be present when form is open" +@patch("submit_order_handler.get_form_status", return_value="open") +@patch("submit_order_handler.current_week", return_value="2026-W20") +def test_form_status_current_maps_to_current_week(mock_week, mock_status): + from submit_order_handler import lambda_handler + + event = _make_event( + method="GET", path="/form-status/current", path_parameters={"week": "current"} + ) + status, body = _parse_response(lambda_handler(event, None)) + assert status == 200, f"Expected 200, got {status}: {body}" + assert body["week"] == "2026-W20" + mock_status.assert_called_once_with("2026-W20") + + +@patch("submit_order_handler.get_form_status") +def test_form_status_rejects_invalid_week(mock_status): + from submit_order_handler import lambda_handler + + event = _make_event( + method="GET", path="/form-status/nope", path_parameters={"week": "nope"} + ) + status, body = _parse_response(lambda_handler(event, None)) + assert status == 400, f"Expected 400, got {status}: {body}" + assert "week path param" in body["error"] + mock_status.assert_not_called() + + @patch("submit_order_handler.get_form_status", return_value="closed") @patch("submit_order_handler.current_week", return_value="2026-W20") def test_form_status_closed_reopen_at(mock_week, mock_status):