munch-ease-backend/tests/test_shopping.py
jableader 5be4e89c4e Squashed commit of the following:
commit fcd005b8624023547f28b7b28e59e6099bcfc7d4
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 20:24:07 2025 +1100

    Openapi tightening

commit f93bd8f641d561052c7bd075bae321b4ff3b676d
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 19:03:52 2025 +1100

    Removed refactor strategy doc

commit 0c5a61092f522be0c47cbbe86917c8a7e4e2d339
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 17:48:33 2025 +1100

    mypy & ruff checks

commit 23d66d6b18984127e17c73c3063f6120385935e9
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 16:49:35 2025 +1100

    Final removal of db.py files

commit f454aed1ca9783cc558cc203f29a7fe31b62a975
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:42:31 2025 +1100

    Finalise restructure, remove db.py files

commit 7187f6dd89489521538791c6bdebb426514beb99
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:34:54 2025 +1100

commit 6fea227ae20d32b8eb1e7a4885006a620fcc7bb1
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:32:53 2025 +1100

commit 27415e7e02d89195ad514cb017a9dbbf84d7a5e4
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:31:10 2025 +1100

commit b773428033d855f9ad82005602e049c1a2e3c585
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:28:58 2025 +1100

commit 116592c95278d995f4c516e87f2cea43cf5b7735
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:25:21 2025 +1100

commit 03ec565faea088971968ee2f9bb83e2de16b21f3
Author: jableader <jacobdunk@gmail.com>
Date:   Sun Oct 19 15:21:29 2025 +1100

    Plan
2025-10-19 20:24:23 +11:00

453 lines
16 KiB
Python

import importlib
import unittest
from datetime import datetime
import tests.test_data as test_data
from db import connect, create
import shopping
from shopping.models import ShoppingList, ShoppingListItem, StoreEnum
import ingredients.repository as ingredients_repo
import ingredients
import meals
import recipes
def reload_test_data():
global test_data
test_data = importlib.reload(test_data)
class TestShopping(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()
async def asyncTearDown(self) -> None:
await self.conn.close()
def test_validate_request(self):
ing = ingredients_repo.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped",
)
ok_item = ShoppingListItem(ingredient_id=ing.id, person_id=1)
shopping.validate_request(ok_item)
bad_person = ShoppingListItem(ingredient_id=ing.id, person_id=-1)
with self.assertRaises(ValueError):
shopping.validate_request(bad_person)
missing_both = ShoppingListItem(person_id=1)
with self.assertRaises(ValueError):
shopping.validate_request(missing_both)
async def test_request_and_purchase_ingredient(self):
ing = ingredients_repo.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped",
)
await ingredients_repo.insert_ingredient(self.conn, ing)
person = test_data.Persons.jacob
req_item = await shopping.request(self.conn, person, ingredient=ing)
self.assertIsNotNone(req_item.id)
shop_item = ShoppingListItem(ingredient=ing, person_id=person.id)
s_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=person.id,
items=[shop_item],
)
await shopping.purchase(self.conn, s_list)
self.assertIsNotNone(s_list.id)
loaded = await shopping.load_shopping_list(self.conn, s_list.id)
self.assertIsNotNone(loaded)
self.assertEqual(loaded.id, s_list.id)
async def test_to_lookups_and_is_requested(self):
# Insert ingredient, recipe, meal
ing = ingredients_repo.Ingredient(
name="Carrot",
line="1 carrot",
unit="Items",
quantity=1.0,
preparation="",
)
await ingredients_repo.insert_ingredient(self.conn, ing)
recipe = recipes.Recipe(
id=1,
name="Test Recipe",
link="http://example.com",
serves=4,
created_by_id=1,
)
await recipes.insert_recipe(self.conn, recipe)
meal = meals.Meal(id=1, suggested_date=datetime.now())
await meals.insert_meal(self.conn, meal)
items = [ShoppingListItem(ingredient_id=ing.id, meal_id=meal.id, recipe_id=recipe.id)]
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(
self.conn, items
)
self.assertIn(meal.id, meals_lookup)
self.assertIn(recipe.id, recipes_lookup)
self.assertIn(ing.id, ingredients_lookup)
person = test_data.Persons.jacob
self.assertFalse(await shopping.is_requested(self.conn, meal))
await shopping.request(self.conn, person, meal=meal)
self.assertTrue(await shopping.is_requested(self.conn, meal))
async def test_complete_meal_purchase_unrequests_and_marks_purchased(self):
# Create a simple recipe with 2 ingredients
recipe = recipes.Recipe(
id=-1,
name="Simple Recipe",
link="http://example.com/simple",
serves=2,
created_by_id=test_data.Persons.jacob.id,
)
await recipes.insert_recipe(self.conn, recipe)
# Create ingredients for the recipe
ingredient1 = ingredients.Ingredient(
name="Bread",
line="2 slices bread",
unit="slices",
quantity=2.0,
preparation="",
recipe_id=recipe.id,
)
ingredient2 = ingredients.Ingredient(
name="Butter",
line="10g butter",
unit="g",
quantity=10.0,
preparation="",
recipe_id=recipe.id,
)
await ingredients.insert_ingredient(self.conn, ingredient1)
await ingredients.insert_ingredient(self.conn, ingredient2)
# Load the recipe with its ingredients
await recipes.load_recipe_ingredients(self.conn, recipe)
# Create and insert a meal
meal_recipe = meals.MealRecipe(meal_id=-1, recipe_id=recipe.id, servings=1.0, recipe=recipe)
meal = meals.Meal(
id=-1,
suggested_date=datetime.now(),
chefs=[test_data.Persons.jacob],
cleanup=[test_data.Persons.ryan],
consumers=[test_data.Persons.ellie],
recipes=[meal_recipe],
)
await meals.insert_meal(self.conn, meal)
# Request the meal
person = test_data.Persons.jacob
await shopping.request(self.conn, person, meal=meal)
self.assertTrue(await shopping.is_requested(self.conn, meal))
# Get initial outstanding requests
(
outstanding_before,
purchased_before,
meal_requests_before,
meals_lookup,
recipes_lookup,
ingredients_lookup,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_before), 2)
self.assertEqual(len(meal_requests_before), 1)
# Purchase all ingredients from the meal
shopping_items = [
ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id,
meal_id=meal.id,
recipe_id=item.recipe_id,
)
for item in outstanding_before
]
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths, purchased_by_id=person.id, items=shopping_items
)
await shopping.purchase(self.conn, shopping_list)
# Verify meal is no longer requested and is marked as purchased
self.assertFalse(await shopping.is_requested(self.conn, meal))
found_meal_after = await meals.find_meal_by_id(self.conn, meal.id)
self.assertIsNotNone(found_meal_after.purchase_date)
# After complete purchase, there should be no outstanding/purchased items for that meal
(
outstanding_after,
purchased_after,
meal_requests_after,
_,
_,
_,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_after), 0)
self.assertEqual(len(purchased_after), 0)
self.assertEqual(len(meal_requests_after), 0)
async def test_complete_meal_with_extra_ingredients_purchase(self):
# Create a recipe with 1 ingredient
recipe = recipes.Recipe(
id=-1,
name="Recipe with Extra",
link="http://example.com/extra",
serves=2,
created_by_id=test_data.Persons.jacob.id,
)
await recipes.insert_recipe(self.conn, recipe)
# Recipe ingredient
recipe_ingredient = ingredients.Ingredient(
name="Main Ingredient",
line="200g main ingredient",
unit="g",
quantity=200.0,
preparation="",
recipe_id=recipe.id,
)
await ingredients.insert_ingredient(self.conn, recipe_ingredient)
await recipes.load_recipe_ingredients(self.conn, recipe)
# Extra ingredient (not part of recipe)
extra_ingredient = ingredients.Ingredient(
name="Side Dish", line="1 side dish", unit="item", quantity=1.0, preparation=""
)
meal_recipe = meals.MealRecipe(meal_id=-1, recipe_id=recipe.id, servings=1.0, recipe=recipe)
meal = meals.Meal(
id=-1,
suggested_date=datetime.now(),
chefs=[test_data.Persons.jacob],
cleanup=[test_data.Persons.ryan],
consumers=[test_data.Persons.ellie],
recipes=[meal_recipe],
extra_ingredients=[extra_ingredient],
)
await meals.insert_meal(self.conn, meal)
person = test_data.Persons.jacob
await shopping.request(self.conn, person, meal=meal)
(
outstanding_before,
_,
_,
_,
_,
_,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_before), 2) # recipe + extra
shopping_items = [
ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id,
meal_id=meal.id,
recipe_id=item.recipe_id,
)
for item in outstanding_before
]
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths, purchased_by_id=person.id, items=shopping_items
)
await shopping.purchase(self.conn, shopping_list)
self.assertFalse(await shopping.is_requested(self.conn, meal))
found_meal_after = await meals.find_meal_by_id(self.conn, meal.id)
self.assertIsNotNone(found_meal_after.purchase_date)
(
outstanding_after,
purchased_after,
meal_requests_after,
_,
_,
_,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_after), 0)
self.assertEqual(len(purchased_after), 0)
self.assertEqual(len(meal_requests_after), 0)
async def test_individual_ingredient_purchase_without_meal(self):
# Create individual ingredients
ingredient1 = ingredients.Ingredient(
name="Milk", line="1L milk", unit="L", quantity=1.0, preparation=""
)
ingredient2 = ingredients.Ingredient(
name="Eggs", line="12 eggs", unit="dozen", quantity=1.0, preparation=""
)
await ingredients.insert_ingredient(self.conn, ingredient1)
await ingredients.insert_ingredient(self.conn, ingredient2)
# Request individual ingredients
person = test_data.Persons.jacob
await shopping.request(self.conn, person, ingredient=ingredient1)
await shopping.request(self.conn, person, ingredient=ingredient2)
(
outstanding_before,
purchased_before,
meal_requests_before,
meals_lookup,
recipes_lookup,
ingredients_lookup,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_before), 2)
self.assertEqual(len(purchased_before), 0)
self.assertEqual(len(meal_requests_before), 0)
# Purchase only Milk
milk_item = next(
ShoppingListItem(ingredient_id=i.ingredient_id, person_id=person.id)
for i in outstanding_before
if ingredients_lookup.get(i.ingredient_id).name == "Milk"
)
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths, purchased_by_id=person.id, items=[milk_item]
)
await shopping.purchase(self.conn, shopping_list)
(
outstanding_after,
purchased_after,
meal_requests_after,
meals_lookup_after,
recipes_lookup_after,
ingredients_lookup_after,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_after), 1)
self.assertEqual(len(purchased_after), 0)
self.assertEqual(len(meal_requests_after), 0)
self.assertEqual(ingredients_lookup_after[outstanding_after[0].ingredient_id].name, "Eggs")
self.assertIsNone(outstanding_after[0].meal_id)
# Purchase remaining Eggs
eggs_item = ShoppingListItem(
ingredient_id=outstanding_after[0].ingredient_id, person_id=person.id
)
shopping_list2 = ShoppingList(
store_name=StoreEnum.coles, purchased_by_id=person.id, items=[eggs_item]
)
await shopping.purchase(self.conn, shopping_list2)
(
outstanding_final,
purchased_final,
meal_requests_final,
_,
_,
_,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_final), 0)
self.assertEqual(len(purchased_final), 0)
self.assertEqual(len(meal_requests_final), 0)
async def test_mixed_meal_and_individual_requests(self):
# Create recipe and meal
recipe = recipes.Recipe(
id=-1,
name="Simple Pasta",
link="http://example.com/pasta",
serves=2,
created_by_id=test_data.Persons.jacob.id,
)
await recipes.insert_recipe(self.conn, recipe)
pasta_ingredient = ingredients.Ingredient(
name="Pasta",
line="200g pasta",
unit="g",
quantity=200.0,
preparation="",
recipe_id=recipe.id,
)
await ingredients.insert_ingredient(self.conn, pasta_ingredient)
await recipes.load_recipe_ingredients(self.conn, recipe)
meal_recipe = meals.MealRecipe(meal_id=-1, recipe_id=recipe.id, servings=1.0, recipe=recipe)
meal = meals.Meal(
id=-1,
suggested_date=datetime.now(),
chefs=[test_data.Persons.jacob],
cleanup=[test_data.Persons.ryan],
consumers=[test_data.Persons.ellie],
recipes=[meal_recipe],
)
await meals.insert_meal(self.conn, meal)
snack_ingredient = ingredients.Ingredient(
name="Chips", line="1 bag chips", unit="bag", quantity=1.0, preparation=""
)
await ingredients.insert_ingredient(self.conn, snack_ingredient)
person = test_data.Persons.jacob
await shopping.request(self.conn, person, meal=meal)
await shopping.request(self.conn, person, ingredient=snack_ingredient)
(
outstanding_before,
purchased_before,
meal_requests_before,
meals_lookup,
recipes_lookup,
ingredients_lookup,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_before), 2)
self.assertEqual(len(purchased_before), 0)
self.assertEqual(len(meal_requests_before), 1)
# Identify items
pasta_item = next(
i for i in outstanding_before if ingredients_lookup.get(i.ingredient_id).name == "Pasta"
)
chips_item = next(
i for i in outstanding_before if ingredients_lookup.get(i.ingredient_id).name == "Chips"
)
self.assertIsNotNone(pasta_item)
self.assertIsNotNone(chips_item)
self.assertEqual(pasta_item.meal_id, meal.id)
self.assertIsNone(chips_item.meal_id)
# Purchase only Chips
chips_shopping_item = ShoppingListItem(
ingredient_id=chips_item.ingredient_id, person_id=person.id
)
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths, purchased_by_id=person.id, items=[chips_shopping_item]
)
await shopping.purchase(self.conn, shopping_list)
(
outstanding_after,
purchased_after,
meal_requests_after,
meals_lookup_after,
recipes_lookup_after,
ingredients_lookup_after,
) = await shopping.get_outstanding_requests(self.conn)
self.assertEqual(len(outstanding_after), 1)
self.assertEqual(len(purchased_after), 0)
self.assertEqual(len(meal_requests_after), 1)
self.assertEqual(ingredients_lookup_after[outstanding_after[0].ingredient_id].name, "Pasta")
self.assertEqual(outstanding_after[0].meal_id, meal.id)