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