munch-ease-backend/tests/test_meals.py

506 lines
18 KiB
Python
Raw Normal View History

import unittest
import asyncio
from datetime import datetime
import importlib
import tests.test_data as test_data
2025-10-18 03:26:42 +00:00
def reload_test_data():
global test_data
test_data = importlib.reload(test_data)
2025-10-18 03:26:42 +00:00
from db import connect, create
import meals
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 09:24:23 +00:00
import meals.repository as meals_db
from meals.models import Meal, MealRecipe
import persons
import recipes
import ingredients
import products
class TestMealsModels(unittest.IsolatedAsyncioTestCase):
"""Test the meals data models"""
2025-10-18 03:26:42 +00:00
async def asyncSetUp(self):
2025-10-18 03:26:42 +00:00
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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie, test_data.Persons.chris],
)
2025-10-18 03:26:42 +00:00
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"""
2025-10-18 03:26:42 +00:00
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"""
2025-10-18 03:26:42 +00:00
async def asyncSetUp(self):
2025-10-18 03:26:42 +00:00
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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Verify meal was inserted and got an ID
self.assertGreater(meal.id, 0)
2025-10-18 03:26:42 +00:00
# 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)
2025-10-18 03:26:42 +00:00
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],
2025-10-18 03:26:42 +00:00
recipes=[meal_recipe],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Verify meal was inserted
self.assertGreater(meal.id, 0)
2025-10-18 03:26:42 +00:00
# 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,
2025-10-18 03:26:42 +00:00
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, {})
2025-10-18 03:26:42 +00:00
# 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="",
2025-10-18 03:26:42 +00:00
product_id=product.id,
)
2025-10-18 03:26:42 +00:00
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],
2025-10-18 03:26:42 +00:00
extra_ingredients=[extra_ingredient],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Verify meal was inserted
self.assertGreater(meal.id, 0)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
original_id = meal.id
2025-10-18 03:26:42 +00:00
# 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
2025-10-18 03:26:42 +00:00
await meals_db.update_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Mark as consumed
consumed_date = datetime(2024, 5, 1, 19, 30)
await meals_db.mark_consumed(self.conn, meal, consumed_date)
2025-10-18 03:26:42 +00:00
# Verify consumed date was set
self.assertEqual(meal.consumed_date, consumed_date)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Mark as purchased
updated_meal = await meals_db.mark_purchased(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Verify purchase date was set
self.assertIsNotNone(updated_meal.purchase_date)
self.assertIsNotNone(meal.purchase_date)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
meal_id = meal.id
2025-10-18 03:26:42 +00:00
# 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)
2025-10-18 03:26:42 +00:00
# Delete the meal
await meals_db.delete_meal(self.conn, meal_id)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
meal2 = Meal(
suggested_date=datetime(2024, 8, 15, 18, 0),
chefs=[test_data.Persons.ryan],
cleanup=[test_data.Persons.jacob],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.chris],
)
2025-10-18 03:26:42 +00:00
meal3 = Meal(
suggested_date=datetime(2024, 9, 1, 18, 0),
chefs=[test_data.Persons.ellie],
cleanup=[test_data.Persons.chris],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.jacob],
)
2025-10-18 03:26:42 +00:00
# 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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
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)
2025-10-18 03:26:42 +00:00
# Mark consumed meal as consumed in DB
await meals_db.mark_consumed(self.conn, consumed_meal, consumed_meal.consumed_date)
2025-10-18 03:26:42 +00:00
# Find meals in August 2024
start_date = datetime(2024, 8, 1)
end_date = datetime(2024, 8, 31)
2025-10-18 03:26:42 +00:00
upcoming_meals = []
2025-10-18 03:26:42 +00:00
async for meal in meals_db.find_upcoming_meals_by_date_range(
self.conn, start_date, end_date
):
upcoming_meals.append(meal)
2025-10-18 03:26:42 +00:00
# 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"""
2025-10-18 03:26:42 +00:00
async def asyncSetUp(self):
2025-10-18 03:26:42 +00:00
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],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Update participants
new_chefs = [test_data.Persons.ryan, test_data.Persons.ellie]
2025-10-18 03:26:42 +00:00
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)
2025-10-18 03:26:42 +00:00
# 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"""
2025-10-18 03:26:42 +00:00
async def asyncSetUp(self):
2025-10-18 03:26:42 +00:00
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)
2025-10-18 03:26:42 +00:00
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)
2025-10-18 03:26:42 +00:00
meal = Meal(
suggested_date=datetime(2024, 11, 1, 18, 0),
chefs=[test_data.Persons.jacob],
cleanup=[test_data.Persons.ryan],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Add recipes to meal
2025-10-18 03:26:42 +00:00
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)
2025-10-18 03:26:42 +00:00
# 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"""
2025-10-18 03:26:42 +00:00
async def asyncSetUp(self):
2025-10-18 03:26:42 +00:00
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,
2025-10-18 03:26:42 +00:00
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, {})
2025-10-18 03:26:42 +00:00
meal = Meal(
suggested_date=datetime(2024, 12, 1, 18, 0),
chefs=[test_data.Persons.jacob],
cleanup=[test_data.Persons.ryan],
2025-10-18 03:26:42 +00:00
consumers=[test_data.Persons.ellie],
)
2025-10-18 03:26:42 +00:00
await meals_db.insert_meal(self.conn, meal)
2025-10-18 03:26:42 +00:00
# Add extra ingredients
extra_ingredient = ingredients.Ingredient(
id=-1,
name="Test Bread Roll",
line="1 roll test bread",
unit="roll",
quantity=1.0,
preparation="",
2025-10-18 03:26:42 +00:00
product_id=product.id,
)
2025-10-18 03:26:42 +00:00
await meals_db.sync_extra_ingredients(self.conn, meal.id, [extra_ingredient])
2025-10-18 03:26:42 +00:00
# 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")
2025-10-18 03:26:42 +00:00
if __name__ == "__main__":
unittest.main()