Compare commits

..

2 commits

Author SHA1 Message Date
255ebd4613 Squashed commit of the following:
commit 1c707260a4059cbdebb3e2720d3899b8773a1c37
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 22:03:48 2025 +1000

    Sync tests

commit 5a71077121921a2cea9f3be9c1d1f7770691520a
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 21:57:00 2025 +1000

    minor refactorings

commit 0bafec1b40bf5cf6f1554f6e5ec6e8f4e043d1cc
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 21:39:52 2025 +1000

    Remove ids from shopping list

commit c1ee30d1b591b241dfa152d8a0fa9af1f6aaaed1
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 21:04:48 2025 +1000

    Added more cases to shopping tests

commit ad3d598bdb574b1aeaabc354bb825ee428aaaa41
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 20:26:36 2025 +1000

    Added 404 response when list not found

commit 336538dc47e12736f7f57d2c80252a700dc1e83b
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 20:15:52 2025 +1000

    Tests for main

commit 238c0f89b2671348091a162ee519e5f1bf2cd66c
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 20:07:12 2025 +1000

    Meals tests

commit b92f2432ee13f5384b6f1a9b8efe76bec9dbd920
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 19:57:45 2025 +1000

    Shopping tests

commit b3c7ae20e6207cf0f9f04bdec5cb6145b04638d9
Author: jableader <jacobdunk@gmail.com>
Date:   Wed Jul 30 19:40:19 2025 +1000

    product tests passing
2025-07-30 22:05:29 +10:00
2a66ff39be Vibe coded some tests -_- 2025-07-30 18:55:51 +10:00
9 changed files with 3473 additions and 461 deletions

View file

@ -53,7 +53,7 @@ async def find_ingredient_by_id(conn, ingredient_id: int) -> Optional[Ingredient
async with conn.execute(f'''
SELECT {','.join(ingredient_keys + product_keys)} FROM Ingredient
LEFT JOIN Product ON Ingredient.product_id = Product.id
WHERE id = ?
WHERE Ingredient.id = ?
''', (ingredient_id,)) as cursor:
async for row in cursor:
product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):])}

16
main.py
View file

@ -263,14 +263,15 @@ class CurrentShoppingList(BaseModel):
@app.get("/api/shopping/current")
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}
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
items = meal_requests + outstanding_requests + purchased_requests + [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)
# Add any additional items from shopping lists to the existing lookups
additional_items = [item for sl in shopping_list_lookup.values() for item in sl.items]
if additional_items:
await shopping.to_lookups(conn, additional_items, meals_lookup, recipes_lookup, ingredients_lookup)
return CurrentShoppingList(
outstanding_items=outstanding_requests,
@ -291,6 +292,9 @@ class PurchasedShoppingList(BaseModel):
@app.get("/api/shopping/{list_id}")
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)
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)
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")
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")
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:
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_add = [req for req in requests if not any(isMatching(req, r) for r in my_shopping_list)]

View file

@ -10,26 +10,10 @@ async def to_lookups(conn, items: List[ShoppingListItem], meals_lookup: Dict[int
recipes_lookup = recipes_lookup or {}
ingredients_lookup = ingredients_lookup or {}
lookups = (meals_lookup, recipes_lookup, ingredients_lookup)
_move_refs_to_lookups(items, *lookups)
await _ensure_lookups_populated(conn, items, *lookups)
return lookups
await _ensure_lookups_populated(conn, items, meals_lookup, recipes_lookup, ingredients_lookup)
return meals_lookup, recipes_lookup, ingredients_lookup
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):
for item in items:
# 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:
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 for item in _find_items_by_list_id(conn, None):
if item.person_id == person_id and item.ingredient_id is not None:
yield item
async def get_persons_requests(conn, person_id: int) -> List[ingredients.Ingredient]:
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]
return [await ingredients.find_ingredient_by_id(conn, ingredient_id) for ingredient_id in ids]
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:
if item.meal:
for mealRecipe in item.meal.recipes:
if item.meal_id and item.meal_id in meals_lookup:
meal = meals_lookup[item.meal_id]
for mealRecipe in meal.recipes:
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:
yield ShoppingListItem(ingredient=ingredient, meal=item.meal, person_id=item.person_id, created_date=item.created_date)
for ingredient in meal.extra_ingredients:
yield ShoppingListItem(
ingredient_id=ingredient.id,
meal_id=item.meal_id,
person_id=item.person_id,
created_date=item.created_date
)
else:
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)]
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]
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()))}
meal_requests = [r for r in current_requests if r.meal_id is not None and r.meal_id > 0]
# 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 = []
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:
# Meal ingredients may have already been purchased
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)
return outstanding_items, purchased_items, meal_requests
return outstanding_items, purchased_items, meal_requests, meals_lookup, recipes_lookup, ingredients_lookup

View file

@ -15,16 +15,12 @@ class ShoppingListItem(BaseLinkedModel):
list_id: Optional[int] = None
person_id: int = -1
person: Optional[Person] = None
ingredient_id: Optional[int] = None
ingredient: Optional[Ingredient] = None
recipe_id: Optional[int] = None
recipe: Optional[Recipe] = None
meal_id: Optional[int] = None
meal: Optional[Meal] = None
created_date: datetime = datetime.now().astimezone()
@ -76,7 +72,7 @@ def validate_request(request: ShoppingListItem) -> None:
raise ValueError('Requests must have a person')
# 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')
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
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:
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:
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)
@ -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')
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]
person_keys = [f'person.{key}' for key in Person.KEYS]
select = f'''
SELECT {','.join(ingredient_keys + product_keys + request_keys + person_keys)}
SELECT {','.join(request_keys)}
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', ()
@ -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)
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)])}
product = Product(**product_keys) if product_keys['id'] else None
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)
request_keys = {k:v for k,v in zip(ShoppingListItem.KEYS, row)}
request = ShoppingListItem(**request_keys)
yield request
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'''
SELECT {','.join(ShoppingListItem.KEYS)}
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:
async for row in cursor:
yield ShoppingListItem(**{k:v for k,v in zip(ShoppingListItem.KEYS, row)})

View file

@ -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
View 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()

File diff suppressed because it is too large Load diff

512
tests/test_meals.py Normal file
View 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