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
453 lines
16 KiB
Python
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)
|