import unittest import asyncio import tests.test_data as test_data import importlib def reload_test_data(): global test_data test_data = importlib.reload(test_data) from db import connect, create import ingredients import ingredients.db as ingredients_db import products.db as products_db import units class TestIngredient(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() return await super().asyncSetUp() async def asyncTearDown(self) -> None: await self.conn.close() return await super().asyncTearDown() async def test_ingredient_creation(self): """Test basic ingredient creation""" ingredient = ingredients_db.Ingredient( name="Broccoli", line="500g fresh broccoli", unit="g", quantity=500.0, preparation="chopped" ) self.assertEqual(ingredient.name, "Broccoli") self.assertEqual(ingredient.line, "500g fresh broccoli") self.assertEqual(ingredient.unit, "g") self.assertEqual(ingredient.quantity, 500.0) self.assertEqual(ingredient.preparation, "chopped") async def test_insert_ingredient(self): """Test inserting an ingredient into the database""" ingredient = ingredients_db.Ingredient( name="Garlic", line="2 cloves garlic", unit="Items", quantity=2.0, preparation="minced" ) await ingredients_db.insert_ingredient(self.conn, ingredient) self.assertGreater(ingredient.id, 0) # Verify it was inserted correctly found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id) self.assertIsNotNone(found_ingredient) self.assertEqual(found_ingredient.name, "Garlic") self.assertEqual(found_ingredient.quantity, 2.0) async def test_insert_ingredient_with_product(self): """Test inserting an ingredient with an associated product""" # First create and insert a product product = test_data.Products.broccoli await products_db.insert_product(self.conn, product, {}) ingredient = ingredients_db.Ingredient( name="Fresh Broccoli", line="1 piece fresh broccoli", unit="Items", quantity=1.0, preparation="", product_id=product.id, product=product ) await ingredients_db.insert_ingredient(self.conn, ingredient) # Verify the ingredient was inserted with the product reference found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id) self.assertIsNotNone(found_ingredient) self.assertEqual(found_ingredient.product_id, product.id) self.assertIsNotNone(found_ingredient.product) self.assertEqual(found_ingredient.product.name, product.name) async def test_find_ingredient_by_id_not_found(self): """Test finding a non-existent ingredient returns None""" result = await ingredients_db.find_ingredient_by_id(self.conn, 999) self.assertIsNone(result) async def test_find_ingredients_by_recipe_id(self): """Test finding ingredients by recipe ID""" # Create ingredients with the same recipe_id recipe_id = 1 ingredient1 = ingredients_db.Ingredient( name="Flour", line="2 cups flour", unit="cups", quantity=2.0, preparation="", recipe_id=recipe_id ) ingredient2 = ingredients_db.Ingredient( name="Sugar", line="1 cup sugar", unit="cups", quantity=1.0, preparation="", recipe_id=recipe_id ) await ingredients_db.insert_ingredient(self.conn, ingredient1) await ingredients_db.insert_ingredient(self.conn, ingredient2) # Find ingredients by recipe ID ingredients_list = [] async for ingredient in ingredients_db.find_ingredients_by_recipe_id(self.conn, recipe_id): ingredients_list.append(ingredient) self.assertEqual(len(ingredients_list), 2) names = [ing.name for ing in ingredients_list] self.assertIn("Flour", names) self.assertIn("Sugar", names) async def test_find_ingredients_by_meal_id(self): """Test finding ingredients by meal ID""" # Create ingredients with the same meal_id meal_id = 1 ingredient1 = ingredients_db.Ingredient( name="Chicken", line="1 lb chicken breast", unit="lb", quantity=1.0, preparation="diced", meal_id=meal_id ) ingredient2 = ingredients_db.Ingredient( name="Rice", line="2 cups rice", unit="cups", quantity=2.0, preparation="", meal_id=meal_id ) await ingredients_db.insert_ingredient(self.conn, ingredient1) await ingredients_db.insert_ingredient(self.conn, ingredient2) # Find ingredients by meal ID ingredients_list = [] async for ingredient in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ingredient) self.assertEqual(len(ingredients_list), 2) names = [ing.name for ing in ingredients_list] self.assertIn("Chicken", names) self.assertIn("Rice", names) async def test_delete_ingredients_by_meal_id(self): """Test deleting ingredients by meal ID""" meal_id = 1 ingredient = ingredients_db.Ingredient( name="Tomato", line="2 tomatoes", unit="Items", quantity=2.0, preparation="sliced", meal_id=meal_id ) await ingredients_db.insert_ingredient(self.conn, ingredient) # Verify ingredient exists ingredients_list = [] async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ing) self.assertEqual(len(ingredients_list), 1) # Delete ingredients by meal ID await ingredients_db.delete_ingredients_by_meal_id(self.conn, meal_id) # Verify ingredients are deleted ingredients_list = [] async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ing) self.assertEqual(len(ingredients_list), 0) import unittest import asyncio import tests.test_data as test_data import importlib def reload_test_data(): global test_data test_data = importlib.reload(test_data) from db import connect, create import ingredients import ingredients.db as ingredients_db import products.db as products_db import units class TestIngredient(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() return await super().asyncSetUp() async def asyncTearDown(self) -> None: await self.conn.close() return await super().asyncTearDown() async def test_ingredient_creation(self): """Test basic ingredient creation""" ingredient = ingredients_db.Ingredient( name="Broccoli", line="500g fresh broccoli", unit="g", quantity=500.0, preparation="chopped" ) self.assertEqual(ingredient.name, "Broccoli") self.assertEqual(ingredient.line, "500g fresh broccoli") self.assertEqual(ingredient.unit, "g") self.assertEqual(ingredient.quantity, 500.0) self.assertEqual(ingredient.preparation, "chopped") async def test_insert_ingredient(self): """Test inserting an ingredient into the database""" ingredient = ingredients_db.Ingredient( name="Garlic", line="2 cloves garlic", unit="Items", quantity=2.0, preparation="minced" ) await ingredients_db.insert_ingredient(self.conn, ingredient) self.assertGreater(ingredient.id, 0) # Verify it was inserted correctly found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id) self.assertIsNotNone(found_ingredient) self.assertEqual(found_ingredient.name, "Garlic") self.assertEqual(found_ingredient.quantity, 2.0) async def test_insert_ingredient_with_product(self): """Test inserting an ingredient with an associated product""" # First create and insert a product product = test_data.Products.broccoli await products_db.insert_product(self.conn, product, {}) ingredient = ingredients_db.Ingredient( name="Fresh Broccoli", line="1 piece fresh broccoli", unit="Items", quantity=1.0, preparation="", product_id=product.id, product=product ) await ingredients_db.insert_ingredient(self.conn, ingredient) # Verify the ingredient was inserted with the product reference found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id) self.assertIsNotNone(found_ingredient) self.assertEqual(found_ingredient.product_id, product.id) self.assertIsNotNone(found_ingredient.product) self.assertEqual(found_ingredient.product.name, product.name) async def test_find_ingredient_by_id_not_found(self): """Test finding a non-existent ingredient returns None""" result = await ingredients_db.find_ingredient_by_id(self.conn, 999) self.assertIsNone(result) async def test_find_ingredients_by_recipe_id(self): """Test finding ingredients by recipe ID""" # Create ingredients with the same recipe_id recipe_id = 1 ingredient1 = ingredients_db.Ingredient( name="Flour", line="2 cups flour", unit="cups", quantity=2.0, preparation="", recipe_id=recipe_id ) ingredient2 = ingredients_db.Ingredient( name="Sugar", line="1 cup sugar", unit="cups", quantity=1.0, preparation="", recipe_id=recipe_id ) await ingredients_db.insert_ingredient(self.conn, ingredient1) await ingredients_db.insert_ingredient(self.conn, ingredient2) # Find ingredients by recipe ID ingredients_list = [] async for ingredient in ingredients_db.find_ingredients_by_recipe_id(self.conn, recipe_id): ingredients_list.append(ingredient) self.assertEqual(len(ingredients_list), 2) names = [ing.name for ing in ingredients_list] self.assertIn("Flour", names) self.assertIn("Sugar", names) async def test_find_ingredients_by_meal_id(self): """Test finding ingredients by meal ID""" # Create ingredients with the same meal_id meal_id = 1 ingredient1 = ingredients_db.Ingredient( name="Chicken", line="1 lb chicken breast", unit="lb", quantity=1.0, preparation="diced", meal_id=meal_id ) ingredient2 = ingredients_db.Ingredient( name="Rice", line="2 cups rice", unit="cups", quantity=2.0, preparation="", meal_id=meal_id ) await ingredients_db.insert_ingredient(self.conn, ingredient1) await ingredients_db.insert_ingredient(self.conn, ingredient2) # Find ingredients by meal ID ingredients_list = [] async for ingredient in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ingredient) self.assertEqual(len(ingredients_list), 2) names = [ing.name for ing in ingredients_list] self.assertIn("Chicken", names) self.assertIn("Rice", names) async def test_delete_ingredients_by_meal_id(self): """Test deleting ingredients by meal ID""" meal_id = 1 ingredient = ingredients_db.Ingredient( name="Tomato", line="2 tomatoes", unit="Items", quantity=2.0, preparation="sliced", meal_id=meal_id ) await ingredients_db.insert_ingredient(self.conn, ingredient) # Verify ingredient exists ingredients_list = [] async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ing) self.assertEqual(len(ingredients_list), 1) # Delete ingredients by meal ID await ingredients_db.delete_ingredients_by_meal_id(self.conn, meal_id) # Verify ingredients are deleted ingredients_list = [] async for ing in ingredients_db.find_ingredients_by_meal_id(self.conn, meal_id): ingredients_list.append(ing) self.assertEqual(len(ingredients_list), 0) async def test_ingredient_with_negative_product_id(self): """Test that negative product_id is converted to None during insertion""" ingredient = ingredients_db.Ingredient( name="Test Ingredient", line="1 test ingredient", unit="Items", quantity=1.0, preparation="", product_id=-1 ) await ingredients_db.insert_ingredient(self.conn, ingredient) found_ingredient = await ingredients_db.find_ingredient_by_id(self.conn, ingredient.id) self.assertIsNone(found_ingredient.product_id) class TestIngredientParsing(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() return await super().asyncSetUp() async def asyncTearDown(self) -> None: await self.conn.close() return await super().asyncTearDown() async def test_parse_ingredient_from_link_invalid_format(self): """Test parsing ingredient from invalid link format returns None""" invalid_links = [ "invalid link format", "just a url https://example.com", "no quantity https://example.com", "", "abc https://example.com" ] for invalid_link in invalid_links: result = await ingredients.parse_ingredient_from_link(self.conn, invalid_link) self.assertIsNone(result, f"Should return None for: {invalid_link}") async def test_parse_ingredient_from_link_valid_format_no_quantity(self): """Test parsing ingredient from valid link format without explicit quantity""" link = "https://www.woolworths.com.au/shop/productdetails/134681/fresh-broccoli" result = await ingredients.parse_ingredient_from_link(self.conn, link) # The scraper actually works for this URL, so we should get a result self.assertIsNotNone(result) self.assertEqual(result.quantity, 1.0) # Default quantity when none specified self.assertEqual(result.unit, units.ITEMS.name) self.assertIsInstance(result, ingredients_db.Ingredient) async def test_parse_ingredient_from_link_regex_parsing(self): """Test that the regex correctly parses quantity and URL from valid links""" import re # Test the regex pattern used in parse_ingredient_from_link test_cases = [ ("2 https://example.com", "2", "https://example.com"), ("10 https://www.woolworths.com.au/product", "10", "https://www.woolworths.com.au/product"), ("https://example.com", None, "https://example.com"), ("1 https://test.com", "1", "https://test.com") ] for link, expected_qty, expected_url in test_cases: match = re.match(r'^(\d+)?\s*(http.*)$', link) if match: quantity = int(match.group(1)) if match.group(1) else 1 url = match.group(2) if expected_qty: self.assertEqual(quantity, int(expected_qty)) else: self.assertEqual(quantity, 1) # Default quantity self.assertEqual(url, expected_url) async def test_parse_ingredient_from_nlp_simple_cases(self): """Test parsing simple ingredient cases that don't require external dependencies""" # Test the basic structure without relying on ingredient_parser # Since ingredient_parser is an external dependency, we'll test what we can # We can test that the function exists and handles basic error cases try: result = ingredients.parse_ingredient_from_nlp("2 cups flour") # The function may fail due to missing ingredient_parser, but it should not crash # If it works, result should be an Ingredient object if result is not None: self.assertIsInstance(result, ingredients_db.Ingredient) except ImportError: # If ingredient_parser is not available, that's expected self.skipTest("ingredient_parser not available") except Exception as e: # Other exceptions should not occur in normal operation self.fail(f"Unexpected exception: {e}") class TestIngredientMatching(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() return await super().asyncSetUp() async def asyncTearDown(self) -> None: await self.conn.close() return await super().asyncTearDown() async def test_match_existing_products_with_real_data(self): """Test matching ingredients to existing products using real operations""" # Setup: Create and insert a product with tags product = test_data.Products.broccoli await products_db.insert_product(self.conn, product, {}) await products_db.add_tag(self.conn, product, "broccoli") # Create ingredients without products ingredient1 = ingredients_db.Ingredient( name="broccoli", line="1 piece broccoli", unit="Items", quantity=1.0, preparation="" ) ingredient2 = ingredients_db.Ingredient( name="unknown vegetable", line="1 piece unknown vegetable", unit="Items", quantity=1.0, preparation="" ) ingredients_list = [ingredient1, ingredient2] result = await ingredients.match_existing_products(self.conn, ingredients_list) # Check that first ingredient got matched self.assertEqual(result[0].product_id, product.id) self.assertIsNotNone(result[0].product) self.assertEqual(result[0].product.name, product.name) # Check that second ingredient remained unmatched self.assertIsNone(result[1].product) async def test_match_existing_products_already_has_product(self): """Test that ingredients with existing products are not re-matched""" product = test_data.Products.broccoli ingredient = ingredients_db.Ingredient( name="broccoli", line="1 piece broccoli", unit="Items", quantity=1.0, preparation="", product=product, product_id=product.id ) ingredients_list = [ingredient] result = await ingredients.match_existing_products(self.conn, ingredients_list) # Should remain unchanged self.assertEqual(result[0].product_id, product.id) self.assertEqual(result[0].product, product) async def test_match_existing_products_empty_list(self): """Test matching empty ingredients list""" result = await ingredients.match_existing_products(self.conn, []) self.assertEqual(result, []) async def test_ingredient_keys_constant(self): """Test that the KEYS constant contains expected fields""" expected_keys = ['id', 'name', 'line', 'preparation', 'unit', 'quantity', 'product_id', 'recipe_id', 'meal_id'] self.assertEqual(ingredients_db.Ingredient.KEYS, expected_keys) async def test_ingredient_default_values(self): """Test ingredient default values""" ingredient = ingredients_db.Ingredient( name="Test", line="Test line", unit="Items", quantity=1.0, preparation="" ) self.assertEqual(ingredient.id, -1) self.assertIsNone(ingredient.product_id) self.assertIsNone(ingredient.recipe_id) self.assertIsNone(ingredient.meal_id) self.assertIsNone(ingredient.product) if __name__ == '__main__': unittest.main()