From 8fd780ee173c7c03bd76a77d74721d78c9dc38ee Mon Sep 17 00:00:00 2001 From: jableader Date: Sat, 1 Nov 2025 16:58:34 +1100 Subject: [PATCH] feat(meals): replace Person with MemberRef (displayName) in v2 Meal DTOs; add Argon2 password hashing --- api/auth_v2.py | 26 +- api/meals_v2.py | 211 +++++++++-- openapi.json | 404 ++++++++++------------ requirements.txt | 3 +- tests/test_health_and_location_headers.py | 2 +- tests/test_meals_write_v2.py | 18 +- 6 files changed, 413 insertions(+), 251 deletions(-) diff --git a/api/auth_v2.py b/api/auth_v2.py index 5a7e656..25509bb 100644 --- a/api/auth_v2.py +++ b/api/auth_v2.py @@ -15,6 +15,14 @@ from common import ApiModel from security import JwtConfig, create_jwt from settings import settings from users import repository as users_db + +# Prefer Argon2 for new passwords; keep PBKDF2 verify for backward compatibility +try: + from argon2 import PasswordHasher + + _ph: PasswordHasher | None = PasswordHasher() +except Exception: # pragma: no cover - optional dependency in some environments + _ph = None from users.models import User router = APIRouter(prefix="/auth", tags=["auth-v2"]) @@ -38,17 +46,32 @@ class TokenResponse(ApiModel): PBKDF2_ALG = "pbkdf2_sha256" -PBKDF2_ITER = 390000 # similar to Django default; adjust in settings if needed +PBKDF2_ITER = 390000 # kept for verifying older hashes SALT_BYTES = 16 def _hash_pw(pw: str) -> str: + """Hash a password. + + Uses Argon2 when available; falls back to PBKDF2 for environments without argon2-cffi. + """ + if _ph is not None: + return _ph.hash(pw) + # Fallback salt = os.urandom(SALT_BYTES) dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, PBKDF2_ITER) return f"{PBKDF2_ALG}${PBKDF2_ITER}${base64.b64encode(salt).decode()}${base64.b64encode(dk).decode()}" def _verify_pw(pw: str, stored: str) -> bool: + """Verify password against either Argon2 or PBKDF2 stored hashes.""" + # Try Argon2 first + if _ph is not None and stored.startswith("$argon2"): + try: + return _ph.verify(stored, pw) + except Exception: + return False + # PBKDF2 fallback try: alg, iter_s, salt_b64, hash_b64 = stored.split("$", 3) if alg != PBKDF2_ALG: @@ -57,7 +80,6 @@ def _verify_pw(pw: str, stored: str) -> bool: salt = base64.b64decode(salt_b64) expected = base64.b64decode(hash_b64) dk = hashlib.pbkdf2_hmac("sha256", pw.encode("utf-8"), salt, iters) - # constant-time compare return hmac.compare_digest(dk, expected) except Exception: return False diff --git a/api/meals_v2.py b/api/meals_v2.py index d4d76f7..86937e2 100644 --- a/api/meals_v2.py +++ b/api/meals_v2.py @@ -7,7 +7,9 @@ import aiosqlite from fastapi import APIRouter, Depends, Query, Request, Response import meals +import persons import shopping +import ingredients from common import ProblemDetails, ApiModel from api.deps import error_response from api.deps import get_db, get_household_from_slug @@ -15,6 +17,41 @@ from api.deps import get_db, get_household_from_slug router = APIRouter(prefix="/households/{householdSlug}/meals", tags=["meals-v2"]) +class MemberRef(ApiModel): + id: int + # Align outward schema to users/household members; use displayName + display_name: str + + +class MealRecipeIn(ApiModel): + meal_id: int + recipe_id: int + servings: float + + +class MealIn(ApiModel): + id: int = -1 + suggested_date: datetime.datetime + consumed_date: Optional[datetime.datetime] = None + chefs: List[MemberRef] + cleanup: List[MemberRef] + consumers: List[MemberRef] + recipes: List[MealRecipeIn] = [] + extra_ingredients: List[ingredients.Ingredient] = [] + + +class MealOut(ApiModel): + id: int = -1 + suggested_date: datetime.datetime + consumed_date: Optional[datetime.datetime] = None + chefs: List[MemberRef] + cleanup: List[MemberRef] + consumers: List[MemberRef] + recipes: List[meals.MealRecipe] + extra_ingredients: List[ingredients.Ingredient] + purchase_date: Optional[datetime.datetime] = None + + class MarkConsumedBody(ApiModel): consumed_date: Optional[datetime.datetime] = None @@ -29,7 +66,7 @@ async def get_upcoming_meals_scoped( date_from: datetime.datetime = Query(..., alias="from"), to: datetime.datetime = Query(...), conn: aiosqlite.Connection = Depends(get_db), -) -> List[meals.Meal]: +) -> List[MealOut]: hid = household["id"] # Use a scoped repository function, falling back to filtering in SQL until repository is fully scoped try: @@ -57,7 +94,7 @@ async def get_upcoming_meals_scoped( result.append(meals.Meal(**{k: v for k, v in zip(meals.Meal.KEYS, row)})) if not result: - return result + return [] # Load relateds similar to v1 await meals.bulk_load_participants(conn, result) @@ -65,14 +102,33 @@ async def get_upcoming_meals_scoped( await meals.load_recipes(conn, meal) await meals.load_extra_ingredients(conn, meal) - return result + # Map domain Meal -> outward MealOut + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + out: List[MealOut] = [] + for m in result: + out.append( + MealOut( + id=m.id, + suggested_date=m.suggested_date, + consumed_date=m.consumed_date, + chefs=[_to_member(p) for p in m.chefs], + cleanup=[_to_member(p) for p in m.cleanup], + consumers=[_to_member(p) for p in m.consumers], + recipes=m.recipes, + extra_ingredients=m.extra_ingredients, + purchase_date=m.purchase_date, + ) + ) + return out @router.get( "/{meal_id}", operation_id="getMealV2", summary="Get a meal by id (scoped)", - response_model=meals.Meal, + response_model=MealOut, responses={404: {"model": ProblemDetails}}, ) async def get_meal_scoped( @@ -80,19 +136,33 @@ async def get_meal_scoped( request: Request, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), -) -> meals.Meal | Response: +) -> MealOut | 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") - return meal + + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + return MealOut( + id=meal.id, + suggested_date=meal.suggested_date, + consumed_date=meal.consumed_date, + chefs=[_to_member(p) for p in meal.chefs], + cleanup=[_to_member(p) for p in meal.cleanup], + consumers=[_to_member(p) for p in meal.consumers], + recipes=meal.recipes, + extra_ingredients=meal.extra_ingredients, + purchase_date=meal.purchase_date, + ) @router.post( "/{meal_id}/consumed", operation_id="markMealConsumedV2", summary="Mark a meal as consumed (scoped)", - response_model=meals.Meal, + response_model=MealOut, responses={ 400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}, @@ -104,7 +174,7 @@ async def mark_meal_consumed_scoped( body: Optional[MarkConsumedBody] = None, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), -) -> meals.Meal | Response: +) -> MealOut | Response: hid = household["id"] consumed_date: Optional[datetime.datetime] = None if body is not None: @@ -121,66 +191,143 @@ async def mark_meal_consumed_scoped( # Clear any outstanding meal request entries for this meal await shopping.remove_request(conn, person=None, meal=meal) - return meal + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + return MealOut( + id=meal.id, + suggested_date=meal.suggested_date, + consumed_date=meal.consumed_date, + chefs=[_to_member(p) for p in meal.chefs], + cleanup=[_to_member(p) for p in meal.cleanup], + consumers=[_to_member(p) for p in meal.consumers], + recipes=meal.recipes, + extra_ingredients=meal.extra_ingredients, + purchase_date=meal.purchase_date, + ) @router.post( "", operation_id="createMealV2", summary="Create a new meal (scoped)", - response_model=meals.Meal, + response_model=MealOut, responses={400: {"model": ProblemDetails}}, ) async def create_meal_scoped( - meal: meals.Meal, + meal: MealIn, response: Response, request: Request, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), -) -> meals.Meal | Response: +) -> MealOut | Response: # Validate using existing service logic - msg = meals.validate_meal(meal) + # Map MealIn -> domain Meal + def _from_member(m: MemberRef) -> persons.Person: + return persons.Person(id=m.id, name=m.display_name) + + domain_meal = meals.Meal( + id=meal.id, + suggested_date=meal.suggested_date, + consumed_date=meal.consumed_date, + chefs=[_from_member(p) for p in meal.chefs], + cleanup=[_from_member(p) for p in meal.cleanup], + consumers=[_from_member(p) for p in meal.consumers], + recipes=[ + meals.MealRecipe(meal_id=r.meal_id, recipe_id=r.recipe_id, servings=r.servings) + for r in meal.recipes + ], + extra_ingredients=list(meal.extra_ingredients), + ) + msg = meals.validate_meal(domain_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 + await meals.insert_meal_scoped(conn, domain_meal, hid) + response.headers["Location"] = f"/api/v1/households/{household['slug']}/meals/{domain_meal.id}" + + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + return MealOut( + id=domain_meal.id, + suggested_date=domain_meal.suggested_date, + consumed_date=domain_meal.consumed_date, + chefs=[_to_member(p) for p in domain_meal.chefs], + cleanup=[_to_member(p) for p in domain_meal.cleanup], + consumers=[_to_member(p) for p in domain_meal.consumers], + recipes=domain_meal.recipes, + extra_ingredients=domain_meal.extra_ingredients, + purchase_date=domain_meal.purchase_date, + ) @router.put( "/{meal_id}", operation_id="updateMealV2", summary="Update an existing meal (scoped)", - response_model=meals.Meal, + response_model=MealOut, responses={400: {"model": ProblemDetails}, 404: {"model": ProblemDetails}}, ) async def update_meal_scoped( meal_id: int, - meal: meals.Meal, + meal: MealIn, request: Request, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), -) -> meals.Meal | Response: +) -> MealOut | 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) + + def _from_member(m: MemberRef) -> persons.Person: + return persons.Person(id=m.id, name=m.display_name) + + domain_meal = meals.Meal( + id=meal.id, + suggested_date=meal.suggested_date, + consumed_date=meal.consumed_date, + chefs=[_from_member(p) for p in meal.chefs], + cleanup=[_from_member(p) for p in meal.cleanup], + consumers=[_from_member(p) for p in meal.consumers], + recipes=[ + meals.MealRecipe(meal_id=r.meal_id, recipe_id=r.recipe_id, servings=r.servings) + for r in meal.recipes + ], + extra_ingredients=list(meal.extra_ingredients), + ) + msg = meals.validate_meal(domain_meal) if msg: return error_response(request, 400, msg) - await meals.update_meal(conn, meal) + await meals.update_meal(conn, domain_meal) # Return updated state - return await get_meal_scoped(meal_id, request, household, conn) + updated = await meals.find_meal_by_id_scoped(conn, meal_id, hid) + assert updated is not None + + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + return MealOut( + id=updated.id, + suggested_date=updated.suggested_date, + consumed_date=updated.consumed_date, + chefs=[_to_member(p) for p in updated.chefs], + cleanup=[_to_member(p) for p in updated.cleanup], + consumers=[_to_member(p) for p in updated.consumers], + recipes=updated.recipes, + extra_ingredients=updated.extra_ingredients, + purchase_date=updated.purchase_date, + ) @router.delete( "/{meal_id}", operation_id="deleteMealV2", summary="Delete a meal (scoped)", - response_model=meals.Meal, + response_model=MealOut, responses={404: {"model": ProblemDetails}}, ) async def delete_meal_scoped( @@ -188,7 +335,7 @@ async def delete_meal_scoped( request: Request, household=Depends(get_household_from_slug), conn: aiosqlite.Connection = Depends(get_db), -) -> meals.Meal | Response: +) -> MealOut | Response: hid = household["id"] meal = await meals.find_meal_by_id_scoped(conn, meal_id, hid) if not meal: @@ -202,4 +349,18 @@ async def delete_meal_scoped( # 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 + + def _to_member(p: persons.Person) -> MemberRef: + return MemberRef(id=p.id, display_name=p.name) + + return MealOut( + id=meal.id, + suggested_date=meal.suggested_date, + consumed_date=meal.consumed_date, + chefs=[_to_member(p) for p in meal.chefs], + cleanup=[_to_member(p) for p in meal.cleanup], + consumers=[_to_member(p) for p in meal.consumers], + recipes=meal.recipes, + extra_ingredients=meal.extra_ingredients, + purchase_date=meal.purchase_date, + ) diff --git a/openapi.json b/openapi.json index 3130eb4..9d7e433 100644 --- a/openapi.json +++ b/openapi.json @@ -696,7 +696,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" }, "title": "Response Getupcomingmealsv2" } @@ -758,7 +758,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" } } } @@ -818,7 +818,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Input" + "$ref": "#/components/schemas/MealIn" } } } @@ -829,7 +829,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" } } } @@ -893,7 +893,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" } } } @@ -973,7 +973,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" } } } @@ -1029,7 +1029,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Input" + "$ref": "#/components/schemas/MealIn" } } } @@ -1040,7 +1040,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/MealOut" } } } @@ -1444,7 +1444,7 @@ }, "mealsLookup": { "additionalProperties": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/Meal" }, "type": "object", "title": "Mealslookup" @@ -1458,7 +1458,7 @@ }, "recipesLookup": { "additionalProperties": { - "$ref": "#/components/schemas/Recipe-Output" + "$ref": "#/components/schemas/Recipe" }, "type": "object", "title": "Recipeslookup" @@ -1799,7 +1799,7 @@ "type": "object", "title": "MarkConsumedBody" }, - "Meal-Input": { + "Meal": { "properties": { "id": { "type": "integer", @@ -1846,85 +1846,7 @@ }, "recipes": { "items": { - "$ref": "#/components/schemas/MealRecipe-Input" - }, - "type": "array", - "title": "Recipes" - }, - "extraIngredients": { - "items": { - "$ref": "#/components/schemas/Ingredient" - }, - "type": "array", - "title": "Extraingredients" - }, - "purchaseDate": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Purchasedate" - } - }, - "type": "object", - "required": [ - "suggestedDate" - ], - "title": "Meal" - }, - "Meal-Output": { - "properties": { - "id": { - "type": "integer", - "title": "Id", - "default": -1 - }, - "suggestedDate": { - "type": "string", - "format": "date-time", - "title": "Suggesteddate" - }, - "consumedDate": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Consumeddate" - }, - "chefs": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array", - "title": "Chefs" - }, - "cleanup": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array", - "title": "Cleanup" - }, - "consumers": { - "items": { - "$ref": "#/components/schemas/Person" - }, - "type": "array", - "title": "Consumers" - }, - "recipes": { - "items": { - "$ref": "#/components/schemas/MealRecipe-Output" + "$ref": "#/components/schemas/MealRecipe" }, "type": "array", "title": "Recipes" @@ -1968,7 +1890,161 @@ ], "title": "MealIdWrapper" }, - "MealRecipe-Input": { + "MealIn": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "default": -1 + }, + "suggestedDate": { + "type": "string", + "format": "date-time", + "title": "Suggesteddate" + }, + "consumedDate": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Consumeddate" + }, + "chefs": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Chefs" + }, + "cleanup": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Cleanup" + }, + "consumers": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Consumers" + }, + "recipes": { + "items": { + "$ref": "#/components/schemas/MealRecipeIn" + }, + "type": "array", + "title": "Recipes", + "default": [] + }, + "extraIngredients": { + "items": { + "$ref": "#/components/schemas/Ingredient" + }, + "type": "array", + "title": "Extraingredients", + "default": [] + } + }, + "type": "object", + "required": [ + "suggestedDate", + "chefs", + "cleanup", + "consumers" + ], + "title": "MealIn" + }, + "MealOut": { + "properties": { + "id": { + "type": "integer", + "title": "Id", + "default": -1 + }, + "suggestedDate": { + "type": "string", + "format": "date-time", + "title": "Suggesteddate" + }, + "consumedDate": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Consumeddate" + }, + "chefs": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Chefs" + }, + "cleanup": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Cleanup" + }, + "consumers": { + "items": { + "$ref": "#/components/schemas/MemberRef" + }, + "type": "array", + "title": "Consumers" + }, + "recipes": { + "items": { + "$ref": "#/components/schemas/MealRecipe" + }, + "type": "array", + "title": "Recipes" + }, + "extraIngredients": { + "items": { + "$ref": "#/components/schemas/Ingredient" + }, + "type": "array", + "title": "Extraingredients" + }, + "purchaseDate": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Purchasedate" + } + }, + "type": "object", + "required": [ + "suggestedDate", + "chefs", + "cleanup", + "consumers", + "recipes", + "extraIngredients" + ], + "title": "MealOut" + }, + "MealRecipe": { "properties": { "mealId": { "type": "integer", @@ -1985,7 +2061,7 @@ "recipe": { "anyOf": [ { - "$ref": "#/components/schemas/Recipe-Input" + "$ref": "#/components/schemas/Recipe" }, { "type": "null" @@ -2001,7 +2077,7 @@ ], "title": "MealRecipe" }, - "MealRecipe-Output": { + "MealRecipeIn": { "properties": { "mealId": { "type": "integer", @@ -2014,16 +2090,6 @@ "servings": { "type": "number", "title": "Servings" - }, - "recipe": { - "anyOf": [ - { - "$ref": "#/components/schemas/Recipe-Output" - }, - { - "type": "null" - } - ] } }, "type": "object", @@ -2032,7 +2098,25 @@ "recipeId", "servings" ], - "title": "MealRecipe" + "title": "MealRecipeIn" + }, + "MemberRef": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "displayName": { + "type": "string", + "title": "Displayname" + } + }, + "type": "object", + "required": [ + "id", + "displayName" + ], + "title": "MemberRef" }, "Ok": { "properties": { @@ -2244,7 +2328,7 @@ }, "mealsLookup": { "additionalProperties": { - "$ref": "#/components/schemas/Meal-Output" + "$ref": "#/components/schemas/Meal" }, "type": "object", "title": "Mealslookup" @@ -2258,7 +2342,7 @@ }, "recipesLookup": { "additionalProperties": { - "$ref": "#/components/schemas/Recipe-Output" + "$ref": "#/components/schemas/Recipe" }, "type": "object", "title": "Recipeslookup" @@ -2273,113 +2357,7 @@ ], "title": "PurchasedShoppingList" }, - "Recipe-Input": { - "properties": { - "id": { - "type": "integer", - "title": "Id", - "default": -1 - }, - "name": { - "type": "string", - "title": "Name" - }, - "link": { - "type": "string", - "title": "Link" - }, - "serves": { - "type": "integer", - "title": "Serves" - }, - "imageUrls": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Imageurls" - }, - "ingredients": { - "items": { - "$ref": "#/components/schemas/Ingredient" - }, - "type": "array", - "title": "Ingredients" - }, - "basedOnRecipe": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Basedonrecipe" - }, - "dateCreated": { - "type": "string", - "format": "date-time", - "title": "Datecreated" - }, - "createdById": { - "type": "integer", - "title": "Createdbyid" - }, - "createdBy": { - "anyOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ] - }, - "dateHidden": { - "anyOf": [ - { - "type": "string", - "format": "date-time" - }, - { - "type": "null" - } - ], - "title": "Datehidden" - }, - "hiddenById": { - "anyOf": [ - { - "type": "integer" - }, - { - "type": "null" - } - ], - "title": "Hiddenbyid" - }, - "hiddenBy": { - "anyOf": [ - { - "$ref": "#/components/schemas/Person" - }, - { - "type": "null" - } - ] - } - }, - "type": "object", - "required": [ - "name", - "link", - "serves", - "createdById" - ], - "title": "Recipe" - }, - "Recipe-Output": { + "Recipe": { "properties": { "id": { "type": "integer", diff --git a/requirements.txt b/requirements.txt index 2e97b36..b5b2048 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ pydantic==2.9.2 httpx==0.27.2 ingredient-parser-nlp==1.1.2 beautifulsoup4==4.12.3 -aiosqlite==0.20.0 \ No newline at end of file +aiosqlite==0.20.0 +argon2-cffi==23.1.0 \ No newline at end of file diff --git a/tests/test_health_and_location_headers.py b/tests/test_health_and_location_headers.py index 5b7113a..1a23381 100644 --- a/tests/test_health_and_location_headers.py +++ b/tests/test_health_and_location_headers.py @@ -57,7 +57,7 @@ class TestHealthAndLocationHeaders(unittest.IsolatedAsyncioTestCase): def test_location_headers_on_create(self): # Use the registered user id placeholder for v2 meal participants - person = {"id": 1, "name": "Loc"} + person = {"id": 1, "displayName": "Loc"} # Skip recipe endpoint complexity here; covered by other tests diff --git a/tests/test_meals_write_v2.py b/tests/test_meals_write_v2.py index d026353..f5e0609 100644 --- a/tests/test_meals_write_v2.py +++ b/tests/test_meals_write_v2.py @@ -42,9 +42,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase): # 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"}], + "chefs": [{"id": 1, "displayName": "A"}], + "cleanup": [{"id": 1, "displayName": "A"}], + "consumers": [{"id": 1, "displayName": "A"}], "recipes": [], "extraIngredients": [ {"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""} @@ -80,9 +80,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase): # Base valid body base = { "suggestedDate": datetime.datetime.now().astimezone().isoformat(), - "chefs": [{"id": 1, "name": "A"}], - "cleanup": [{"id": 1, "name": "A"}], - "consumers": [{"id": 1, "name": "A"}], + "chefs": [{"id": 1, "displayName": "A"}], + "cleanup": [{"id": 1, "displayName": "A"}], + "consumers": [{"id": 1, "displayName": "A"}], "recipes": [], "extraIngredients": [ {"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""} @@ -139,9 +139,9 @@ class TestMealsWriteV2(unittest.IsolatedAsyncioTestCase): # Create a valid meal first body = { "suggestedDate": datetime.datetime.now().astimezone().isoformat(), - "chefs": [{"id": 1, "name": "A"}], - "cleanup": [{"id": 1, "name": "A"}], - "consumers": [{"id": 1, "name": "A"}], + "chefs": [{"id": 1, "displayName": "A"}], + "cleanup": [{"id": 1, "displayName": "A"}], + "consumers": [{"id": 1, "displayName": "A"}], "recipes": [], "extraIngredients": [ {"name": "Salt", "line": "Salt", "unit": "Items", "quantity": 1, "preparation": ""}