munch-ease-backend/tests/test_meals.py
2025-11-01 22:35:10 +11:00

507 lines
18 KiB
Python

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.repository as meals_db
from meals.models import Meal, MealRecipe
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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.ellie, test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.ellie, test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.ryan] # Change chef
meal.cleanup = [test_data.MemberRefs.ellie] # Change cleanup
meal.consumers = [
test_data.MemberRefs.jacob,
test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.ellie],
)
meal2 = Meal(
suggested_date=datetime(2024, 8, 15, 18, 0),
chefs=[test_data.MemberRefs.ryan],
cleanup=[test_data.MemberRefs.jacob],
consumers=[test_data.MemberRefs.chris],
)
meal3 = Meal(
suggested_date=datetime(2024, 9, 1, 18, 0),
chefs=[test_data.MemberRefs.ellie],
cleanup=[test_data.MemberRefs.chris],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.ellie],
)
await meals_db.insert_meal(self.conn, meal)
# Update participants
new_chefs = [test_data.MemberRefs.ryan, test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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.MemberRefs.jacob],
cleanup=[test_data.MemberRefs.ryan],
consumers=[test_data.MemberRefs.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()