From 2a66ff39be127f282299f135744e92afe9658f86 Mon Sep 17 00:00:00 2001 From: jableader Date: Wed, 30 Jul 2025 18:55:51 +1000 Subject: [PATCH] Vibe coded some tests -_- --- ingredients/db.py | 2 +- tests/test_ingredients.py | 593 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 594 insertions(+), 1 deletion(-) create mode 100644 tests/test_ingredients.py diff --git a/ingredients/db.py b/ingredients/db.py index d4d983e..4e5b1ec 100644 --- a/ingredients/db.py +++ b/ingredients/db.py @@ -53,7 +53,7 @@ async def find_ingredient_by_id(conn, ingredient_id: int) -> Optional[Ingredient async with conn.execute(f''' SELECT {','.join(ingredient_keys + product_keys)} FROM Ingredient LEFT JOIN Product ON Ingredient.product_id = Product.id - WHERE id = ? + WHERE Ingredient.id = ? ''', (ingredient_id,)) as cursor: async for row in cursor: product_keys = {k:v for k,v in zip(Product.KEYS, row[len(Ingredient.KEYS):])} diff --git a/tests/test_ingredients.py b/tests/test_ingredients.py new file mode 100644 index 0000000..e5b4b08 --- /dev/null +++ b/tests/test_ingredients.py @@ -0,0 +1,593 @@ +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()