feat(v2): green checks, OpenAPI export, and spec updates for household-scoped API

This commit is contained in:
jableader 2025-11-01 16:40:01 +11:00
parent 50f908f204
commit 87e3dba6f8
2 changed files with 26 additions and 23 deletions

View file

@ -1,4 +1,4 @@
## 0.4 Validated behaviors and invariants
## 0.5 Validated v2 household behaviors (tests snapshot)
- GET `/api/v1/households/{householdSlug}/shopping/{listId}` returning purchased list + lookups scoped to household; cross-household returns 404.
- POST `/api/v1/households/{householdSlug}/meals/{mealId}/consumed` marks a meal consumed within the household; validates timezone on provided `consumedDate`; clears outstanding meal requests only within that household.
- Tests: `tests/test_meals_consumed_v2.py` validates scoping (requests cleared in same household, unaffected in other household). PASS.
@ -10,7 +10,7 @@
This document has been validated against the current codebase (v1) to ensure the plan captures all required changes. It starts with a concise baseline of what exists today, then details the v2 multi-tenancy/auth refactor with concrete, file-scoped steps and acceptance criteria.
Date reviewed: 2025-11-01 (updated after completing meals write flows, scoped endpoints, and v1 removal)
Date reviewed: 2025-11-01 (updated after porting legacy coverage to v2; all checks green; OpenAPI exported)
Repo modules checked: `main.py`, `api/*` (v2 routers only), `persons/*` (legacy, pending removal), `meals/*`, `ingredients/*`, `recipes/*`, `products/*`, `shopping/*`, `common.py`, `db.py`, `settings.py`, tests in `tests/*`.
@ -278,23 +278,16 @@ Impact on existing routes (exact files to refactor):
## 5. Gaps vs Current Codebase (summary)
Whats missing today (must be implemented in v2):
- JWT-based authentication (register/login/google) and `users` domain. Replace `persons` entirely.
- Household domain: `households`, `household_members`, `household_invitations` tables, repos, and APIs.
- Household scoping: path prefixes, membership checks, repository filtering by `household_id` across all data tables.
- OpenAPI security scheme update to JWT bearer; `403` responses for membership violations.
Status summary:
- Implemented: JWT auth v2 with refresh cookie; household domains and membership; invitations; full household scoping across recipes/meals/shopping; OpenAPI augmentation with bearerAuth and 403; RFC7807 preserved; tests green.
- Preserved from v1: camelCase response models, `Page<T>` pagination and cursor semantics, `Location` headers on create, shopping storeName normalization ("home").
Immediate priorities (feedback-incorporated):
- Upgrade password hashing from SHA-256 to a secure, adaptive scheme (PBKDF2/bcrypt/argon2). DONE with PBKDF2; consider migrating to argon2 in future.
- Finalize and commit `openapi.json` with household-scoped routes to unblock frontend (temporary X-Household-Slug header can be removed).
- Plan v1 cleanup: once parity is achieved for scoped routes, rename `*_v2.py` to canonical filenames and remove original v1 modules to avoid long-term duplication.
What to preserve from v1:
- CamelCase response keys, non-null collection properties, `Page<T>` envelope and cursor semantics, RFC7807 responses, `Location` header on create.
- Shopping outward storeName normalization ("home" instead of empty string).
Nice-to-have carryovers (already partially implemented as per `tighten-api-spec.md`):
- Outward union models for shopping list items to reduce nullability (implemented in API without changing DB schema).
Remaining work (post-v2 cleanup):
- Remove `persons/` package and any residual references; consolidate entirely on `users`.
- Optional: migrate PBKDF2 to argon2/bcrypt and add password rehash-on-login.
- Invitations: integrate email delivery provider and track send status.
- DB: Add composite indices like `(household_id, id)` for common pagination; evaluate adding FK constraints from tenant tables to `households(id)` where safe.
- Rename `*_v2.py` modules back to canonical names once v1 has been fully retired (routers already removed) to reduce duplication noise.
---

View file

@ -92,21 +92,27 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
# No chefs
body = dict(base)
body["chefs"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 400
assert "chef" in r.json()["title"].lower()
# No cleanup
body = dict(base)
body["cleanup"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 400
assert "cleanup" in r.json()["title"].lower()
# No consumers
body = dict(base)
body["consumers"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 400
assert "consumer" in r.json()["title"].lower()
@ -114,14 +120,18 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
body = dict(base)
body["recipes"] = []
body["extraIngredients"] = []
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 400
assert "recipe" in r.json()["title"].lower() or "ingredient" in r.json()["title"].lower()
# Zero servings in recipe
body = dict(base)
body["recipes"] = [{"mealId": -1, "recipeId": 1, "servings": 0}]
r = self.client.post(f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body)
r = self.client.post(
f"/api/v1/households/{self.slug}/meals", headers=self.headers, json=body
)
assert r.status_code == 400
assert "servings" in r.json()["title"].lower()