munch-ease-backend/tests/test_shopping.py

1250 lines
46 KiB
Python
Raw Normal View History

2024-05-17 09:09:03 +00:00
import unittest
import asyncio
from datetime import datetime
from unittest.mock import AsyncMock, Mock, patch
2024-05-17 09:09:03 +00:00
import tests.test_data as test_data
import importlib
2024-05-17 09:09:03 +00:00
def reload_test_data():
global test_data
test_data = importlib.reload(test_data)
from db import connect, create
import shopping
import shopping.db as shopping_db
from shopping.db import ShoppingList, ShoppingListItem, StoreEnum
import ingredients.db as ingredients_db
import products.db as products_db
import persons
import meals
from meals.db import MealRecipe
import recipes
2024-05-17 09:09:03 +00:00
class TestShoppingModels(unittest.IsolatedAsyncioTestCase):
"""Test the shopping data models"""
2024-05-17 09:09:03 +00:00
async def asyncSetUp(self):
self.conn = await connect(':memory:')
await create(self.conn)
await test_data.create_persons(self.conn)
2024-05-17 09:09:03 +00:00
reload_test_data()
return await super().asyncSetUp()
async def asyncTearDown(self) -> None:
await self.conn.close()
return await super().asyncTearDown()
def test_shopping_list_item_creation(self):
"""Test basic ShoppingListItem creation"""
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
item = ShoppingListItem(
id=1,
ingredient_id=ingredient.id,
person_id=1,
created_date=datetime.now()
)
self.assertEqual(item.id, 1)
self.assertEqual(item.ingredient_id, 1)
self.assertEqual(item.person_id, 1)
2024-05-17 09:09:03 +00:00
def test_shopping_list_creation(self):
"""Test basic ShoppingList creation"""
shopping_list = ShoppingList(
id=1,
store_name=StoreEnum.woolworths,
purchased_by_id=1
)
self.assertEqual(shopping_list.id, 1)
self.assertEqual(shopping_list.store_name, StoreEnum.woolworths)
self.assertEqual(shopping_list.purchased_by_id, 1)
self.assertEqual(shopping_list.items, [])
2024-05-20 10:09:57 +00:00
def test_store_enum_values(self):
"""Test StoreEnum values"""
self.assertEqual(StoreEnum.woolworths, 'woolworths')
self.assertEqual(StoreEnum.coles, 'coles')
self.assertEqual(StoreEnum.home, '')
2024-05-20 10:09:57 +00:00
class TestShoppingValidation(unittest.IsolatedAsyncioTestCase):
"""Test shopping validation functions"""
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()
2024-05-20 10:09:57 +00:00
async def asyncTearDown(self) -> None:
await self.conn.close()
return await super().asyncTearDown()
2024-05-20 10:09:57 +00:00
def test_validate_request_valid_ingredient(self):
"""Test validation of valid ingredient request"""
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
item = ShoppingListItem(
ingredient_id=ingredient.id,
person_id=1
)
# Should not raise any exception
shopping_db.validate_request(item)
2024-05-20 10:09:57 +00:00
def test_validate_request_valid_meal(self):
"""Test validation of valid meal request"""
meal = meals.Meal(
id=1,
name="Dinner",
suggested_date=datetime.now()
)
item = ShoppingListItem(
meal_id=meal.id,
person_id=1
)
# Should not raise any exception
shopping_db.validate_request(item)
def test_validate_request_no_person(self):
"""Test validation fails when no person is specified"""
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
item = ShoppingListItem(
ingredient_id=ingredient.id,
person_id=-1 # Invalid person id
)
with self.assertRaises(ValueError) as context:
shopping_db.validate_request(item)
self.assertIn("Requests must have a person", str(context.exception))
2024-05-20 10:09:57 +00:00
def test_validate_request_no_ingredient_or_meal(self):
"""Test validation fails when neither ingredient nor meal is specified"""
item = ShoppingListItem(person_id=1)
with self.assertRaises(ValueError) as context:
shopping_db.validate_request(item)
self.assertIn("Request must have either an ingredient or a meal", str(context.exception))
class TestShoppingRequests(unittest.IsolatedAsyncioTestCase):
"""Test shopping request functionality"""
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_request_ingredient(self):
"""Test requesting an ingredient"""
# Create test ingredient
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
# Request the ingredient
person = test_data.Persons.jacob
item = await shopping_db.request(self.conn, person, ingredient=ingredient)
self.assertIsNotNone(item.id)
self.assertEqual(item.ingredient_id, ingredient.id)
self.assertEqual(item.person_id, person.id)
self.assertIsNone(item.meal_id)
async def test_request_meal(self):
"""Test requesting a meal"""
# Create a mock meal with proper structure
meal = meals.Meal(
id=1,
name="Test Meal",
suggested_date=datetime.now()
)
with patch('shopping.db.is_requested', return_value=False):
person = test_data.Persons.jacob
item = await shopping_db.request(self.conn, person, meal=meal)
self.assertIsNotNone(item.id)
self.assertEqual(item.meal_id, meal.id)
self.assertEqual(item.person_id, person.id)
self.assertIsNone(item.ingredient_id)
async def test_request_both_ingredient_and_meal_fails(self):
"""Test that requesting both ingredient and meal fails"""
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
meal = meals.Meal(
id=1,
name="Test Meal",
suggested_date=datetime.now()
)
person = test_data.Persons.jacob
with self.assertRaises(ValueError) as context:
await shopping_db.request(self.conn, person, ingredient=ingredient, meal=meal)
self.assertIn("Cannot request both an ingredient and a meal", str(context.exception))
async def test_request_neither_ingredient_nor_meal_fails(self):
"""Test that requesting neither ingredient nor meal fails"""
person = test_data.Persons.jacob
with self.assertRaises(ValueError) as context:
await shopping_db.request(self.conn, person)
self.assertIn("Must specify either an ingredient or a meal to request", str(context.exception))
async def test_request_meal_already_requested_fails(self):
"""Test that requesting an already requested meal fails"""
meal = meals.Meal(
id=1,
name="Test Meal",
suggested_date=datetime.now()
)
person = test_data.Persons.jacob
with patch('shopping.db.is_requested', return_value=True):
with self.assertRaises(ValueError) as context:
await shopping_db.request(self.conn, person, meal=meal)
self.assertIn("Meal is already requested", str(context.exception))
async def test_remove_request_meal(self):
"""Test removing a meal request"""
# First create a meal request
meal = meals.Meal(
id=1,
name="Test Meal",
suggested_date=datetime.now()
)
person = test_data.Persons.jacob
with patch('shopping.db.is_requested', return_value=False):
await shopping_db.request(self.conn, person, meal=meal)
# Then remove it
result = await shopping_db.remove_request(self.conn, meal=meal)
self.assertTrue(result)
async def test_remove_request_ingredient(self):
"""Test removing an ingredient request"""
# Create test ingredient
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
2024-05-17 09:09:03 +00:00
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient)
2024-05-17 09:09:03 +00:00
# Remove the request
result = await shopping_db.remove_request(self.conn, person=person, ingredient=ingredient)
self.assertTrue(result)
async def test_remove_request_invalid_parameters_fails(self):
"""Test that removing request with invalid parameters fails"""
person = test_data.Persons.jacob
2024-05-17 09:09:03 +00:00
with self.assertRaises(ValueError) as context:
await shopping_db.remove_request(self.conn, person=person)
self.assertIn("Must specify either a meal or an ingredient to remove", str(context.exception))
2024-05-17 09:09:03 +00:00
class TestShoppingPurchase(unittest.IsolatedAsyncioTestCase):
"""Test shopping purchase functionality"""
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()
2024-05-17 09:09:03 +00:00
async def asyncTearDown(self) -> None:
await self.conn.close()
return await super().asyncTearDown()
2024-05-17 09:09:03 +00:00
async def test_purchase_shopping_list(self):
"""Test purchasing a shopping list"""
# Create test ingredient
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
# First create a request for the ingredient
2024-05-17 09:09:03 +00:00
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient)
# Create shopping list item (this will reference the existing request)
item = ShoppingListItem(
ingredient=ingredient,
person_id=test_data.Persons.jacob.id
)
# Create shopping list
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=test_data.Persons.jacob.id,
items=[item]
)
await shopping_db.purchase(self.conn, shopping_list)
self.assertIsNotNone(shopping_list.id)
self.assertIsNotNone(item.list_id)
self.assertEqual(item.list_id, shopping_list.id)
async def test_purchase_no_person_fails(self):
"""Test that purchasing without a person fails"""
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=-1, # Invalid person id
items=[]
)
2024-05-17 09:09:03 +00:00
with self.assertRaises(ValueError) as context:
await shopping_db.purchase(self.conn, shopping_list)
self.assertIn("Shopping list must have a person id", str(context.exception))
async def test_purchase_no_items_fails(self):
"""Test that purchasing with no items fails"""
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=test_data.Persons.jacob.id,
items=[]
)
2024-05-17 09:09:03 +00:00
with self.assertRaises(ValueError) as context:
await shopping_db.purchase(self.conn, shopping_list)
self.assertIn("Shopping list must have items", str(context.exception))
2024-05-20 10:09:57 +00:00
2024-05-17 09:09:03 +00:00
class TestShoppingHelperFunctions(unittest.IsolatedAsyncioTestCase):
"""Test shopping helper functions"""
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()
2024-05-17 09:09:03 +00:00
async def asyncTearDown(self) -> None:
await self.conn.close()
return await super().asyncTearDown()
async def test_to_lookups(self):
"""Test to_lookups function"""
# Create actual items with proper IDs - need to insert them first to get valid lookups
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
# Insert the ingredient to get a valid ID
await ingredients_db.insert_ingredient(self.conn, ingredient)
# Insert test meal and recipe to get valid IDs
meal = meals.Meal(
id=1,
suggested_date=datetime.now()
)
await meals.insert_meal(self.conn, meal)
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)
2024-05-17 09:09:03 +00:00
items = [
ShoppingListItem(ingredient_id=ingredient.id, meal_id=meal.id, recipe_id=recipe.id)
]
# Test the function - it should populate the lookups based on IDs
meals_lookup, recipes_lookup, ingredients_lookup = await shopping.to_lookups(
self.conn, items
)
# Check that the lookups contain our objects
self.assertEqual(len(meals_lookup), 1)
self.assertEqual(len(recipes_lookup), 1)
self.assertEqual(len(ingredients_lookup), 1)
self.assertEqual(meals_lookup[1].id, meal.id)
self.assertEqual(recipes_lookup[1].id, recipe.id)
self.assertEqual(ingredients_lookup[1].id, ingredient.id)
# Items should still have their IDs
self.assertEqual(items[0].meal_id, meal.id)
self.assertEqual(items[0].recipe_id, recipe.id)
self.assertEqual(items[0].ingredient_id, ingredient.id)
2024-05-17 09:09:03 +00:00
def test_flatten_items_with_meal(self):
"""Test flatten_items function with meal items"""
# Create meal with recipes and ingredients
recipe_ingredient = ingredients_db.Ingredient(
id=1,
name="Recipe Ingredient",
line="500g recipe ingredient",
unit="g",
quantity=500.0,
preparation="chopped"
)
extra_ingredient = ingredients_db.Ingredient(
id=2,
name="Extra Ingredient",
line="200g extra ingredient",
unit="g",
quantity=200.0,
preparation="diced"
)
recipe = recipes.Recipe(
id=1,
name="Test Recipe",
link="http://example.com",
serves=4,
created_by_id=1,
ingredients=[recipe_ingredient]
)
meal_recipe = MealRecipe(
meal_id=1,
recipe_id=1,
servings=2.0,
recipe=recipe
)
2024-05-17 09:09:03 +00:00
meal = meals.Meal(
id=1,
suggested_date=datetime.now(),
recipes=[meal_recipe],
extra_ingredients=[extra_ingredient]
)
2024-05-17 09:09:03 +00:00
item = ShoppingListItem(meal_id=meal.id, person_id=1)
# Create lookups for flatten_items
meals_lookup = {meal.id: meal}
flattened = list(shopping.flatten_items([item], meals_lookup))
# Should have 2 items: one for recipe ingredient, one for extra ingredient
self.assertEqual(len(flattened), 2)
self.assertEqual(flattened[0].ingredient_id, 1)
self.assertEqual(flattened[1].ingredient_id, 2)
2024-05-17 09:09:03 +00:00
def test_flatten_items_without_meal(self):
"""Test flatten_items function with non-meal items"""
ingredient = ingredients_db.Ingredient(
id=1,
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
item = ShoppingListItem(ingredient_id=ingredient.id, person_id=1)
# Empty lookups since no meal/recipe is involved
flattened = list(shopping.flatten_items([item], {}))
2024-05-19 03:43:06 +00:00
self.assertEqual(len(flattened), 1)
self.assertEqual(flattened[0], item)
2024-05-19 03:43:06 +00:00
async def test_get_persons_requests(self):
"""Test get_persons_requests function"""
person_id = 1
# Create actual ingredients and requests
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
# Create a request for person 1
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient)
# Get the person's requests
requests = await shopping.get_persons_requests(self.conn, person_id)
# Should have one request for the ingredient
self.assertEqual(len(requests), 1)
self.assertEqual(requests[0].id, ingredient.id)
2024-05-19 03:43:06 +00:00
async def test_get_outstanding_requests(self):
"""Test get_outstanding_requests function"""
# Create actual data
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
# Create requests
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient)
# Test the function
outstanding, purchased, meal_requests, _, _, _ = await shopping.get_outstanding_requests(self.conn)
# Should have one outstanding ingredient request
self.assertGreaterEqual(len(outstanding), 1)
self.assertEqual(len(purchased), 0)
self.assertEqual(len(meal_requests), 0)
2024-05-19 03:43:06 +00:00
async def test_is_requested(self):
"""Test is_requested function"""
meal = meals.Meal(
id=1,
suggested_date=datetime.now()
)
# Test with a meal that hasn't been requested
result = await shopping_db.is_requested(self.conn, meal)
self.assertFalse(result)
# Create a request for the meal
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, meal=meal)
# Now it should be requested
result = await shopping_db.is_requested(self.conn, meal)
self.assertTrue(result)
2024-05-17 09:09:03 +00:00
async def test_is_requested_invalid_meal(self):
"""Test is_requested with invalid meal"""
meal = meals.Meal(
id=-1,
name="Invalid Meal",
suggested_date=datetime.now()
)
result = await shopping_db.is_requested(self.conn, meal)
self.assertFalse(result)
2024-05-20 10:09:57 +00:00
async def test_load_shopping_list(self):
"""Test load_shopping_list function"""
# Create and purchase a shopping list first
ingredient = ingredients_db.Ingredient(
name="Broccoli",
line="500g fresh broccoli",
unit="g",
quantity=500.0,
preparation="chopped"
)
await ingredients_db.insert_ingredient(self.conn, ingredient)
# First create a request for the ingredient
2024-05-20 10:09:57 +00:00
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient)
item = ShoppingListItem(
ingredient_id=ingredient.id,
person_id=test_data.Persons.jacob.id
)
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=test_data.Persons.jacob.id,
items=[item]
)
await shopping_db.purchase(self.conn, shopping_list)
# Now load it back
loaded_list = await shopping_db.load_shopping_list(self.conn, shopping_list.id)
self.assertIsNotNone(loaded_list)
self.assertEqual(loaded_list.id, shopping_list.id)
self.assertEqual(loaded_list.store_name, shopping_list.store_name)
self.assertEqual(len(loaded_list.items), 1)
class TestShoppingComplexEdgeCases(unittest.IsolatedAsyncioTestCase):
"""Test shopping complex edge cases for meal requests and purchases"""
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_meal_request_includes_ingredients_in_outstanding(self):
"""Test that when user requests a meal, get_outstanding_requests includes the ingredients of that meal"""
# Create a recipe with ingredients
recipe = recipes.Recipe(
id=-1,
name="Test Pasta Recipe",
link="http://example.com/pasta",
serves=4,
created_by_id=test_data.Persons.jacob.id
)
await recipes.insert_recipe(self.conn, recipe)
# Create ingredients for the recipe
pasta_ingredient = ingredients_db.Ingredient(
name="Pasta",
line="500g pasta",
unit="g",
quantity=500.0,
preparation="",
recipe_id=recipe.id
)
tomato_ingredient = ingredients_db.Ingredient(
name="Tomatoes",
line="400g canned tomatoes",
unit="g",
quantity=400.0,
preparation="",
recipe_id=recipe.id
)
await ingredients_db.insert_ingredient(self.conn, pasta_ingredient)
await ingredients_db.insert_ingredient(self.conn, tomato_ingredient)
# Load the recipe with its ingredients
await recipes.load_recipe_ingredients(self.conn, recipe)
# Create a meal with this recipe and extra ingredients
extra_ingredient = ingredients_db.Ingredient(
name="Garlic Bread",
line="1 loaf garlic bread",
unit="loaf",
quantity=1.0,
preparation=""
)
meal_recipe = MealRecipe(
meal_id=-1,
recipe_id=recipe.id,
servings=2.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]
)
# Insert the meal
await meals.insert_meal(self.conn, meal)
# Request the meal
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, meal=meal)
# Get outstanding requests
outstanding, purchased, meal_requests, meals_lookup, recipes_lookup, ingredients_lookup = await shopping.get_outstanding_requests(self.conn)
# Should have one meal request
self.assertEqual(len(meal_requests), 1)
self.assertEqual(meal_requests[0].meal_id, meal.id)
# Should have 3 outstanding items: 2 from recipe + 1 extra ingredient
self.assertEqual(len(outstanding), 3)
# Check that all ingredients are included
ingredient_names = {ingredients_lookup[item.ingredient_id].name for item in outstanding if item.ingredient_id in ingredients_lookup}
self.assertIn("Pasta", ingredient_names)
self.assertIn("Tomatoes", ingredient_names)
self.assertIn("Garlic Bread", ingredient_names)
# All should be associated with the meal
for item in outstanding:
self.assertEqual(item.meal_id, meal.id)
async def test_partial_meal_purchase_moves_item_to_purchased(self):
"""Test that a user can purchase an individual item from a meal, moving it to purchased items"""
# Create a recipe with multiple ingredients
recipe = recipes.Recipe(
id=-1,
name="Multi-Ingredient Recipe",
link="http://example.com/multi",
serves=4,
created_by_id=test_data.Persons.jacob.id
)
await recipes.insert_recipe(self.conn, recipe)
# Create multiple ingredients for the recipe
ingredient1 = ingredients_db.Ingredient(
name="Rice",
line="200g rice",
unit="g",
quantity=200.0,
preparation="",
recipe_id=recipe.id
)
ingredient2 = ingredients_db.Ingredient(
name="Chicken",
line="300g chicken breast",
unit="g",
quantity=300.0,
preparation="",
recipe_id=recipe.id
)
ingredient3 = ingredients_db.Ingredient(
name="Vegetables",
line="150g mixed vegetables",
unit="g",
quantity=150.0,
preparation="",
recipe_id=recipe.id
)
await ingredients_db.insert_ingredient(self.conn, ingredient1)
await ingredients_db.insert_ingredient(self.conn, ingredient2)
await ingredients_db.insert_ingredient(self.conn, ingredient3)
# Load the recipe with its ingredients
await recipes.load_recipe_ingredients(self.conn, recipe)
# Create and insert a meal
meal_recipe = MealRecipe(
meal_id=-1,
recipe_id=recipe.id,
servings=2.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_db.request(self.conn, person, meal=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), 3) # All 3 ingredients
self.assertEqual(len(purchased_before), 0) # Nothing purchased yet
# Purchase only one ingredient (Rice) from the meal
rice_item = None
for item in outstanding_before:
if item.ingredient_id in ingredients_lookup and ingredients_lookup[item.ingredient_id].name == "Rice":
rice_item = ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id,
meal_id=meal.id,
recipe_id=recipe.id
)
break
self.assertIsNotNone(rice_item)
# Create and purchase a shopping list with just the rice
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=person.id,
items=[rice_item]
)
await shopping_db.purchase(self.conn, shopping_list)
# Get outstanding requests after purchase
outstanding_after, purchased_after, meal_requests_after, meals_lookup2, recipes_lookup2, ingredients_lookup2 = await shopping.get_outstanding_requests(self.conn)
# Should have 2 outstanding items (Chicken and Vegetables)
self.assertEqual(len(outstanding_after), 2)
outstanding_names = {ingredients_lookup2[item.ingredient_id].name for item in outstanding_after if item.ingredient_id in ingredients_lookup2}
self.assertIn("Chicken", outstanding_names)
self.assertIn("Vegetables", outstanding_names)
self.assertNotIn("Rice", outstanding_names)
# Should have 1 purchased item (Rice)
self.assertEqual(len(purchased_after), 1)
self.assertEqual(purchased_after[0].ingredient_id, ingredient1.id)
# Meal should still be requested (not all ingredients purchased)
self.assertEqual(len(meal_requests_after), 1)
async def test_complete_meal_purchase_unrequests_and_marks_purchased(self):
"""Test that when all items of a meal are purchased, the meal is unrequested and marked as purchased"""
# 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_db.Ingredient(
name="Bread",
line="2 slices bread",
unit="slices",
quantity=2.0,
preparation="",
recipe_id=recipe.id
)
ingredient2 = ingredients_db.Ingredient(
name="Butter",
line="10g butter",
unit="g",
quantity=10.0,
preparation="",
recipe_id=recipe.id
)
await ingredients_db.insert_ingredient(self.conn, ingredient1)
await ingredients_db.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 = 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_db.request(self.conn, person, meal=meal)
# Verify meal is requested
is_requested_before = await shopping_db.is_requested(self.conn, meal)
self.assertTrue(is_requested_before)
# 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) # Both ingredients
self.assertEqual(len(meal_requests_before), 1) # Meal is requested
# Verify the meal is not marked as purchased yet
found_meal_before = await meals.find_meal_by_id(self.conn, meal.id)
self.assertIsNone(found_meal_before.purchase_date)
# Purchase all ingredients from the meal
shopping_items = []
for item in outstanding_before:
shopping_items.append(ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id,
meal_id=meal.id,
recipe_id=recipe.id
))
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=person.id,
items=shopping_items
)
await shopping_db.purchase(self.conn, shopping_list)
# Verify meal is no longer requested
is_requested_after = await shopping_db.is_requested(self.conn, meal)
self.assertFalse(is_requested_after)
# Verify meal is marked as purchased
found_meal_after = await meals.find_meal_by_id(self.conn, meal.id)
self.assertIsNotNone(found_meal_after.purchase_date)
# Get outstanding requests after complete purchase
outstanding_after, purchased_after, meal_requests_after, _, _, _ = await shopping.get_outstanding_requests(self.conn)
# Should have no outstanding items from this meal
self.assertEqual(len(outstanding_after), 0)
# Should have no purchased items (meal is complete so ingredients don't appear)
self.assertEqual(len(purchased_after), 0)
# Should have no meal requests
self.assertEqual(len(meal_requests_after), 0)
async def test_complete_meal_with_extra_ingredients_purchase(self):
"""Test that meals with both recipe ingredients and extra ingredients are properly handled"""
# 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)
# Create recipe ingredient
recipe_ingredient = ingredients_db.Ingredient(
name="Main Ingredient",
line="200g main ingredient",
unit="g",
quantity=200.0,
preparation="",
recipe_id=recipe.id
)
await ingredients_db.insert_ingredient(self.conn, recipe_ingredient)
await recipes.load_recipe_ingredients(self.conn, recipe)
# Create extra ingredient (not part of recipe)
extra_ingredient = ingredients_db.Ingredient(
name="Side Dish",
line="1 side dish",
unit="item",
quantity=1.0,
preparation=""
)
# Create and insert a meal with both recipe and extra ingredients
meal_recipe = 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)
# Request the meal
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, meal=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) # Recipe ingredient + extra ingredient
self.assertEqual(len(meal_requests_before), 1)
# Purchase all ingredients
shopping_items = []
for item in outstanding_before:
shopping_items.append(ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id,
meal_id=meal.id,
recipe_id=item.recipe_id
))
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=person.id,
items=shopping_items
)
await shopping_db.purchase(self.conn, shopping_list)
# Verify meal is unrequested and marked as purchased
is_requested_after = await shopping_db.is_requested(self.conn, meal)
self.assertFalse(is_requested_after)
found_meal_after = await meals.find_meal_by_id(self.conn, meal.id)
self.assertIsNotNone(found_meal_after.purchase_date)
# Get final state
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) # No purchased items since meal is complete
self.assertEqual(len(meal_requests_after), 0)
async def test_individual_ingredient_purchase_without_meal(self):
"""Test purchasing individual ingredients that are not part of a meal"""
# Create individual ingredients
ingredient1 = ingredients_db.Ingredient(
name="Milk",
line="1L milk",
unit="L",
quantity=1.0,
preparation=""
)
ingredient2 = ingredients_db.Ingredient(
name="Eggs",
line="12 eggs",
unit="dozen",
quantity=1.0,
preparation=""
)
await ingredients_db.insert_ingredient(self.conn, ingredient1)
await ingredients_db.insert_ingredient(self.conn, ingredient2)
# Request individual ingredients (not part of any meal)
person = test_data.Persons.jacob
await shopping_db.request(self.conn, person, ingredient=ingredient1)
await shopping_db.request(self.conn, person, ingredient=ingredient2)
# Verify both ingredients appear in 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(purchased_before), 0)
self.assertEqual(len(meal_requests_before), 0) # No meal requests
# Verify the ingredients in outstanding requests
ingredient_names = {ingredients_lookup[item.ingredient_id].name for item in outstanding_before if item.ingredient_id in ingredients_lookup}
self.assertIn("Milk", ingredient_names)
self.assertIn("Eggs", ingredient_names)
# All should be individual requests (no meal_id)
for item in outstanding_before:
self.assertIsNone(item.meal_id)
self.assertEqual(item.person_id, person.id)
# Purchase only one ingredient (Milk)
milk_item = None
for item in outstanding_before:
if item.ingredient_id in ingredients_lookup and ingredients_lookup[item.ingredient_id].name == "Milk":
milk_item = ShoppingListItem(
ingredient_id=item.ingredient_id,
person_id=person.id
)
break
self.assertIsNotNone(milk_item)
shopping_list = ShoppingList(
store_name=StoreEnum.woolworths,
purchased_by_id=person.id,
items=[milk_item]
)
await shopping_db.purchase(self.conn, shopping_list)
# Verify only eggs remains in outstanding, milk is purchased
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) # Individual purchases don't appear in purchased list
self.assertEqual(len(meal_requests_after), 0)
# Verify only eggs remains
self.assertEqual(ingredients_lookup_after[outstanding_after[0].ingredient_id].name, "Eggs")
self.assertIsNone(outstanding_after[0].meal_id)
# Purchase the remaining ingredient (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_db.purchase(self.conn, shopping_list2)
# Verify no outstanding requests remain
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):
"""Test a mix of meal requests and individual ingredient requests"""
# Create a simple 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)
# Create recipe ingredient
pasta_ingredient = ingredients_db.Ingredient(
name="Pasta",
line="200g pasta",
unit="g",
quantity=200.0,
preparation="",
recipe_id=recipe.id
)
await ingredients_db.insert_ingredient(self.conn, pasta_ingredient)
await recipes.load_recipe_ingredients(self.conn, recipe)
# Create meal
meal_recipe = 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)
# Create individual ingredient
snack_ingredient = ingredients_db.Ingredient(
name="Chips",
line="1 bag chips",
unit="bag",
quantity=1.0,
preparation=""
)
await ingredients_db.insert_ingredient(self.conn, snack_ingredient)
person = test_data.Persons.jacob
# Request both meal and individual ingredient
await shopping_db.request(self.conn, person, meal=meal)
await shopping_db.request(self.conn, person, ingredient=snack_ingredient)
# Verify we have both meal and individual 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) # Pasta from meal + Chips individual
self.assertEqual(len(purchased_before), 0)
self.assertEqual(len(meal_requests_before), 1) # One meal request
# Verify the mix of ingredients
ingredient_names = {ingredients_lookup[item.ingredient_id].name for item in outstanding_before if item.ingredient_id in ingredients_lookup}
self.assertIn("Pasta", ingredient_names)
self.assertIn("Chips", ingredient_names)
# Check that pasta is from meal, chips is individual
pasta_item = None
chips_item = None
for item in outstanding_before:
if item.ingredient_id in ingredients_lookup:
ingredient_name = ingredients_lookup[item.ingredient_id].name
if ingredient_name == "Pasta":
pasta_item = item
elif ingredient_name == "Chips":
chips_item = item
self.assertIsNotNone(pasta_item)
self.assertIsNotNone(chips_item)
self.assertEqual(pasta_item.meal_id, meal.id)
self.assertIsNone(chips_item.meal_id)
# Purchase the individual ingredient (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_db.purchase(self.conn, shopping_list)
2024-05-20 10:09:57 +00:00
# Verify only meal ingredient remains
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) # Only pasta from meal
self.assertEqual(len(purchased_after), 0)
self.assertEqual(len(meal_requests_after), 1) # Meal still requested
2024-05-20 10:09:57 +00:00
self.assertEqual(ingredients_lookup_after[outstanding_after[0].ingredient_id].name, "Pasta")
self.assertEqual(outstanding_after[0].meal_id, meal.id)
2024-05-20 10:09:57 +00:00
if __name__ == '__main__':
unittest.main()