Compare commits
2 commits
54f0dcbbb9
...
255ebd4613
| Author | SHA1 | Date | |
|---|---|---|---|
| 255ebd4613 | |||
| 2a66ff39be |
9 changed files with 3473 additions and 461 deletions
|
|
@ -53,7 +53,7 @@ async def find_ingredient_by_id(conn, ingredient_id: int) -> Optional[Ingredient
|
||||||
async with conn.execute(f'''
|
async with conn.execute(f'''
|
||||||
SELECT {','.join(ingredient_keys + product_keys)} FROM Ingredient
|
SELECT {','.join(ingredient_keys + product_keys)} FROM Ingredient
|
||||||
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
||||||
WHERE id = ?
|
WHERE Ingredient.id = ?
|
||||||
''', (ingredient_id,)) as cursor:
|
''', (ingredient_id,)) as cursor:
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):])}
|
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):])}
|
||||||
|
|
|
||||||
16
main.py
16
main.py
|
|
@ -263,14 +263,15 @@ class CurrentShoppingList(BaseModel):
|
||||||
|
|
||||||
@app.get("/api/shopping/current")
|
@app.get("/api/shopping/current")
|
||||||
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
|
async def get_current_shopping_list(conn: sqlite3.Connection = Depends(get_db)) -> CurrentShoppingList:
|
||||||
outstanding_requests, purchased_requests, meal_requests = await shopping.get_outstanding_requests(conn)
|
outstanding_requests, purchased_requests, meal_requests, meals_lookup, recipes_lookup, ingredients_lookup = await shopping.get_outstanding_requests(conn)
|
||||||
other_shopping_list_ids = {item.list_id for item in purchased_requests}
|
other_shopping_list_ids = {item.list_id for item in purchased_requests}
|
||||||
|
|
||||||
shopping_list_lookup = { list_id: await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids }
|
shopping_list_lookup = { list_id: await shopping.load_shopping_list(conn, list_id) for list_id in other_shopping_list_ids }
|
||||||
|
|
||||||
# Reduce the data structure to items and lookups
|
# Add any additional items from shopping lists to the existing lookups
|
||||||
items = meal_requests + outstanding_requests + purchased_requests + [item for sl in shopping_list_lookup.values() for item in sl.items]
|
additional_items = [item for sl in shopping_list_lookup.values() for item in sl.items]
|
||||||
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, items)
|
if additional_items:
|
||||||
|
await shopping.to_lookups(conn, additional_items, meals_lookup, recipes_lookup, ingredients_lookup)
|
||||||
|
|
||||||
return CurrentShoppingList(
|
return CurrentShoppingList(
|
||||||
outstanding_items=outstanding_requests,
|
outstanding_items=outstanding_requests,
|
||||||
|
|
@ -291,6 +292,9 @@ class PurchasedShoppingList(BaseModel):
|
||||||
@app.get("/api/shopping/{list_id}")
|
@app.get("/api/shopping/{list_id}")
|
||||||
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> PurchasedShoppingList:
|
async def get_shopping_list(list_id: int, conn: sqlite3.Connection = Depends(get_db)) -> PurchasedShoppingList:
|
||||||
shopping_list = await shopping.load_shopping_list(conn, list_id)
|
shopping_list = await shopping.load_shopping_list(conn, list_id)
|
||||||
|
if not shopping_list:
|
||||||
|
return JSONResponse(status_code=404, content={'message': 'Shopping list not found'})
|
||||||
|
|
||||||
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, shopping_list.items)
|
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(conn, shopping_list.items)
|
||||||
return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup)
|
return PurchasedShoppingList(list=shopping_list, meals_lookup=meals_lookup, recipes_lookup=recipes_lookup, ingredients_lookup=ingredients_lookup)
|
||||||
|
|
||||||
|
|
@ -307,14 +311,14 @@ async def purchase_ingredients(shopping_list: shopping.ShoppingList, conn: sqlit
|
||||||
|
|
||||||
@app.get("/api/shopping/current/me/ingredients")
|
@app.get("/api/shopping/current/me/ingredients")
|
||||||
async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
async def get_my_shopping_list(conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
||||||
return [r.ingredient async for r in shopping.get_persons_requests(conn, person.id)]
|
return await shopping.get_persons_requests(conn, person.id)
|
||||||
|
|
||||||
@app.post("/api/shopping/current/me/ingredients")
|
@app.post("/api/shopping/current/me/ingredients")
|
||||||
async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
async def sync_my_shopping_list(requests: List[ingredients.Ingredient], conn: sqlite3.Connection = Depends(get_db), person: persons.Person = Depends(cookie_person)) -> List[ingredients.Ingredient]:
|
||||||
def isMatching(a: ingredients.Ingredient, b: ingredients.Ingredient) -> bool:
|
def isMatching(a: ingredients.Ingredient, b: ingredients.Ingredient) -> bool:
|
||||||
return a.id == b.id or a.line == b.line
|
return a.id == b.id or a.line == b.line
|
||||||
|
|
||||||
my_shopping_list = [r.ingredient async for r in shopping.get_persons_requests(conn, person.id) if r.ingredient is not None]
|
my_shopping_list = await shopping.get_persons_requests(conn, person.id)
|
||||||
to_remove = [r for r in my_shopping_list if not any(isMatching(r, req) for req in requests)]
|
to_remove = [r for r in my_shopping_list if not any(isMatching(r, req) for req in requests)]
|
||||||
to_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]
|
to_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,26 +10,10 @@ async def to_lookups(conn, items: List[ShoppingListItem], meals_lookup: Dict[int
|
||||||
recipes_lookup = recipes_lookup or {}
|
recipes_lookup = recipes_lookup or {}
|
||||||
ingredients_lookup = ingredients_lookup or {}
|
ingredients_lookup = ingredients_lookup or {}
|
||||||
|
|
||||||
lookups = (meals_lookup, recipes_lookup, ingredients_lookup)
|
await _ensure_lookups_populated(conn, items, meals_lookup, recipes_lookup, ingredients_lookup)
|
||||||
_move_refs_to_lookups(items, *lookups)
|
return meals_lookup, recipes_lookup, ingredients_lookup
|
||||||
await _ensure_lookups_populated(conn, items, *lookups)
|
|
||||||
return lookups
|
|
||||||
|
|
||||||
|
|
||||||
def _move_refs_to_lookups(items: List[ShoppingListItem], meals_lookup: Dict[int, Any], recipes_lookup: Dict[int, Any], ingredients_lookup: Dict[int, Any]):
|
|
||||||
for item in items:
|
|
||||||
if item.meal and not item.meal.id in meals_lookup:
|
|
||||||
meals_lookup[item.meal.id] = item.meal
|
|
||||||
item.meal = None
|
|
||||||
|
|
||||||
if item.ingredient and not item.ingredient.id in ingredients_lookup:
|
|
||||||
ingredients_lookup[item.ingredient.id] = item.ingredient
|
|
||||||
item.ingredient = None
|
|
||||||
|
|
||||||
if item.recipe and not item.recipe.id in recipes_lookup:
|
|
||||||
recipes_lookup[item.recipe.id] = item.recipe
|
|
||||||
item.recipe = None
|
|
||||||
|
|
||||||
async def _ensure_lookups_populated(conn, items: List[ShoppingListItem], meals_lookup, recipes_lookup, ingredients_lookup):
|
async def _ensure_lookups_populated(conn, items: List[ShoppingListItem], meals_lookup, recipes_lookup, ingredients_lookup):
|
||||||
for item in items:
|
for item in items:
|
||||||
# If the any item is not in the lookup, we need to add it
|
# If the any item is not in the lookup, we need to add it
|
||||||
|
|
@ -40,32 +24,51 @@ async def _ensure_lookups_populated(conn, items: List[ShoppingListItem], meals_l
|
||||||
if item.ingredient_id and item.ingredient_id not in ingredients_lookup:
|
if item.ingredient_id and item.ingredient_id not in ingredients_lookup:
|
||||||
ingredients_lookup[item.ingredient_id] = await ingredients.find_ingredient_by_id(conn, item.ingredient_id)
|
ingredients_lookup[item.ingredient_id] = await ingredients.find_ingredient_by_id(conn, item.ingredient_id)
|
||||||
|
|
||||||
async def get_persons_requests(conn, person_id: int) -> AsyncIterator[ShoppingListItem]:
|
async def get_persons_requests(conn, person_id: int) -> List[ingredients.Ingredient]:
|
||||||
async for item in _find_items_by_list_id(conn, None):
|
ids = [item.ingredient_id async for item in _find_items_by_list_id(conn, None) if item.person_id == person_id and item.ingredient_id is not None and item.meal_id is None]
|
||||||
if item.person_id == person_id and item.ingredient_id is not None:
|
return [await ingredients.find_ingredient_by_id(conn, ingredient_id) for ingredient_id in ids]
|
||||||
yield item
|
|
||||||
|
|
||||||
def flatten_items(items: Iterator[ShoppingListItem]) -> Iterator[ShoppingListItem]:
|
def flatten_items(items: Iterator[ShoppingListItem], meals_lookup: Dict[int, Any]) -> Iterator[ShoppingListItem]:
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.meal:
|
if item.meal_id and item.meal_id in meals_lookup:
|
||||||
for mealRecipe in item.meal.recipes:
|
meal = meals_lookup[item.meal_id]
|
||||||
|
for mealRecipe in meal.recipes:
|
||||||
for ingredient in mealRecipe.recipe.ingredients:
|
for ingredient in mealRecipe.recipe.ingredients:
|
||||||
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, recipe=mealRecipe.recipe, person_id=item.person_id, created_date=item.created_date)
|
yield ShoppingListItem(
|
||||||
|
ingredient_id=ingredient.id,
|
||||||
|
meal_id=item.meal_id,
|
||||||
|
recipe_id=mealRecipe.recipe.id,
|
||||||
|
person_id=item.person_id,
|
||||||
|
created_date=item.created_date
|
||||||
|
)
|
||||||
|
|
||||||
for ingredient in item.meal.extra_ingredients:
|
for ingredient in meal.extra_ingredients:
|
||||||
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date)
|
yield ShoppingListItem(
|
||||||
|
ingredient_id=ingredient.id,
|
||||||
|
meal_id=item.meal_id,
|
||||||
|
person_id=item.person_id,
|
||||||
|
created_date=item.created_date
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
yield item
|
yield item
|
||||||
|
|
||||||
async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem], List[ShoppingListItem]]:
|
async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[ShoppingListItem], List[ShoppingListItem], Dict[int, Any], Dict[int, Any], Dict[int, Any]]:
|
||||||
current_requests = [r async for r in _find_items_by_list_id(conn, None)]
|
current_requests = [r async for r in _find_items_by_list_id(conn, None)]
|
||||||
meal_requests = [r for r in current_requests if r.meal_id is not None and r.meal_id > 0 and r.meal is not None]
|
meal_requests = [r for r in current_requests if r.meal_id is not None and r.meal_id > 0]
|
||||||
meals = {r.meal_id: r.meal for r in meal_requests}
|
|
||||||
purchased_ingredients = {(r.ingredient_id, r.meal_id, r.recipe_id): r async for r in _get_purchased_ingredients(conn, list(meals.keys()))}
|
# Get lookups for meals to enable flattening
|
||||||
|
meals_lookup, recipes_lookup, ingredients_lookup = await to_lookups(conn, current_requests)
|
||||||
|
|
||||||
|
meal_ids = [r.meal_id for r in meal_requests if r.meal_id]
|
||||||
|
purchased_ingredients = {(r.ingredient_id, r.meal_id, r.recipe_id): r async for r in _get_purchased_ingredients(conn, meal_ids)}
|
||||||
|
|
||||||
outstanding_items = []
|
outstanding_items = []
|
||||||
purchased_items = []
|
purchased_items = []
|
||||||
flattened = flatten_items(current_requests)
|
flattened = list(flatten_items(current_requests, meals_lookup))
|
||||||
|
|
||||||
|
# Now ensure that all ingredients from the flattened items are in the lookup
|
||||||
|
await _ensure_lookups_populated(conn, flattened, meals_lookup, recipes_lookup, ingredients_lookup)
|
||||||
|
|
||||||
for r in flattened:
|
for r in flattened:
|
||||||
# Meal ingredients may have already been purchased
|
# Meal ingredients may have already been purchased
|
||||||
if r.meal_id is not None and r.meal_id > 0:
|
if r.meal_id is not None and r.meal_id > 0:
|
||||||
|
|
@ -76,4 +79,4 @@ async def get_outstanding_requests(conn) -> Tuple[List[ShoppingListItem], List[S
|
||||||
|
|
||||||
outstanding_items.append(r)
|
outstanding_items.append(r)
|
||||||
|
|
||||||
return outstanding_items, purchased_items, meal_requests
|
return outstanding_items, purchased_items, meal_requests, meals_lookup, recipes_lookup, ingredients_lookup
|
||||||
|
|
|
||||||
|
|
@ -15,16 +15,12 @@ class ShoppingListItem(BaseLinkedModel):
|
||||||
list_id: Optional[int] = None
|
list_id: Optional[int] = None
|
||||||
|
|
||||||
person_id: int = -1
|
person_id: int = -1
|
||||||
person: Optional[Person] = None
|
|
||||||
|
|
||||||
ingredient_id: Optional[int] = None
|
ingredient_id: Optional[int] = None
|
||||||
ingredient: Optional[Ingredient] = None
|
|
||||||
|
|
||||||
recipe_id: Optional[int] = None
|
recipe_id: Optional[int] = None
|
||||||
recipe: Optional[Recipe] = None
|
|
||||||
|
|
||||||
meal_id: Optional[int] = None
|
meal_id: Optional[int] = None
|
||||||
meal: Optional[Meal] = None
|
|
||||||
|
|
||||||
created_date: datetime = datetime.now().astimezone()
|
created_date: datetime = datetime.now().astimezone()
|
||||||
|
|
||||||
|
|
@ -76,7 +72,7 @@ def validate_request(request: ShoppingListItem) -> None:
|
||||||
raise ValueError('Requests must have a person')
|
raise ValueError('Requests must have a person')
|
||||||
|
|
||||||
# A request must have either an ingredient or a meal, but not both
|
# A request must have either an ingredient or a meal, but not both
|
||||||
if not request.ingredient and not request.meal:
|
if not request.ingredient_id and not request.meal_id:
|
||||||
raise ValueError('Request must have either an ingredient or a meal')
|
raise ValueError('Request must have either an ingredient or a meal')
|
||||||
|
|
||||||
async def purchase(conn, shopping_list: ShoppingList) -> None:
|
async def purchase(conn, shopping_list: ShoppingList) -> None:
|
||||||
|
|
@ -98,9 +94,6 @@ async def purchase(conn, shopping_list: ShoppingList) -> None:
|
||||||
item.list_id = shopping_list.id
|
item.list_id = shopping_list.id
|
||||||
validate_request(item)
|
validate_request(item)
|
||||||
|
|
||||||
if item.ingredient and item.ingredient.id < 0:
|
|
||||||
await insert_ingredient(conn, item.ingredient)
|
|
||||||
|
|
||||||
if item.ingredient_id is None or item.ingredient_id < 0:
|
if item.ingredient_id is None or item.ingredient_id < 0:
|
||||||
raise ValueError('Ingredient request must have a valid ingredient id')
|
raise ValueError('Ingredient request must have a valid ingredient id')
|
||||||
|
|
||||||
|
|
@ -177,7 +170,14 @@ async def request(conn, person: Person, ingredient: Optional[Ingredient] = None,
|
||||||
if ingredient is not None and ingredient.id < 0:
|
if ingredient is not None and ingredient.id < 0:
|
||||||
await insert_ingredient(conn, ingredient)
|
await insert_ingredient(conn, ingredient)
|
||||||
|
|
||||||
item = ShoppingListItem(ingredient=ingredient, person=person, meal=meal)
|
ingredient_id = ingredient.id if ingredient else None
|
||||||
|
meal_id = meal.id if meal else None
|
||||||
|
|
||||||
|
item = ShoppingListItem(
|
||||||
|
ingredient_id=ingredient_id,
|
||||||
|
person_id=person.id,
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
validate_request(item)
|
validate_request(item)
|
||||||
|
|
||||||
|
|
@ -210,18 +210,11 @@ async def remove_request(conn, person: Person = None, meal: Optional[Meal] = Non
|
||||||
raise ValueError('Must specify either a meal or an ingredient to remove')
|
raise ValueError('Must specify either a meal or an ingredient to remove')
|
||||||
|
|
||||||
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
|
async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[ShoppingListItem]:
|
||||||
# Join Ingredient and Product to also load ingredient and product
|
|
||||||
ingredient_keys = [f'ingredient.{key}' for key in Ingredient.KEYS]
|
|
||||||
product_keys = [f'product.{key}' for key in Product.KEYS]
|
|
||||||
request_keys = [f'shoppinglistitem.{key}' for key in ShoppingListItem.KEYS]
|
request_keys = [f'shoppinglistitem.{key}' for key in ShoppingListItem.KEYS]
|
||||||
person_keys = [f'person.{key}' for key in Person.KEYS]
|
|
||||||
|
|
||||||
select = f'''
|
select = f'''
|
||||||
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
|
SELECT {','.join(request_keys)}
|
||||||
FROM ShoppingListItem
|
FROM ShoppingListItem
|
||||||
LEFT JOIN Ingredient ON ShoppingListItem.ingredient_id = Ingredient.id
|
|
||||||
LEFT JOIN Product ON Ingredient.product_id = Product.id
|
|
||||||
LEFT JOIN Person ON ShoppingListItem.person_id = Person.id
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
where, params = ' WHERE list_id IS NULL', ()
|
where, params = ' WHERE list_id IS NULL', ()
|
||||||
|
|
@ -231,21 +224,8 @@ async def find_items_by_list_id(conn, list_id: Optional[int]) -> AsyncIterator[S
|
||||||
cursor = await conn.execute(select + where, params)
|
cursor = await conn.execute(select + where, params)
|
||||||
|
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):len(Ingredient.KEYS) + len(Product.KEYS)])}
|
request_keys = {k:v for k,v in zip(ShoppingListItem.KEYS, row)}
|
||||||
product = Product(**product_keys) if product_keys['id'] else None
|
request = ShoppingListItem(**request_keys)
|
||||||
|
|
||||||
ingredient_keys = {k:v for k,v in zip(Ingredient.KEYS, row[:len(Ingredient.KEYS)])}
|
|
||||||
ingredient = Ingredient(**ingredient_keys, product=product) if ingredient_keys['id'] else None
|
|
||||||
|
|
||||||
person_keys = {k:v for k,v in zip(Person.KEYS, row[-len(Person.KEYS):])}
|
|
||||||
person = Person(**person_keys) if person_keys['id'] else None
|
|
||||||
|
|
||||||
request_keys = {k:v for k,v in zip(ShoppingListItem.KEYS, row[len(Ingredient.KEYS) + len(Product.KEYS):-len(Person.KEYS)])}
|
|
||||||
request = ShoppingListItem(**request_keys, ingredient=ingredient, person=person)
|
|
||||||
|
|
||||||
if request.meal_id is not None:
|
|
||||||
request.meal = await find_meal_by_id(conn, request.meal_id)
|
|
||||||
|
|
||||||
yield request
|
yield request
|
||||||
|
|
||||||
async def load_shopping_list(conn, id: int) -> ShoppingList:
|
async def load_shopping_list(conn, id: int) -> ShoppingList:
|
||||||
|
|
@ -272,7 +252,7 @@ async def get_purchased_ingredients(conn, meal_ids: List[int]) -> AsyncIterator[
|
||||||
async with conn.execute(f'''
|
async with conn.execute(f'''
|
||||||
SELECT {','.join(ShoppingListItem.KEYS)}
|
SELECT {','.join(ShoppingListItem.KEYS)}
|
||||||
FROM ShoppingListItem
|
FROM ShoppingListItem
|
||||||
WHERE meal_id IN ({','.join(['?'] * len(meal_ids))})
|
WHERE meal_id IN ({','.join(['?'] * len(meal_ids))}) AND list_id IS NOT NULL
|
||||||
''', meal_ids) as cursor:
|
''', meal_ids) as cursor:
|
||||||
async for row in cursor:
|
async for row in cursor:
|
||||||
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
|
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "https://www.coles.com.au/",
|
||||||
|
"headers": {
|
||||||
|
"host": "www.coles.com.au",
|
||||||
|
"user-agent": "Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0",
|
||||||
|
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
||||||
|
"accept-language": "en-US,en;q=0.5",
|
||||||
|
"accept-encoding": "gzip, deflate, br",
|
||||||
|
"dnt": "1",
|
||||||
|
"sec-gpc": "1",
|
||||||
|
"connection": "keep-alive",
|
||||||
|
"upgrade-insecure-requests": "1",
|
||||||
|
"sec-fetch-dest": "document",
|
||||||
|
"sec-fetch-mode": "navigate",
|
||||||
|
"sec-fetch-site": "none",
|
||||||
|
"sec-fetch-user": "?1",
|
||||||
|
"pragma": "no-cache",
|
||||||
|
"cache-control": "no-cache"
|
||||||
|
},
|
||||||
|
"content": ""
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"status_code": 200,
|
||||||
|
"headers": {
|
||||||
|
"content-type": "text/html",
|
||||||
|
"cache-control": "no-cache, no-store",
|
||||||
|
"connection": "close",
|
||||||
|
"content-length": "3345",
|
||||||
|
"x-iinfo": "7-26769261-0 0CNN RT(1727584618151 22) q(0 -1 -1 1) r(0 -1) B10(14,0,0)",
|
||||||
|
"strict-transport-security": "max-age=31536000; includeSubDomains",
|
||||||
|
"set-cookie": "visid_incap_2800108=vNYK15gzRoOHbzyO31mNh2rZ+GYAAAAAQUIPAAAAAABwIQoipD3Nw8q38f6JHIb5; expires=Sun, 28 Sep 2025 12:16:28 GMT; HttpOnly; path=/; Domain=.coles.com.au; Secure; SameSite=None, incap_ses_808_2800108=AWejGplhXmNxgGAG25c2C2rZ+GYAAAAAr91ZD4bl3op+nNtxHCpveg==; path=/; Domain=.coles.com.au; Secure; SameSite=None"
|
||||||
|
},
|
||||||
|
"content": "<!DOCTYPE html><html><head><title>Coles Product Page</title></head><body><div>Mock Coles product page with version 20240926.02_v4.18.0 for testing</div></body></html>",
|
||||||
|
"cookies": {
|
||||||
|
"visid_incap_2800108": "vNYK15gzRoOHbzyO31mNh2rZ+GYAAAAAQUIPAAAAAABwIQoipD3Nw8q38f6JHIb5",
|
||||||
|
"incap_ses_808_2800108": "AWejGplhXmNxgGAG25c2C2rZ+GYAAAAAr91ZD4bl3op+nNtxHCpveg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
593
tests/test_ingredients.py
Normal file
593
tests/test_ingredients.py
Normal file
|
|
@ -0,0 +1,593 @@
|
||||||
|
import unittest
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import tests.test_data as test_data
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
def reload_test_data():
|
||||||
|
global test_data
|
||||||
|
test_data = importlib.reload(test_data)
|
||||||
|
|
||||||
|
from db import connect, create
|
||||||
|
import ingredients
|
||||||
|
import ingredients.db as ingredients_db
|
||||||
|
import products.db as products_db
|
||||||
|
import units
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngredient(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_persons(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_ingredient_creation(self):
|
||||||
|
"""Test basic ingredient creation"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Broccoli",
|
||||||
|
line="500g fresh broccoli",
|
||||||
|
unit="g",
|
||||||
|
quantity=500.0,
|
||||||
|
preparation="chopped"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ingredient.name, "Broccoli")
|
||||||
|
self.assertEqual(ingredient.line, "500g fresh broccoli")
|
||||||
|
self.assertEqual(ingredient.unit, "g")
|
||||||
|
self.assertEqual(ingredient.quantity, 500.0)
|
||||||
|
self.assertEqual(ingredient.preparation, "chopped")
|
||||||
|
|
||||||
|
async def test_insert_ingredient(self):
|
||||||
|
"""Test inserting an ingredient into the database"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Garlic",
|
||||||
|
line="2 cloves garlic",
|
||||||
|
unit="Items",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="minced"
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
self.assertGreater(ingredient.id, 0)
|
||||||
|
|
||||||
|
# Verify it was inserted correctly
|
||||||
|
found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id)
|
||||||
|
self.assertIsNotNone(found_ingredient)
|
||||||
|
self.assertEqual(found_ingredient.name, "Garlic")
|
||||||
|
self.assertEqual(found_ingredient.quantity, 2.0)
|
||||||
|
|
||||||
|
async def test_insert_ingredient_with_product(self):
|
||||||
|
"""Test inserting an ingredient with an associated product"""
|
||||||
|
# First create and insert a product
|
||||||
|
product = test_data.Products.broccoli
|
||||||
|
await products_db.insert_product(self.conn, product, {})
|
||||||
|
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Fresh Broccoli",
|
||||||
|
line="1 piece fresh broccoli",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product_id=product.id,
|
||||||
|
product=product
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
# Verify the ingredient was inserted with the product reference
|
||||||
|
found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id)
|
||||||
|
self.assertIsNotNone(found_ingredient)
|
||||||
|
self.assertEqual(found_ingredient.product_id, product.id)
|
||||||
|
self.assertIsNotNone(found_ingredient.product)
|
||||||
|
self.assertEqual(found_ingredient.product.name, product.name)
|
||||||
|
|
||||||
|
async def test_find_ingredient_by_id_not_found(self):
|
||||||
|
"""Test finding a non-existent ingredient returns None"""
|
||||||
|
result = await ingredients_db.find_ingredient_by_id(self.conn, 999)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
async def test_find_ingredients_by_recipe_id(self):
|
||||||
|
"""Test finding ingredients by recipe ID"""
|
||||||
|
# Create ingredients with the same recipe_id
|
||||||
|
recipe_id = 1
|
||||||
|
|
||||||
|
ingredient1 = ingredients_db.Ingredient(
|
||||||
|
name="Flour",
|
||||||
|
line="2 cups flour",
|
||||||
|
unit="cups",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="",
|
||||||
|
recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient2 = ingredients_db.Ingredient(
|
||||||
|
name="Sugar",
|
||||||
|
line="1 cup sugar",
|
||||||
|
unit="cups",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient1)
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient2)
|
||||||
|
|
||||||
|
# Find ingredients by recipe ID
|
||||||
|
ingredients_list = []
|
||||||
|
async for ingredient in ingredients_db.find_ingredients_by_recipe_id(self.conn, recipe_id):
|
||||||
|
ingredients_list.append(ingredient)
|
||||||
|
|
||||||
|
self.assertEqual(len(ingredients_list), 2)
|
||||||
|
names = [ing.name for ing in ingredients_list]
|
||||||
|
self.assertIn("Flour", names)
|
||||||
|
self.assertIn("Sugar", names)
|
||||||
|
|
||||||
|
async def test_find_ingredients_by_meal_id(self):
|
||||||
|
"""Test finding ingredients by meal ID"""
|
||||||
|
# Create ingredients with the same meal_id
|
||||||
|
meal_id = 1
|
||||||
|
|
||||||
|
ingredient1 = ingredients_db.Ingredient(
|
||||||
|
name="Chicken",
|
||||||
|
line="1 lb chicken breast",
|
||||||
|
unit="lb",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="diced",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient2 = ingredients_db.Ingredient(
|
||||||
|
name="Rice",
|
||||||
|
line="2 cups rice",
|
||||||
|
unit="cups",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient1)
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient2)
|
||||||
|
|
||||||
|
# Find ingredients by meal ID
|
||||||
|
ingredients_list = []
|
||||||
|
async for ingredient in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ingredient)
|
||||||
|
|
||||||
|
self.assertEqual(len(ingredients_list), 2)
|
||||||
|
names = [ing.name for ing in ingredients_list]
|
||||||
|
self.assertIn("Chicken", names)
|
||||||
|
self.assertIn("Rice", names)
|
||||||
|
|
||||||
|
async def test_delete_ingredients_by_meal_id(self):
|
||||||
|
"""Test deleting ingredients by meal ID"""
|
||||||
|
meal_id = 1
|
||||||
|
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Tomato",
|
||||||
|
line="2 tomatoes",
|
||||||
|
unit="Items",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="sliced",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
# Verify ingredient exists
|
||||||
|
ingredients_list = []
|
||||||
|
async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ing)
|
||||||
|
self.assertEqual(len(ingredients_list), 1)
|
||||||
|
|
||||||
|
# Delete ingredients by meal ID
|
||||||
|
await ingredients_db.delete_ingredients_by_meal_id(self.conn, meal_id)
|
||||||
|
|
||||||
|
# Verify ingredients are deleted
|
||||||
|
ingredients_list = []
|
||||||
|
async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ing)
|
||||||
|
self.assertEqual(len(ingredients_list), 0)
|
||||||
|
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
import tests.test_data as test_data
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
def reload_test_data():
|
||||||
|
global test_data
|
||||||
|
test_data = importlib.reload(test_data)
|
||||||
|
|
||||||
|
from db import connect, create
|
||||||
|
import ingredients
|
||||||
|
import ingredients.db as ingredients_db
|
||||||
|
import products.db as products_db
|
||||||
|
import units
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngredient(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_persons(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_ingredient_creation(self):
|
||||||
|
"""Test basic ingredient creation"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Broccoli",
|
||||||
|
line="500g fresh broccoli",
|
||||||
|
unit="g",
|
||||||
|
quantity=500.0,
|
||||||
|
preparation="chopped"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ingredient.name, "Broccoli")
|
||||||
|
self.assertEqual(ingredient.line, "500g fresh broccoli")
|
||||||
|
self.assertEqual(ingredient.unit, "g")
|
||||||
|
self.assertEqual(ingredient.quantity, 500.0)
|
||||||
|
self.assertEqual(ingredient.preparation, "chopped")
|
||||||
|
|
||||||
|
async def test_insert_ingredient(self):
|
||||||
|
"""Test inserting an ingredient into the database"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Garlic",
|
||||||
|
line="2 cloves garlic",
|
||||||
|
unit="Items",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="minced"
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
self.assertGreater(ingredient.id, 0)
|
||||||
|
|
||||||
|
# Verify it was inserted correctly
|
||||||
|
found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id)
|
||||||
|
self.assertIsNotNone(found_ingredient)
|
||||||
|
self.assertEqual(found_ingredient.name, "Garlic")
|
||||||
|
self.assertEqual(found_ingredient.quantity, 2.0)
|
||||||
|
|
||||||
|
async def test_insert_ingredient_with_product(self):
|
||||||
|
"""Test inserting an ingredient with an associated product"""
|
||||||
|
# First create and insert a product
|
||||||
|
product = test_data.Products.broccoli
|
||||||
|
await products_db.insert_product(self.conn, product, {})
|
||||||
|
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Fresh Broccoli",
|
||||||
|
line="1 piece fresh broccoli",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product_id=product.id,
|
||||||
|
product=product
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
# Verify the ingredient was inserted with the product reference
|
||||||
|
found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id)
|
||||||
|
self.assertIsNotNone(found_ingredient)
|
||||||
|
self.assertEqual(found_ingredient.product_id, product.id)
|
||||||
|
self.assertIsNotNone(found_ingredient.product)
|
||||||
|
self.assertEqual(found_ingredient.product.name, product.name)
|
||||||
|
|
||||||
|
async def test_find_ingredient_by_id_not_found(self):
|
||||||
|
"""Test finding a non-existent ingredient returns None"""
|
||||||
|
result = await ingredients_db.find_ingredient_by_id(self.conn, 999)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
async def test_find_ingredients_by_recipe_id(self):
|
||||||
|
"""Test finding ingredients by recipe ID"""
|
||||||
|
# Create ingredients with the same recipe_id
|
||||||
|
recipe_id = 1
|
||||||
|
|
||||||
|
ingredient1 = ingredients_db.Ingredient(
|
||||||
|
name="Flour",
|
||||||
|
line="2 cups flour",
|
||||||
|
unit="cups",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="",
|
||||||
|
recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient2 = ingredients_db.Ingredient(
|
||||||
|
name="Sugar",
|
||||||
|
line="1 cup sugar",
|
||||||
|
unit="cups",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
recipe_id=recipe_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient1)
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient2)
|
||||||
|
|
||||||
|
# Find ingredients by recipe ID
|
||||||
|
ingredients_list = []
|
||||||
|
async for ingredient in ingredients_db.find_ingredients_by_recipe_id(self.conn, recipe_id):
|
||||||
|
ingredients_list.append(ingredient)
|
||||||
|
|
||||||
|
self.assertEqual(len(ingredients_list), 2)
|
||||||
|
names = [ing.name for ing in ingredients_list]
|
||||||
|
self.assertIn("Flour", names)
|
||||||
|
self.assertIn("Sugar", names)
|
||||||
|
|
||||||
|
async def test_find_ingredients_by_meal_id(self):
|
||||||
|
"""Test finding ingredients by meal ID"""
|
||||||
|
# Create ingredients with the same meal_id
|
||||||
|
meal_id = 1
|
||||||
|
|
||||||
|
ingredient1 = ingredients_db.Ingredient(
|
||||||
|
name="Chicken",
|
||||||
|
line="1 lb chicken breast",
|
||||||
|
unit="lb",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="diced",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient2 = ingredients_db.Ingredient(
|
||||||
|
name="Rice",
|
||||||
|
line="2 cups rice",
|
||||||
|
unit="cups",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient1)
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient2)
|
||||||
|
|
||||||
|
# Find ingredients by meal ID
|
||||||
|
ingredients_list = []
|
||||||
|
async for ingredient in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ingredient)
|
||||||
|
|
||||||
|
self.assertEqual(len(ingredients_list), 2)
|
||||||
|
names = [ing.name for ing in ingredients_list]
|
||||||
|
self.assertIn("Chicken", names)
|
||||||
|
self.assertIn("Rice", names)
|
||||||
|
|
||||||
|
async def test_delete_ingredients_by_meal_id(self):
|
||||||
|
"""Test deleting ingredients by meal ID"""
|
||||||
|
meal_id = 1
|
||||||
|
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Tomato",
|
||||||
|
line="2 tomatoes",
|
||||||
|
unit="Items",
|
||||||
|
quantity=2.0,
|
||||||
|
preparation="sliced",
|
||||||
|
meal_id=meal_id
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
# Verify ingredient exists
|
||||||
|
ingredients_list = []
|
||||||
|
async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ing)
|
||||||
|
self.assertEqual(len(ingredients_list), 1)
|
||||||
|
|
||||||
|
# Delete ingredients by meal ID
|
||||||
|
await ingredients_db.delete_ingredients_by_meal_id(self.conn, meal_id)
|
||||||
|
|
||||||
|
# Verify ingredients are deleted
|
||||||
|
ingredients_list = []
|
||||||
|
async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id):
|
||||||
|
ingredients_list.append(ing)
|
||||||
|
self.assertEqual(len(ingredients_list), 0)
|
||||||
|
|
||||||
|
async def test_ingredient_with_negative_product_id(self):
|
||||||
|
"""Test that negative product_id is converted to None during insertion"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Test Ingredient",
|
||||||
|
line="1 test ingredient",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product_id=-1
|
||||||
|
)
|
||||||
|
|
||||||
|
await ingredients_db.insert_ingredient(self.conn, ingredient)
|
||||||
|
|
||||||
|
found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id)
|
||||||
|
self.assertIsNone(found_ingredient.product_id)
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngredientParsing(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_persons(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_parse_ingredient_from_link_invalid_format(self):
|
||||||
|
"""Test parsing ingredient from invalid link format returns None"""
|
||||||
|
invalid_links = [
|
||||||
|
"invalid link format",
|
||||||
|
"just a url https://example.com",
|
||||||
|
"no quantity https://example.com",
|
||||||
|
"",
|
||||||
|
"abc https://example.com"
|
||||||
|
]
|
||||||
|
|
||||||
|
for invalid_link in invalid_links:
|
||||||
|
result = await ingredients.parse_ingredient_from_link(self.conn, invalid_link)
|
||||||
|
self.assertIsNone(result, f"Should return None for: {invalid_link}")
|
||||||
|
|
||||||
|
async def test_parse_ingredient_from_link_valid_format_no_quantity(self):
|
||||||
|
"""Test parsing ingredient from valid link format without explicit quantity"""
|
||||||
|
link = "https://www.woolworths.com.au/shop/productdetails/134681/fresh-broccoli"
|
||||||
|
|
||||||
|
result = await ingredients.parse_ingredient_from_link(self.conn, link)
|
||||||
|
|
||||||
|
# The scraper actually works for this URL, so we should get a result
|
||||||
|
self.assertIsNotNone(result)
|
||||||
|
self.assertEqual(result.quantity, 1.0) # Default quantity when none specified
|
||||||
|
self.assertEqual(result.unit, units.ITEMS.name)
|
||||||
|
self.assertIsInstance(result, ingredients_db.Ingredient)
|
||||||
|
|
||||||
|
async def test_parse_ingredient_from_link_regex_parsing(self):
|
||||||
|
"""Test that the regex correctly parses quantity and URL from valid links"""
|
||||||
|
import re
|
||||||
|
|
||||||
|
# Test the regex pattern used in parse_ingredient_from_link
|
||||||
|
test_cases = [
|
||||||
|
("2 https://example.com", "2", "https://example.com"),
|
||||||
|
("10 https://www.woolworths.com.au/product", "10", "https://www.woolworths.com.au/product"),
|
||||||
|
("https://example.com", None, "https://example.com"),
|
||||||
|
("1 https://test.com", "1", "https://test.com")
|
||||||
|
]
|
||||||
|
|
||||||
|
for link, expected_qty, expected_url in test_cases:
|
||||||
|
match = re.match(r'^(\d+)?\s*(http.*)$', link)
|
||||||
|
if match:
|
||||||
|
quantity = int(match.group(1)) if match.group(1) else 1
|
||||||
|
url = match.group(2)
|
||||||
|
|
||||||
|
if expected_qty:
|
||||||
|
self.assertEqual(quantity, int(expected_qty))
|
||||||
|
else:
|
||||||
|
self.assertEqual(quantity, 1) # Default quantity
|
||||||
|
self.assertEqual(url, expected_url)
|
||||||
|
|
||||||
|
async def test_parse_ingredient_from_nlp_simple_cases(self):
|
||||||
|
"""Test parsing simple ingredient cases that don't require external dependencies"""
|
||||||
|
# Test the basic structure without relying on ingredient_parser
|
||||||
|
# Since ingredient_parser is an external dependency, we'll test what we can
|
||||||
|
|
||||||
|
# We can test that the function exists and handles basic error cases
|
||||||
|
try:
|
||||||
|
result = ingredients.parse_ingredient_from_nlp("2 cups flour")
|
||||||
|
# The function may fail due to missing ingredient_parser, but it should not crash
|
||||||
|
# If it works, result should be an Ingredient object
|
||||||
|
if result is not None:
|
||||||
|
self.assertIsInstance(result, ingredients_db.Ingredient)
|
||||||
|
except ImportError:
|
||||||
|
# If ingredient_parser is not available, that's expected
|
||||||
|
self.skipTest("ingredient_parser not available")
|
||||||
|
except Exception as e:
|
||||||
|
# Other exceptions should not occur in normal operation
|
||||||
|
self.fail(f"Unexpected exception: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
class TestIngredientMatching(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_persons(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
async def test_match_existing_products_with_real_data(self):
|
||||||
|
"""Test matching ingredients to existing products using real operations"""
|
||||||
|
# Setup: Create and insert a product with tags
|
||||||
|
product = test_data.Products.broccoli
|
||||||
|
await products_db.insert_product(self.conn, product, {})
|
||||||
|
await products_db.add_tag(self.conn, product, "broccoli")
|
||||||
|
|
||||||
|
# Create ingredients without products
|
||||||
|
ingredient1 = ingredients_db.Ingredient(
|
||||||
|
name="broccoli",
|
||||||
|
line="1 piece broccoli",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation=""
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredient2 = ingredients_db.Ingredient(
|
||||||
|
name="unknown vegetable",
|
||||||
|
line="1 piece unknown vegetable",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation=""
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredients_list = [ingredient1, ingredient2]
|
||||||
|
result = await ingredients.match_existing_products(self.conn, ingredients_list)
|
||||||
|
|
||||||
|
# Check that first ingredient got matched
|
||||||
|
self.assertEqual(result[0].product_id, product.id)
|
||||||
|
self.assertIsNotNone(result[0].product)
|
||||||
|
self.assertEqual(result[0].product.name, product.name)
|
||||||
|
|
||||||
|
# Check that second ingredient remained unmatched
|
||||||
|
self.assertIsNone(result[1].product)
|
||||||
|
|
||||||
|
async def test_match_existing_products_already_has_product(self):
|
||||||
|
"""Test that ingredients with existing products are not re-matched"""
|
||||||
|
product = test_data.Products.broccoli
|
||||||
|
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="broccoli",
|
||||||
|
line="1 piece broccoli",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product=product,
|
||||||
|
product_id=product.id
|
||||||
|
)
|
||||||
|
|
||||||
|
ingredients_list = [ingredient]
|
||||||
|
result = await ingredients.match_existing_products(self.conn, ingredients_list)
|
||||||
|
|
||||||
|
# Should remain unchanged
|
||||||
|
self.assertEqual(result[0].product_id, product.id)
|
||||||
|
self.assertEqual(result[0].product, product)
|
||||||
|
|
||||||
|
async def test_match_existing_products_empty_list(self):
|
||||||
|
"""Test matching empty ingredients list"""
|
||||||
|
result = await ingredients.match_existing_products(self.conn, [])
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
|
||||||
|
async def test_ingredient_keys_constant(self):
|
||||||
|
"""Test that the KEYS constant contains expected fields"""
|
||||||
|
expected_keys = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id', 'recipe_id', 'meal_id']
|
||||||
|
self.assertEqual(ingredients_db.Ingredient.KEYS, expected_keys)
|
||||||
|
|
||||||
|
async def test_ingredient_default_values(self):
|
||||||
|
"""Test ingredient default values"""
|
||||||
|
ingredient = ingredients_db.Ingredient(
|
||||||
|
name="Test",
|
||||||
|
line="Test line",
|
||||||
|
unit="Items",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation=""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(ingredient.id, -1)
|
||||||
|
self.assertIsNone(ingredient.product_id)
|
||||||
|
self.assertIsNone(ingredient.recipe_id)
|
||||||
|
self.assertIsNone(ingredient.meal_id)
|
||||||
|
self.assertIsNone(ingredient.product)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
1326
tests/test_main.py
1326
tests/test_main.py
File diff suppressed because it is too large
Load diff
512
tests/test_meals.py
Normal file
512
tests/test_meals.py
Normal file
|
|
@ -0,0 +1,512 @@
|
||||||
|
import unittest
|
||||||
|
import asyncio
|
||||||
|
from datetime import datetime
|
||||||
|
import importlib
|
||||||
|
|
||||||
|
import tests.test_data as test_data
|
||||||
|
|
||||||
|
def reload_test_data():
|
||||||
|
global test_data
|
||||||
|
test_data = importlib.reload(test_data)
|
||||||
|
|
||||||
|
from db import connect, create
|
||||||
|
import meals
|
||||||
|
import meals.db as meals_db
|
||||||
|
from meals.db import Meal, MealRecipe
|
||||||
|
import persons
|
||||||
|
import recipes
|
||||||
|
import ingredients
|
||||||
|
import products
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealsModels(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test the meals data models"""
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_persons(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
def test_meal_creation(self):
|
||||||
|
"""Test basic Meal creation"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 1, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie, test_data.Persons.chris]
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(meal.id, -1) # Default ID
|
||||||
|
self.assertEqual(meal.suggested_date, datetime(2024, 1, 1, 18, 0))
|
||||||
|
self.assertIsNone(meal.consumed_date)
|
||||||
|
self.assertEqual(len(meal.chefs), 1)
|
||||||
|
self.assertEqual(len(meal.cleanup), 1)
|
||||||
|
self.assertEqual(len(meal.consumers), 2)
|
||||||
|
self.assertEqual(len(meal.recipes), 0)
|
||||||
|
self.assertEqual(len(meal.extra_ingredients), 0)
|
||||||
|
|
||||||
|
def test_meal_recipe_creation(self):
|
||||||
|
"""Test basic MealRecipe creation"""
|
||||||
|
meal_recipe = MealRecipe(
|
||||||
|
meal_id=1,
|
||||||
|
recipe_id=2,
|
||||||
|
servings=4.0
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(meal_recipe.meal_id, 1)
|
||||||
|
self.assertEqual(meal_recipe.recipe_id, 2)
|
||||||
|
self.assertEqual(meal_recipe.servings, 4.0)
|
||||||
|
self.assertIsNone(meal_recipe.recipe)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealsCRUD(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test meals CRUD operations"""
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_test_data(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_insert_meal_basic(self):
|
||||||
|
"""Test inserting a basic meal with participants"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 1, 15, 19, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Verify meal was inserted and got an ID
|
||||||
|
self.assertGreater(meal.id, 0)
|
||||||
|
|
||||||
|
# Verify we can find it by ID
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(found_meal.suggested_date, meal.suggested_date)
|
||||||
|
self.assertEqual(len(found_meal.chefs), 1)
|
||||||
|
self.assertEqual(found_meal.chefs[0].name, "Jacob")
|
||||||
|
self.assertEqual(len(found_meal.cleanup), 1)
|
||||||
|
self.assertEqual(found_meal.cleanup[0].name, "Ryan")
|
||||||
|
self.assertEqual(len(found_meal.consumers), 1)
|
||||||
|
self.assertEqual(found_meal.consumers[0].name, "Ellie")
|
||||||
|
|
||||||
|
async def test_insert_meal_with_recipes(self):
|
||||||
|
"""Test inserting a meal with recipes"""
|
||||||
|
# First create a recipe
|
||||||
|
recipe = test_data.Recipes.broccoli_soup
|
||||||
|
recipe.id = -1 # Reset ID
|
||||||
|
await recipes.insert_recipe(self.conn, recipe)
|
||||||
|
|
||||||
|
meal_recipe = MealRecipe(
|
||||||
|
meal_id=-1,
|
||||||
|
recipe_id=recipe.id,
|
||||||
|
servings=3.0,
|
||||||
|
recipe=recipe
|
||||||
|
)
|
||||||
|
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 2, 1, 18, 30),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie, test_data.Persons.chris],
|
||||||
|
recipes=[meal_recipe]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Verify meal was inserted
|
||||||
|
self.assertGreater(meal.id, 0)
|
||||||
|
|
||||||
|
# Verify recipe was associated
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(len(found_meal.recipes), 1)
|
||||||
|
self.assertEqual(found_meal.recipes[0].recipe_id, recipe.id)
|
||||||
|
self.assertEqual(found_meal.recipes[0].servings, 3.0)
|
||||||
|
self.assertIsNotNone(found_meal.recipes[0].recipe)
|
||||||
|
self.assertEqual(found_meal.recipes[0].recipe.name, recipe.name)
|
||||||
|
|
||||||
|
async def test_insert_meal_with_extra_ingredients(self):
|
||||||
|
"""Test inserting a meal with extra ingredients"""
|
||||||
|
# Create a new product for testing
|
||||||
|
product = products.Product(
|
||||||
|
id=-1,
|
||||||
|
shop_code='woolworths',
|
||||||
|
name="Test Garlic Bread",
|
||||||
|
product_id="test_294517",
|
||||||
|
quantity=1,
|
||||||
|
unit="Loaf",
|
||||||
|
link="https://example.com/test-garlic-bread",
|
||||||
|
img_small="https://example.com/test-small.jpg",
|
||||||
|
img_large="https://example.com/test-large.jpg",
|
||||||
|
raw_data={},
|
||||||
|
)
|
||||||
|
await products.insert_product(self.conn, product, {})
|
||||||
|
|
||||||
|
# Create an ingredient
|
||||||
|
extra_ingredient = ingredients.Ingredient(
|
||||||
|
id=-1,
|
||||||
|
name="Test Garlic Bread",
|
||||||
|
line="1 loaf test garlic bread",
|
||||||
|
unit="loaf",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product_id=product.id
|
||||||
|
)
|
||||||
|
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 3, 1, 19, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie],
|
||||||
|
extra_ingredients=[extra_ingredient]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Verify meal was inserted
|
||||||
|
self.assertGreater(meal.id, 0)
|
||||||
|
|
||||||
|
# Verify extra ingredients were associated
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(len(found_meal.extra_ingredients), 1)
|
||||||
|
self.assertEqual(found_meal.extra_ingredients[0].name, "Test Garlic Bread")
|
||||||
|
|
||||||
|
async def test_find_meal_by_id_not_found(self):
|
||||||
|
"""Test finding a meal that doesn't exist"""
|
||||||
|
result = await meals_db.find_meal_by_id(self.conn, 999)
|
||||||
|
self.assertIsNone(result)
|
||||||
|
|
||||||
|
async def test_update_meal(self):
|
||||||
|
"""Test updating a meal"""
|
||||||
|
# Create and insert initial meal
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 4, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
original_id = meal.id
|
||||||
|
|
||||||
|
# Update the meal
|
||||||
|
meal.suggested_date = datetime(2024, 4, 2, 19, 0)
|
||||||
|
meal.chefs = [test_data.Persons.ryan] # Change chef
|
||||||
|
meal.cleanup = [test_data.Persons.ellie] # Change cleanup
|
||||||
|
meal.consumers = [test_data.Persons.jacob, test_data.Persons.chris] # Change consumers
|
||||||
|
|
||||||
|
await meals_db.update_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Verify updates
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, original_id)
|
||||||
|
self.assertEqual(found_meal.suggested_date, datetime(2024, 4, 2, 19, 0))
|
||||||
|
self.assertEqual(len(found_meal.chefs), 1)
|
||||||
|
self.assertEqual(found_meal.chefs[0].name, "Ryan")
|
||||||
|
self.assertEqual(len(found_meal.cleanup), 1)
|
||||||
|
self.assertEqual(found_meal.cleanup[0].name, "Ellie")
|
||||||
|
self.assertEqual(len(found_meal.consumers), 2)
|
||||||
|
consumer_names = {p.name for p in found_meal.consumers}
|
||||||
|
self.assertIn("Jacob", consumer_names)
|
||||||
|
self.assertIn("Chris", consumer_names)
|
||||||
|
|
||||||
|
async def test_mark_consumed(self):
|
||||||
|
"""Test marking a meal as consumed"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 5, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Mark as consumed
|
||||||
|
consumed_date = datetime(2024, 5, 1, 19, 30)
|
||||||
|
await meals_db.mark_consumed(self.conn, meal, consumed_date)
|
||||||
|
|
||||||
|
# Verify consumed date was set
|
||||||
|
self.assertEqual(meal.consumed_date, consumed_date)
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(found_meal.consumed_date, consumed_date)
|
||||||
|
|
||||||
|
async def test_mark_purchased(self):
|
||||||
|
"""Test marking a meal as purchased"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 6, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Mark as purchased
|
||||||
|
updated_meal = await meals_db.mark_purchased(self.conn, meal)
|
||||||
|
|
||||||
|
# Verify purchase date was set
|
||||||
|
self.assertIsNotNone(updated_meal.purchase_date)
|
||||||
|
self.assertIsNotNone(meal.purchase_date)
|
||||||
|
|
||||||
|
# Verify in database
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertIsNotNone(found_meal.purchase_date)
|
||||||
|
|
||||||
|
async def test_delete_meal(self):
|
||||||
|
"""Test soft deleting a meal"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 7, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
meal_id = meal.id
|
||||||
|
|
||||||
|
# Verify meal exists and is in upcoming meals before deletion
|
||||||
|
start_date = datetime(2024, 7, 1)
|
||||||
|
end_date = datetime(2024, 7, 31)
|
||||||
|
upcoming_meals_before = []
|
||||||
|
async for m in meals_db.find_upcoming_meals_by_date_range(self.conn, start_date, end_date):
|
||||||
|
if m.id == meal_id:
|
||||||
|
upcoming_meals_before.append(m)
|
||||||
|
self.assertEqual(len(upcoming_meals_before), 1)
|
||||||
|
|
||||||
|
# Delete the meal
|
||||||
|
await meals_db.delete_meal(self.conn, meal_id)
|
||||||
|
|
||||||
|
# Verify meal no longer appears in upcoming meals (soft deleted)
|
||||||
|
upcoming_meals_after = []
|
||||||
|
async for m in meals_db.find_upcoming_meals_by_date_range(self.conn, start_date, end_date):
|
||||||
|
if m.id == meal_id:
|
||||||
|
upcoming_meals_after.append(m)
|
||||||
|
self.assertEqual(len(upcoming_meals_after), 0)
|
||||||
|
|
||||||
|
async def test_find_upcoming_meals_by_date_range(self):
|
||||||
|
"""Test finding upcoming meals within a date range"""
|
||||||
|
# Create several meals with different dates
|
||||||
|
meal1 = Meal(
|
||||||
|
suggested_date=datetime(2024, 8, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
meal2 = Meal(
|
||||||
|
suggested_date=datetime(2024, 8, 15, 18, 0),
|
||||||
|
chefs=[test_data.Persons.ryan],
|
||||||
|
cleanup=[test_data.Persons.jacob],
|
||||||
|
consumers=[test_data.Persons.chris]
|
||||||
|
)
|
||||||
|
|
||||||
|
meal3 = Meal(
|
||||||
|
suggested_date=datetime(2024, 9, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.ellie],
|
||||||
|
cleanup=[test_data.Persons.chris],
|
||||||
|
consumers=[test_data.Persons.jacob]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a consumed meal (should not appear in upcoming)
|
||||||
|
consumed_meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 8, 10, 18, 0),
|
||||||
|
consumed_date=datetime(2024, 8, 10, 19, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal1)
|
||||||
|
await meals_db.insert_meal(self.conn, meal2)
|
||||||
|
await meals_db.insert_meal(self.conn, meal3)
|
||||||
|
await meals_db.insert_meal(self.conn, consumed_meal)
|
||||||
|
|
||||||
|
# Mark consumed meal as consumed in DB
|
||||||
|
await meals_db.mark_consumed(self.conn, consumed_meal, consumed_meal.consumed_date)
|
||||||
|
|
||||||
|
# Find meals in August 2024
|
||||||
|
start_date = datetime(2024, 8, 1)
|
||||||
|
end_date = datetime(2024, 8, 31)
|
||||||
|
|
||||||
|
upcoming_meals = []
|
||||||
|
async for meal in meals_db.find_upcoming_meals_by_date_range(self.conn, start_date, end_date):
|
||||||
|
upcoming_meals.append(meal)
|
||||||
|
|
||||||
|
# Should find meal1 and meal2, but not meal3 (outside range) or consumed_meal (consumed)
|
||||||
|
self.assertEqual(len(upcoming_meals), 2)
|
||||||
|
meal_dates = [meal.suggested_date for meal in upcoming_meals]
|
||||||
|
self.assertIn(datetime(2024, 8, 1, 18, 0), meal_dates)
|
||||||
|
self.assertIn(datetime(2024, 8, 15, 18, 0), meal_dates)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealParticipants(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test meal participant management"""
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_test_data(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_sync_meal_participants(self):
|
||||||
|
"""Test syncing meal participants"""
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 10, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Update participants
|
||||||
|
new_chefs = [test_data.Persons.ryan, test_data.Persons.ellie]
|
||||||
|
await meals_db.sync_meal_participants(self.conn, meal.id, new_chefs, 'chef')
|
||||||
|
|
||||||
|
# Verify participants were updated
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(len(found_meal.chefs), 2)
|
||||||
|
chef_names = {chef.name for chef in found_meal.chefs}
|
||||||
|
self.assertIn("Ryan", chef_names)
|
||||||
|
self.assertIn("Ellie", chef_names)
|
||||||
|
self.assertNotIn("Jacob", chef_names)
|
||||||
|
|
||||||
|
# Cleanup and consumers should remain unchanged
|
||||||
|
self.assertEqual(len(found_meal.cleanup), 1)
|
||||||
|
self.assertEqual(found_meal.cleanup[0].name, "Ryan")
|
||||||
|
self.assertEqual(len(found_meal.consumers), 1)
|
||||||
|
self.assertEqual(found_meal.consumers[0].name, "Ellie")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealRecipes(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test meal recipe management"""
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_test_data(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_insert_meal_recipe_validation(self):
|
||||||
|
"""Test meal recipe validation during insertion"""
|
||||||
|
# Try to insert meal recipe without valid meal_id
|
||||||
|
meal_recipe = MealRecipe(meal_id=-1, recipe_id=1, servings=2.0)
|
||||||
|
|
||||||
|
with self.assertRaises(ValueError) as context:
|
||||||
|
await meals_db.insert_meal_recipe(self.conn, meal_recipe)
|
||||||
|
self.assertIn("Meal must be inserted", str(context.exception))
|
||||||
|
|
||||||
|
async def test_sync_meal_recipes(self):
|
||||||
|
"""Test syncing meal recipes"""
|
||||||
|
# Create a recipe first
|
||||||
|
recipe = test_data.Recipes.broccoli_soup
|
||||||
|
recipe.id = -1 # Reset ID
|
||||||
|
await recipes.insert_recipe(self.conn, recipe)
|
||||||
|
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 11, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Add recipes to meal
|
||||||
|
meal_recipes = [
|
||||||
|
MealRecipe(meal_id=meal.id, recipe_id=recipe.id, servings=4.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
await meals_db.sync_meal_recipes(self.conn, meal.id, meal_recipes)
|
||||||
|
|
||||||
|
# Verify recipes were added
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(len(found_meal.recipes), 1)
|
||||||
|
self.assertEqual(found_meal.recipes[0].servings, 4.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMealIngredients(unittest.IsolatedAsyncioTestCase):
|
||||||
|
"""Test meal extra ingredients management"""
|
||||||
|
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.conn = await connect(':memory:')
|
||||||
|
await create(self.conn)
|
||||||
|
await test_data.create_test_data(self.conn)
|
||||||
|
reload_test_data()
|
||||||
|
return await super().asyncSetUp()
|
||||||
|
|
||||||
|
async def asyncTearDown(self) -> None:
|
||||||
|
await self.conn.close()
|
||||||
|
return await super().asyncTearDown()
|
||||||
|
|
||||||
|
async def test_sync_extra_ingredients(self):
|
||||||
|
"""Test syncing extra ingredients"""
|
||||||
|
# Create a new product for testing
|
||||||
|
product = products.Product(
|
||||||
|
id=-1,
|
||||||
|
shop_code='woolworths',
|
||||||
|
name="Test Bread Roll",
|
||||||
|
product_id="test_bread_123",
|
||||||
|
quantity=1,
|
||||||
|
unit="Roll",
|
||||||
|
link="https://example.com/test-bread-roll",
|
||||||
|
img_small="https://example.com/test-small.jpg",
|
||||||
|
img_large="https://example.com/test-large.jpg",
|
||||||
|
raw_data={},
|
||||||
|
)
|
||||||
|
await products.insert_product(self.conn, product, {})
|
||||||
|
|
||||||
|
meal = Meal(
|
||||||
|
suggested_date=datetime(2024, 12, 1, 18, 0),
|
||||||
|
chefs=[test_data.Persons.jacob],
|
||||||
|
cleanup=[test_data.Persons.ryan],
|
||||||
|
consumers=[test_data.Persons.ellie]
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.insert_meal(self.conn, meal)
|
||||||
|
|
||||||
|
# Add extra ingredients
|
||||||
|
extra_ingredient = ingredients.Ingredient(
|
||||||
|
id=-1,
|
||||||
|
name="Test Bread Roll",
|
||||||
|
line="1 roll test bread",
|
||||||
|
unit="roll",
|
||||||
|
quantity=1.0,
|
||||||
|
preparation="",
|
||||||
|
product_id=product.id
|
||||||
|
)
|
||||||
|
|
||||||
|
await meals_db.sync_extra_ingredients(self.conn, meal.id, [extra_ingredient])
|
||||||
|
|
||||||
|
# Verify ingredients were added
|
||||||
|
found_meal = await meals_db.find_meal_by_id(self.conn, meal.id)
|
||||||
|
self.assertEqual(len(found_meal.extra_ingredients), 1)
|
||||||
|
self.assertEqual(found_meal.extra_ingredients[0].name, "Test Bread Roll")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue