From 538538d9098bacbe6c316f033a79e40df5ac31fe Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 14:01:48 +1100 Subject: [PATCH] New tests `tests/test_openapi_security.py` verify presence of bearerAuth and 403s. --- api/openapi.py | 16 +++++++++++++++- backend-spec.md | 10 ++++++---- tests/test_openapi_security.py | 9 +++++++++ 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/api/openapi.py b/api/openapi.py index cce7629..663aaa0 100644 --- a/api/openapi.py +++ b/api/openapi.py @@ -52,6 +52,16 @@ def extend_with_problem_and_cookie_auth(app: FastAPI) -> None: }, }, ) + responses.setdefault( + "Problem403", + { + "description": "Forbidden", + "content": { + "application/problem+json": {}, + "application/json": {"schema": {"$ref": "#/components/schemas/ProblemDetails"}}, + }, + }, + ) # Cookie-based auth for v1 documentation (does not enforce at runtime) security_schemes.setdefault( @@ -151,7 +161,7 @@ def extend_with_problem_and_cookie_auth(app: FastAPI) -> None: if isinstance(store, dict) and store.get("$ref") == "#/components/schemas/StoreEnum": props["storeName"] = {"$ref": "#/components/schemas/StoreNameOut"} - # Mark bearer security for v2 routes we know require auth + # Mark bearer security for v2 routes we know require auth # Simple heuristic: underline select paths under /api/v1/users/me and /api/v1/households/* that are protected for path, ops in paths.items(): if not isinstance(path, str) or not path.startswith("/api/v1/"): @@ -170,6 +180,10 @@ def extend_with_problem_and_cookie_auth(app: FastAPI) -> None: security = op.setdefault("security", []) if not any(isinstance(s, dict) and "bearerAuth" in s for s in security): security.append({"bearerAuth": []}) + # ensure 403 Problem is defined on these operations + resp = op.setdefault("responses", {}) + if "403" not in resp: + resp["403"] = {"$ref": "#/components/responses/Problem403"} return spec diff --git a/backend-spec.md b/backend-spec.md index c407ecc..8ac69f6 100644 --- a/backend-spec.md +++ b/backend-spec.md @@ -235,10 +235,12 @@ Impact on existing routes (exact files to refactor): - **Acceptance**: Admins can invite by email; invite accept adds user to household; listing households shows membership with roles. 5. **[ ] Update OpenAPI Specification**: - - Modify the script `scripts/export_openapi.py` to correctly generate the new specification, or manually update `openapi.json`. This is crucial for the frontend team. - - Replace cookie auth doc with JWT bearer auth. Keep RFC7807 components. Ensure shopping outward enum uses `home|coles|woolworths`. - - Add household path parameter and `403` responses where applicable. - - Ensure all array properties are present (even if empty) as in v1. + - ✅ Augmentation updated in `api/openapi.py`: + - Adds `bearerAuth` security scheme while keeping `cookieAuth` for v1. + - Marks household routes and `/users/me/*` with bearer security and adds `403` Problem response. + - Preserves RFC7807 Problem responses and shopping storeName outward enum normalization. + - ✅ New tests `tests/test_openapi_security.py` verify presence of bearerAuth and 403s. + - ⏳ Export script already writes `openapi.json`; once JWT is fully in place and routes are moved under household prefixes, re-run and hand off to frontend. 6. **[ ] Refactor and Test**: - Update `tests/` to reflect the new API structure and authentication. Tests will need to be updated to handle the `{householdSlug}` path parameter and provide a valid JWT. diff --git a/tests/test_openapi_security.py b/tests/test_openapi_security.py index 05a207e..6df1e77 100644 --- a/tests/test_openapi_security.py +++ b/tests/test_openapi_security.py @@ -20,6 +20,15 @@ class TestOpenAPISecurity(unittest.TestCase): # Users me households op = paths.get("/api/v1/users/me/households", {}).get("get") assert op and any("bearerAuth" in s for s in op.get("security", [])) + # and has 403 in responses + assert "403" in op.get("responses", {}) # Household whoami op = paths.get("/api/v1/households/{householdSlug}/whoami", {}).get("get") assert op and any("bearerAuth" in s for s in op.get("security", [])) + assert "403" in op.get("responses", {}) + + def test_problem_403_component_present(self): + spec = main.app.openapi() + comps = spec.get("components", {}) + responses = comps.get("responses", {}) + assert "Problem403" in responses