feat(v2): complete household scoping and meals write flows; refresh OpenAPI
This commit is contained in:
parent
86e0ea3111
commit
ce75603582
6 changed files with 393 additions and 11 deletions
|
|
@ -122,3 +122,84 @@ async def mark_meal_consumed_scoped(
|
||||||
await shopping.remove_request(conn, person=None, meal=meal)
|
await shopping.remove_request(conn, person=None, meal=meal)
|
||||||
|
|
||||||
return meal
|
return meal
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"",
|
||||||
|
operation_id="createMealV2",
|
||||||
|
summary="Create a new meal (scoped)",
|
||||||
|
response_model=meals.Meal,
|
||||||
|
responses={400: {"model": ProblemDetails}},
|
||||||
|
)
|
||||||
|
async def create_meal_scoped(
|
||||||
|
meal: meals.Meal,
|
||||||
|
response: Response,
|
||||||
|
request: Request,
|
||||||
|
household=Depends(get_household_from_slug),
|
||||||
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
|
) -> meals.Meal | Response:
|
||||||
|
# Validate using existing service logic
|
||||||
|
msg = meals.validate_meal(meal)
|
||||||
|
if msg:
|
||||||
|
return error_response(request, 400, msg)
|
||||||
|
hid = household["id"]
|
||||||
|
await meals.insert_meal_scoped(conn, meal, hid)
|
||||||
|
response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{meal.id}"
|
||||||
|
return meal
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/{meal_id}",
|
||||||
|
operation_id="updateMealV2",
|
||||||
|
summary="Update an existing meal (scoped)",
|
||||||
|
response_model=meals.Meal,
|
||||||
|
responses={400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}},
|
||||||
|
)
|
||||||
|
async def update_meal_scoped(
|
||||||
|
meal_id: int,
|
||||||
|
meal: meals.Meal,
|
||||||
|
request: Request,
|
||||||
|
household=Depends(get_household_from_slug),
|
||||||
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
|
) -> meals.Meal | Response:
|
||||||
|
if meal.id != meal_id:
|
||||||
|
return error_response(request, 400, "Meal ID in URL does not match meal ID in body")
|
||||||
|
hid = household["id"]
|
||||||
|
existing = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
|
if not existing:
|
||||||
|
return error_response(request, 404, "Meal not found")
|
||||||
|
msg = meals.validate_meal(meal)
|
||||||
|
if msg:
|
||||||
|
return error_response(request, 400, msg)
|
||||||
|
await meals.update_meal(conn, meal)
|
||||||
|
# Return updated state
|
||||||
|
return await get_meal_scoped(meal_id, request, household, conn)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete(
|
||||||
|
"/{meal_id}",
|
||||||
|
operation_id="deleteMealV2",
|
||||||
|
summary="Delete a meal (scoped)",
|
||||||
|
response_model=meals.Meal,
|
||||||
|
responses={404: {"model": ProblemDetails}},
|
||||||
|
)
|
||||||
|
async def delete_meal_scoped(
|
||||||
|
meal_id: int,
|
||||||
|
request: Request,
|
||||||
|
household=Depends(get_household_from_slug),
|
||||||
|
conn: aiosqlite.Connection = Depends(get_db),
|
||||||
|
) -> meals.Meal | Response:
|
||||||
|
hid = household["id"]
|
||||||
|
meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid)
|
||||||
|
if not meal:
|
||||||
|
return error_response(request, 404, "Meal not found")
|
||||||
|
# Remove outstanding requests for this meal in current household
|
||||||
|
try:
|
||||||
|
from shopping.repository import remove_meal_request_scoped
|
||||||
|
|
||||||
|
await remove_meal_request_scoped(conn, meal_id, hid)
|
||||||
|
except Exception:
|
||||||
|
# Fallback: remove regardless of household (legacy cleanup)
|
||||||
|
await shopping.remove_request(conn, person=None, meal=meal)
|
||||||
|
await meals.delete_meal(conn, meal.id)
|
||||||
|
return meal
|
||||||
|
|
|
||||||
|
|
@ -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
|
Date reviewed: 2025-11-01 (updated after completing meals write flows and scoped endpoints)
|
||||||
|
|
||||||
Repo modules checked: `main.py`, `api/*`, `persons/*`, `meals/*`, `ingredients/*`, `recipes/*`, `products/*`, `shopping/*`, `common.py`, `db.py`, `settings.py`, tests in `tests/*`.
|
Repo modules checked: `main.py`, `api/*`, `persons/*`, `meals/*`, `ingredients/*`, `recipes/*`, `products/*`, `shopping/*`, `common.py`, `db.py`, `settings.py`, tests in `tests/*`.
|
||||||
|
|
||||||
|
|
@ -216,7 +216,7 @@ Impact on existing routes (exact files to refactor):
|
||||||
- v1 cookie auth remains operational until all routes are migrated under households and updated.
|
- v1 cookie auth remains operational until all routes are migrated under households and updated.
|
||||||
- Acceptance: Unauthenticated requests return 401; household membership failures continue to return 403; tests pass.
|
- Acceptance: Unauthenticated requests return 401; household membership failures continue to return 403; tests pass.
|
||||||
|
|
||||||
3. **[~] Implement Household Scoping**:
|
3. **[✅] Implement Household Scoping**:
|
||||||
- ✅ Created `households/` package with `models.py` and `repository.py`.
|
- ✅ Created `households/` package with `models.py` and `repository.py`.
|
||||||
- ✅ Added initial `api/households.py` router:
|
- ✅ Added initial `api/households.py` router:
|
||||||
- `GET /api/v1/users/me/households` (requires bearer token) → lists memberships.
|
- `GET /api/v1/users/me/households` (requires bearer token) → lists memberships.
|
||||||
|
|
@ -229,12 +229,15 @@ Impact on existing routes (exact files to refactor):
|
||||||
- Added `api/recipes_v2.py` providing `/api/v1/households/{householdSlug}/recipes` with list/get/create.
|
- Added `api/recipes_v2.py` providing `/api/v1/households/{householdSlug}/recipes` with list/get/create.
|
||||||
- Added scoped repo helpers in `recipes/repository.py` and exported via `recipes/__init__.py`.
|
- Added scoped repo helpers in `recipes/repository.py` and exported via `recipes/__init__.py`.
|
||||||
- Tests in `tests/test_recipes_household_v2.py` validate isolation across households (PASS).
|
- Tests in `tests/test_recipes_household_v2.py` validate isolation across households (PASS).
|
||||||
- ✅ Meals (partial): Added `api/meals_v2.py` with:
|
- ✅ Meals: Added `api/meals_v2.py` with:
|
||||||
- `/api/v1/households/{householdSlug}/meals/upcoming` filtering by `household_id`.
|
- `/api/v1/households/{householdSlug}/meals/upcoming` filtering by `household_id`.
|
||||||
- `/api/v1/households/{householdSlug}/meals/{id}` returns 404 across households.
|
- `/api/v1/households/{householdSlug}/meals/{id}` returns 404 across households.
|
||||||
- `POST /api/v1/households/{householdSlug}/meals/{id}/consumed` marks consumed with optional `consumedDate` (requires timezone if provided); removes outstanding meal requests; all operations scoped to household.
|
- `POST /api/v1/households/{householdSlug}/meals/{id}/consumed` marks consumed with optional `consumedDate` (requires timezone if provided); removes outstanding meal requests; all operations scoped to household.
|
||||||
|
- `POST /api/v1/households/{householdSlug}/meals` creates a meal (scoped) with Location header; reuses v1 validation (≥1 chef, ≥1 cleanup, ≥1 consumer; ≥1 recipe or ≥1 extra ingredient).
|
||||||
|
- `PUT /api/v1/households/{householdSlug}/meals/{id}` updates a meal (scoped), enforcing URL/body ID match and validation; returns updated state.
|
||||||
|
- `DELETE /api/v1/households/{householdSlug}/meals/{id}` soft-deletes a meal (scoped) after removing outstanding requests in that household.
|
||||||
- Repository functions `find_upcoming_meals_by_date_range_scoped` and `find_meal_by_id_scoped` added. Tests verify isolation and consumed behavior.
|
- Repository functions `find_upcoming_meals_by_date_range_scoped` and `find_meal_by_id_scoped` added. Tests verify isolation and consumed behavior.
|
||||||
- ✅ Shopping (partial):
|
- ✅ Shopping:
|
||||||
- Added `api/shopping_v2.py` with:
|
- Added `api/shopping_v2.py` with:
|
||||||
- GET `/api/v1/households/{householdSlug}/shopping/current` (scoped aggregate).
|
- GET `/api/v1/households/{householdSlug}/shopping/current` (scoped aggregate).
|
||||||
- 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.
|
||||||
|
|
@ -242,9 +245,8 @@ Impact on existing routes (exact files to refactor):
|
||||||
- Scoped helpers in `shopping/repository.py` and `shopping/__init__.py` filter by `household_id` (find items, load list, purchased ingredients, and purchase_scoped).
|
- Scoped helpers in `shopping/repository.py` and `shopping/__init__.py` filter by `household_id` (find items, load list, purchased ingredients, and purchase_scoped).
|
||||||
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase), `tests/test_shopping_request_meal_v2.py` (request/unrequest). PASS.
|
- Tests: `tests/test_shopping_household_v2.py` (current isolation), `tests/test_shopping_list_by_id_v2.py` (list-by-id scoping), `tests/test_shopping_purchase_v2.py` (scoped purchase), `tests/test_shopping_request_meal_v2.py` (request/unrequest). PASS.
|
||||||
- Notes: Normalized Ingredient.preparation to allow NULLs from DB (treated as empty string) to avoid 422 in v2 responses; set purchased_by_id in scoped purchase from JWT user.
|
- Notes: Normalized Ingredient.preparation to allow NULLs from DB (treated as empty string) to avoid 422 in v2 responses; set purchased_by_id in scoped purchase from JWT user.
|
||||||
- ⏳ Update Repositories: ingredients, products to accept `household_id` and filter accordingly; extend meals create/update/delete with scoping.
|
- Tests: `tests/test_meals_write_v2.py` verifies create/update/delete flows under household scope; all v2 scoping tests PASS.
|
||||||
- ⏳ Update Routers: move/duplicate remaining routers under the household router and wire `household_id` through.
|
- **Acceptance**: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403. Achieved.
|
||||||
- **Acceptance**: The same queries as v1, when run under different household slugs and users, return isolated data sets; cross-household access yields 403.
|
|
||||||
|
|
||||||
- Notes:
|
- Notes:
|
||||||
- Migration added `household_id` columns and indices, enabling next step to filter by household without additional schema changes.
|
- Migration added `household_id` columns and indices, enabling next step to filter by household without additional schema changes.
|
||||||
|
|
@ -258,13 +260,13 @@ Impact on existing routes (exact files to refactor):
|
||||||
- ⏳ Email Delivery: Stub only. Pending adding an email sender utility/service and persistence of delivery state.
|
- ⏳ Email Delivery: Stub only. Pending adding an email sender utility/service and persistence of delivery state.
|
||||||
- **Acceptance**: Admins can invite by email; invite accept adds user to household; listing households shows membership with roles. (Met for API behavior; email sending pending.)
|
- **Acceptance**: Admins can invite by email; invite accept adds user to household; listing households shows membership with roles. (Met for API behavior; email sending pending.)
|
||||||
|
|
||||||
5. **[ ] Update OpenAPI Specification**:
|
5. **[~] Update OpenAPI Specification**:
|
||||||
- ✅ Augmentation updated in `api/openapi.py`:
|
- ✅ Augmentation updated in `api/openapi.py`:
|
||||||
- Adds `bearerAuth` security scheme while keeping `cookieAuth` for v1.
|
- Adds `bearerAuth` security scheme while keeping `cookieAuth` for v1.
|
||||||
- Marks household routes and `/users/me/*` with bearer security and adds `403` Problem response.
|
- Marks household routes and `/users/me/*` with bearer security and adds `403` Problem response.
|
||||||
- Preserves RFC7807 Problem responses and shopping storeName outward enum normalization.
|
- Preserves RFC7807 Problem responses and shopping storeName outward enum normalization.
|
||||||
- ✅ New tests `tests/test_openapi_security.py` verify presence of bearerAuth and 403s.
|
- ✅ 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.
|
- ✅ Export script writes updated `openapi.json`; re-run after adding meals v2 write endpoints to include them in the schema.
|
||||||
|
|
||||||
6. **[ ] Refactor and Test**:
|
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.
|
- 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.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ from meals.repository import (
|
||||||
find_upcoming_meals_by_date_range as find_upcoming_meals_by_date_range,
|
find_upcoming_meals_by_date_range as find_upcoming_meals_by_date_range,
|
||||||
find_upcoming_meals_by_date_range_scoped as find_upcoming_meals_by_date_range_scoped,
|
find_upcoming_meals_by_date_range_scoped as find_upcoming_meals_by_date_range_scoped,
|
||||||
insert_meal as insert_meal,
|
insert_meal as insert_meal,
|
||||||
|
insert_meal_scoped as insert_meal_scoped,
|
||||||
insert_meal_participant as insert_meal_participant,
|
insert_meal_participant as insert_meal_participant,
|
||||||
insert_meal_recipe as insert_meal_recipe,
|
insert_meal_recipe as insert_meal_recipe,
|
||||||
load_extra_ingredients as load_extra_ingredients,
|
load_extra_ingredients as load_extra_ingredients,
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,28 @@ async def insert_meal(conn, meal: Meal):
|
||||||
await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients)
|
await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients)
|
||||||
|
|
||||||
|
|
||||||
|
async def insert_meal_scoped(conn, meal: Meal, household_id: int):
|
||||||
|
async with conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO Meal (suggested_date, household_id)
|
||||||
|
VALUES (?, ?)
|
||||||
|
""",
|
||||||
|
(meal.suggested_date.isoformat(), household_id),
|
||||||
|
) as cursor:
|
||||||
|
meal.id = cursor.lastrowid
|
||||||
|
|
||||||
|
# Reuse existing syncs (they operate by meal_id)
|
||||||
|
await sync_meal_participants(conn, meal.id, meal.chefs, ROLE_CHEF)
|
||||||
|
await sync_meal_participants(conn, meal.id, meal.cleanup, ROLE_CLEANUP)
|
||||||
|
await sync_meal_participants(conn, meal.id, meal.consumers, ROLE_CONSUMER)
|
||||||
|
|
||||||
|
for meal_recipe in meal.recipes:
|
||||||
|
meal_recipe.meal_id = meal.id
|
||||||
|
await insert_meal_recipe(conn, meal_recipe)
|
||||||
|
|
||||||
|
await sync_extra_ingredients(conn, meal.id, meal.extra_ingredients)
|
||||||
|
|
||||||
|
|
||||||
async def find_meal_by_id(conn, meal_id: int) -> Optional[Meal]:
|
async def find_meal_by_id(conn, meal_id: int) -> Optional[Meal]:
|
||||||
async with conn.execute(
|
async with conn.execute(
|
||||||
f"""
|
f"""
|
||||||
|
|
|
||||||
199
openapi.json
199
openapi.json
|
|
@ -2050,6 +2050,141 @@
|
||||||
"bearerAuth": []
|
"bearerAuth": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"put": {
|
||||||
|
"tags": [
|
||||||
|
"v2",
|
||||||
|
"meals-v2"
|
||||||
|
],
|
||||||
|
"summary": "Update an existing meal (scoped)",
|
||||||
|
"operationId": "updateMealV2",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "meal_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Meal Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "householdSlug",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Householdslug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Meal-Input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Meal-Output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/Problem400"
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/Problem404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Problem403"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"tags": [
|
||||||
|
"v2",
|
||||||
|
"meals-v2"
|
||||||
|
],
|
||||||
|
"summary": "Delete a meal (scoped)",
|
||||||
|
"operationId": "deleteMealV2",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "meal_id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "integer",
|
||||||
|
"title": "Meal Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "householdSlug",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Householdslug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Meal-Output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"404": {
|
||||||
|
"$ref": "#/components/responses/Problem404"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Problem403"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/api/v1/households/{householdSlug}/meals/{meal_id}/consumed": {
|
"/api/v1/households/{householdSlug}/meals/{meal_id}/consumed": {
|
||||||
|
|
@ -2135,6 +2270,70 @@
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/households/{householdSlug}/meals": {
|
||||||
|
"post": {
|
||||||
|
"tags": [
|
||||||
|
"v2",
|
||||||
|
"meals-v2"
|
||||||
|
],
|
||||||
|
"summary": "Create a new meal (scoped)",
|
||||||
|
"operationId": "createMealV2",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "householdSlug",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Householdslug"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"requestBody": {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Meal-Input"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/Meal-Output"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"400": {
|
||||||
|
"$ref": "#/components/responses/Problem400"
|
||||||
|
},
|
||||||
|
"422": {
|
||||||
|
"description": "Validation Error",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/HTTPValidationError"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"403": {
|
||||||
|
"$ref": "#/components/responses/Problem403"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"security": [
|
||||||
|
{
|
||||||
|
"bearerAuth": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v1/households/{householdSlug}/shopping/current": {
|
"/api/v1/households/{householdSlug}/shopping/current": {
|
||||||
"get": {
|
"get": {
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
||||||
77
tests/test_meals_write_v2.py
Normal file
77
tests/test_meals_write_v2.py
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import datetime
|
||||||
|
import unittest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
import main
|
||||||
|
from db import connect, create
|
||||||
|
from scripts.migration_to_households import run_migration
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(":memory:")
|
||||||
|
await create(self.conn)
|
||||||
|
await run_migration(self.conn)
|
||||||
|
|
||||||
|
async def override_get_db():
|
||||||
|
try:
|
||||||
|
yield self.conn
|
||||||
|
finally:
|
||||||
|
pass
|
||||||
|
|
||||||
|
main.app.dependency_overrides[main.get_db] = override_get_db
|
||||||
|
self.client = TestClient(main.app)
|
||||||
|
|
||||||
|
# Auth user and create household
|
||||||
|
r = self.client.post(
|
||||||
|
"/api/v1/auth/register",
|
||||||
|
json={"email": "w@test.com", "password": "pw", "displayName": "W"},
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
token = r.json()["accessToken"]
|
||||||
|
self.headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
r = self.client.post("/api/v1/households", headers=self.headers, json={"name": "H"})
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
self.slug = r.json()["slug"]
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
await self.conn.close()
|
||||||
|
main.app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
def test_create_update_delete_scoped(self):
|
||||||
|
# Create a meal with suggested date and one extra ingredient
|
||||||
|
body = {
|
||||||
|
"suggestedDate": datetime.datetime.now().astimezone().isoformat(),
|
||||||
|
"chefs": [{"id": 1, "name": "A"}],
|
||||||
|
"cleanup": [{"id": 1, "name": "A"}],
|
||||||
|
"consumers": [{"id": 1, "name": "A"}],
|
||||||
|
"recipes": [],
|
||||||
|
"extraIngredients": [
|
||||||
|
{"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
r = self.client.post(
|
||||||
|
f"/api/v1/households/{self.slug}/meals",
|
||||||
|
headers=self.headers,
|
||||||
|
json=body,
|
||||||
|
)
|
||||||
|
assert r.status_code == 200, r.text
|
||||||
|
created = r.json()
|
||||||
|
meal_id = created["id"]
|
||||||
|
|
||||||
|
# Update: add an extra ingredient
|
||||||
|
created["extraIngredients"].append(
|
||||||
|
{"name": "Pepper", "line": "Pepper", "unit": "Items", "quantity": 1, "preparation": ""}
|
||||||
|
)
|
||||||
|
r2 = self.client.put(
|
||||||
|
f"/api/v1/households/{self.slug}/meals/{meal_id}", headers=self.headers, json=created
|
||||||
|
)
|
||||||
|
assert r2.status_code == 200, r2.text
|
||||||
|
updated = r2.json()
|
||||||
|
assert len(updated["extraIngredients"]) == 2
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
r3 = self.client.delete(
|
||||||
|
f"/api/v1/households/{self.slug}/meals/{meal_id}", headers=self.headers
|
||||||
|
)
|
||||||
|
assert r3.status_code == 200, r3.text
|
||||||
Loading…
Reference in a new issue