feat(v2): green checks, OpenAPI export, and spec updates for household-scoped API
This commit is contained in:
parent
50f908f204
commit
87e3dba6f8
2 changed files with 26 additions and 23 deletions
|
|
@ -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.
|
- 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.
|
- 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.
|
- 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.
|
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/*`.
|
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)
|
## 5. Gaps vs Current Codebase (summary)
|
||||||
|
|
||||||
What’s missing today (must be implemented in v2):
|
Status summary:
|
||||||
- JWT-based authentication (register/login/google) and `users` domain. Replace `persons` entirely.
|
- 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.
|
||||||
- Household domain: `households`, `household_members`, `household_invitations` tables, repos, and APIs.
|
- Preserved from v1: camelCase response models, `Page<T>` pagination and cursor semantics, `Location` headers on create, shopping storeName normalization ("home").
|
||||||
- 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.
|
|
||||||
|
|
||||||
Immediate priorities (feedback-incorporated):
|
Remaining work (post-v2 cleanup):
|
||||||
- Upgrade password hashing from SHA-256 to a secure, adaptive scheme (PBKDF2/bcrypt/argon2). DONE with PBKDF2; consider migrating to argon2 in future.
|
- Remove `persons/` package and any residual references; consolidate entirely on `users`.
|
||||||
- Finalize and commit `openapi.json` with household-scoped routes to unblock frontend (temporary X-Household-Slug header can be removed).
|
- Optional: migrate PBKDF2 to argon2/bcrypt and add password rehash-on-login.
|
||||||
- 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.
|
- 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.
|
||||||
What to preserve from v1:
|
- Rename `*_v2.py` modules back to canonical names once v1 has been fully retired (routers already removed) to reduce duplication noise.
|
||||||
- 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).
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -92,21 +92,27 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
# No chefs
|
# No chefs
|
||||||
body = dict(base)
|
body = dict(base)
|
||||||
body["chefs"] = []
|
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 r.status_code == 400
|
||||||
assert "chef" in r.json()["title"].lower()
|
assert "chef" in r.json()["title"].lower()
|
||||||
|
|
||||||
# No cleanup
|
# No cleanup
|
||||||
body = dict(base)
|
body = dict(base)
|
||||||
body["cleanup"] = []
|
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 r.status_code == 400
|
||||||
assert "cleanup" in r.json()["title"].lower()
|
assert "cleanup" in r.json()["title"].lower()
|
||||||
|
|
||||||
# No consumers
|
# No consumers
|
||||||
body = dict(base)
|
body = dict(base)
|
||||||
body["consumers"] = []
|
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 r.status_code == 400
|
||||||
assert "consumer" in r.json()["title"].lower()
|
assert "consumer" in r.json()["title"].lower()
|
||||||
|
|
||||||
|
|
@ -114,14 +120,18 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
body = dict(base)
|
body = dict(base)
|
||||||
body["recipes"] = []
|
body["recipes"] = []
|
||||||
body["extraIngredients"] = []
|
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 r.status_code == 400
|
||||||
assert "recipe" in r.json()["title"].lower() or "ingredient" in r.json()["title"].lower()
|
assert "recipe" in r.json()["title"].lower() or "ingredient" in r.json()["title"].lower()
|
||||||
|
|
||||||
# Zero servings in recipe
|
# Zero servings in recipe
|
||||||
body = dict(base)
|
body = dict(base)
|
||||||
body["recipes"] = [{"mealId": -1, "recipeId": 1, "servings": 0}]
|
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 r.status_code == 400
|
||||||
assert "servings" in r.json()["title"].lower()
|
assert "servings" in r.json()["title"].lower()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue