1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-03 04:25:24 +02:00

Refactor Shopping List API (#2021)

* tidied up shopping list item models
redefined recipe refs and updated models
added calculated display attribute to unify shopping list item rendering
added validation to use a food's label if an item's label is null

* fixed schema reference

* refactored shopping list item service
route all operations through one central method to account for edgecases
return item collections for all operations to account for merging
consolidate recipe items before sending them to the shopping list

* made fractions prettier

* replaced redundant display text util

* fixed edgecase for zero quantity items on a recipe

* fix for pre-merging recipe ingredients

* fixed edgecase for merging create_items together

* fixed bug with merged updated items creating dupes

* added test for self-removing recipe ref

* update items are now merged w/ existing items

* refactored service to make it easier to read

* added a lot of tests

* made it so checked items are never merged

* fixed bug with dragging + re-ordering

* fix for postgres cascade issue

* added prevalidator to recipe ref to avoid db error
This commit is contained in:
Michael Genson 2023-01-28 18:45:02 -06:00 committed by GitHub
parent 3415a9c310
commit 617cc1fdfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1398 additions and 576 deletions

View file

@ -1,31 +1,22 @@
import random
from math import ceil, floor
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from pydantic import UUID4
from mealie.schema.group.group_shopping_list import ShoppingListItemOut, ShoppingListOut
from tests import utils
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def create_item(list_id: UUID4) -> dict:
return {
"shopping_list_id": str(list_id),
"checked": False,
"position": 0,
"is_food": False,
"note": random_string(10),
"quantity": 1,
"unit_id": None,
"unit": None,
"food_id": None,
"food": None,
"recipe_id": None,
"label_id": None,
"quantity": random_int(1, 10),
}
@ -49,9 +40,10 @@ def test_shopping_list_items_create_one(
response = api_client.post(api_routes.groups_shopping_items, json=item, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == 1
# Test Item is Getable
created_item_id = as_json["id"]
created_item_id = as_json["createdItems"][0]["id"]
response = api_client.get(api_routes.groups_shopping_items_item_id(created_item_id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
@ -64,10 +56,39 @@ def test_shopping_list_items_create_one(
assert len(response_list["listItems"]) == 1
# Check Item Id's
# Check Item Ids
assert response_list["listItems"][0]["id"] == created_item_id
def test_shopping_list_items_create_many(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
) -> None:
items = [create_item(shopping_list.id) for _ in range(10)]
response = api_client.post(api_routes.groups_shopping_items_create_bulk, json=items, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == len(items)
assert len(as_json["updatedItems"]) == 0
assert len(as_json["deletedItems"]) == 0
# test items in list
created_item_ids = [item["id"] for item in as_json["createdItems"]]
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
# make sure the list is the correct size
assert len(as_json["listItems"]) == len(items)
for item in as_json["listItems"]:
# Ensure List Id is Set
assert item["shoppingListId"] == str(shopping_list.id)
assert item["id"] in created_item_ids
created_item_ids.remove(item["id"])
# make sure we found all items
assert not created_item_ids
def test_shopping_list_items_get_one(
api_client: TestClient,
unique_user: TestUser,
@ -103,7 +124,82 @@ def test_shopping_list_items_update_one(
api_routes.groups_shopping_items_item_id(item.id), json=update_data, headers=unique_user.token
)
item_json = utils.assert_derserialize(response, 200)
assert item_json["note"] == update_data["note"]
assert len(item_json["createdItems"]) == 0
assert len(item_json["updatedItems"]) == 1
assert len(item_json["deletedItems"]) == 0
assert item_json["updatedItems"][0]["note"] == update_data["note"]
assert item_json["updatedItems"][0]["quantity"] == update_data["quantity"]
# make sure the list didn't change sizes
response = api_client.get(api_routes.groups_shopping_lists_item_id(list_with_items.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(list_with_items.list_items)
def test_shopping_list_items_update_many(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
) -> None:
# create a bunch of items
items = [create_item(shopping_list.id) for _ in range(10)]
for item in items:
item["quantity"] += 10
response = api_client.post(api_routes.groups_shopping_items_create_bulk, json=items, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == len(items)
# update the items and compare values
item_quantity_map = {}
for update_item in as_json["createdItems"]:
update_item["quantity"] += random_int(-5, 5)
item_quantity_map[update_item["id"]] = update_item["quantity"]
response = api_client.put(api_routes.groups_shopping_items, json=as_json["createdItems"], headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["updatedItems"]) == len(items)
for updated_item in as_json["updatedItems"]:
assert item_quantity_map[updated_item["id"]] == updated_item["quantity"]
# make sure the list didn't change sizes
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(items)
def test_shopping_list_items_update_many_reorder(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
list_items = list_with_items.list_items
# reorder list in random order
random.shuffle(list_items)
# update item posiitons and serialize
as_dict = []
for i, item in enumerate(list_items):
item.position = i
item_dict = item.dict(by_alias=True)
item_dict["shoppingListId"] = str(list_with_items.id)
item_dict["id"] = str(item.id)
as_dict.append(item_dict)
# update list
# the default serializer fails on certain complex objects, so we use FastAPI's serializer first
as_dict = utils.jsonify(as_dict)
response = api_client.put(api_routes.groups_shopping_items, json=as_dict, headers=unique_user.token)
assert response.status_code == 200
# retrieve list and check positions against list
response = api_client.get(api_routes.groups_shopping_lists_item_id(list_with_items.id), headers=unique_user.token)
response_list = utils.assert_derserialize(response, 200)
for i, item_data in enumerate(response_list["listItems"]):
assert item_data["position"] == i
assert item_data["id"] == str(list_items[i].id)
def test_shopping_list_items_delete_one(
@ -122,44 +218,6 @@ def test_shopping_list_items_delete_one(
assert response.status_code == 404
def test_shopping_list_items_update_many(api_client: TestClient, unique_user: TestUser) -> None:
assert True
def test_shopping_list_items_update_many_reorder(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
list_items = list_with_items.list_items
# reorder list in random order
random.shuffle(list_items)
# update List posiitons and serialize
as_dict = []
for i, item in enumerate(list_items):
item.position = i
item_dict = item.dict(by_alias=True)
item_dict["shoppingListId"] = str(list_with_items.id)
item_dict["id"] = str(item.id)
as_dict.append(item_dict)
# update list
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
as_dict = utils.jsonify(as_dict)
response = api_client.put(api_routes.groups_shopping_items, json=as_dict, headers=unique_user.token)
assert response.status_code == 200
# retrieve list and check positions against list
response = api_client.get(api_routes.groups_shopping_lists_item_id(list_with_items.id), headers=unique_user.token)
response_list = utils.assert_derserialize(response, 200)
for i, item_data in enumerate(response_list["listItems"]):
assert item_data["position"] == i
assert item_data["id"] == str(list_items[i].id)
def test_shopping_list_items_update_many_consolidates_common_items(
api_client: TestClient,
unique_user: TestUser,
@ -189,14 +247,250 @@ def test_shopping_list_items_update_many_consolidates_common_items(
assert response_list["listItems"][0]["note"] == master_note
@pytest.mark.skip("TODO: Implement")
def test_shopping_list_items_update_many_remove_recipe_with_other_items(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
# list_items = list_with_items.list_items
pass
def test_shopping_list_items_add_mergeable(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
):
# add a bunch of items that can be consolidated
items = [create_item(shopping_list.id) for _ in range(5)]
common_note = random_string()
duplicate_items = [create_item(shopping_list.id) for _ in range(5)]
for item in duplicate_items:
item["note"] = common_note
merged_qty = sum([item["quantity"] for item in duplicate_items]) # type: ignore
response = api_client.post(
api_routes.groups_shopping_items_create_bulk, json=items + duplicate_items, headers=unique_user.token
)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == len(items) + 1
assert len(as_json["updatedItems"]) == 0
assert len(as_json["deletedItems"]) == 0
found = False
for item in as_json["createdItems"]:
if item["note"] == common_note:
assert item["quantity"] == merged_qty
found = True
break
assert found
# add more items that can be merged into the existing items
item_to_merge_into = random.choice(as_json["createdItems"])
new_item = create_item(shopping_list.id)
new_item["note"] = item_to_merge_into["note"]
updated_quantity = new_item["quantity"] + item_to_merge_into["quantity"]
response = api_client.post(api_routes.groups_shopping_items, json=new_item, headers=unique_user.token)
item_json = utils.assert_derserialize(response, 201)
# we should have received an updated item, not a created item
assert len(item_json["createdItems"]) == 0
assert len(item_json["updatedItems"]) == 1
assert len(item_json["deletedItems"]) == 0
assert item_json["updatedItems"][0]["id"] == item_to_merge_into["id"]
assert item_json["updatedItems"][0]["quantity"] == updated_quantity
# fetch the list and make sure we have the correct number of items
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
list_json = utils.assert_derserialize(response, 200)
assert len(list_json["listItems"]) == len(as_json["createdItems"])
def test_shopping_list_items_update_mergable(
api_client: TestClient, unique_user: TestUser, list_with_items: ShoppingListOut
):
# update every other item so it merges into the previous item
for i, item in enumerate(list_with_items.list_items):
if not i % 2:
continue
item.note = list_with_items.list_items[i - 1].note
payload = utils.jsonify([item.dict() for item in list_with_items.list_items])
response = api_client.put(api_routes.groups_shopping_items, json=payload, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == ceil(len(list_with_items.list_items) / 2)
assert len(as_json["deletedItems"]) == floor(len(list_with_items.list_items) / 2)
# check that every other item was updated, and its quantity matches the sum of itself and the previous item
for i, item in enumerate(list_with_items.list_items):
if not i % 2:
continue
assert (
as_json["updatedItems"][floor(i / 2)]["quantity"]
== item.quantity + list_with_items.list_items[i - 1].quantity
)
# confirm the number of items on the list matches
response = api_client.get(api_routes.groups_shopping_lists_item_id(list_with_items.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
updated_list_items = as_json["listItems"]
assert len(updated_list_items) == ceil(len(list_with_items.list_items) / 2)
# update two of the items so they merge into each other
new_note = random_string()
items_to_merge = random.sample(updated_list_items, 2)
for item_data in items_to_merge:
item_data["note"] = new_note
merged_quantity = sum([item["quantity"] for item in items_to_merge])
payload = utils.jsonify(items_to_merge)
response = api_client.put(api_routes.groups_shopping_items, json=payload, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == 1
assert len(as_json["deletedItems"]) == 1
assert as_json["deletedItems"][0]["id"] in [item["id"] for item in items_to_merge]
found = False
for item_data in as_json["updatedItems"]:
if item_data["id"] not in [item["id"] for item in items_to_merge]:
continue
assert item_data["quantity"] == merged_quantity
found = True
break
assert found
def test_shopping_list_items_checked_off(
api_client: TestClient, unique_user: TestUser, list_with_items: ShoppingListOut
):
# rename an item to match another item and check it off, and make sure it does not affect the other item
checked_item, reference_item = random.sample(list_with_items.list_items, 2)
checked_item.note = reference_item.note
checked_item.checked = True
response = api_client.put(
api_routes.groups_shopping_items_item_id(checked_item.id),
json=utils.jsonify(checked_item.dict()),
headers=unique_user.token,
)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == 1
assert len(as_json["deletedItems"]) == 0
updated_item = as_json["updatedItems"][0]
assert updated_item["checked"]
# get the reference item and make sure it didn't change
response = api_client.get(api_routes.groups_shopping_items_item_id(reference_item.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
reference_item_get = ShoppingListItemOut.parse_obj(as_json)
assert reference_item_get.id == reference_item.id
assert reference_item_get.shopping_list_id == reference_item.shopping_list_id
assert reference_item_get.note == reference_item.note
assert reference_item_get.quantity == reference_item.quantity
assert reference_item_get.checked == reference_item.checked
# rename an item to match another item and check both off, and make sure they are not merged
response = api_client.get(api_routes.groups_shopping_lists_item_id(list_with_items.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
updated_list = ShoppingListOut.parse_obj(as_json)
item_1, item_2 = random.sample(updated_list.list_items, 2)
item_1.checked = True
item_2.checked = True
item_2.note = item_1.note
response = api_client.put(
api_routes.groups_shopping_items,
json=utils.jsonify([item_1.dict(), item_2.dict()]),
headers=unique_user.token,
)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == 2
assert len(as_json["deletedItems"]) == 0
updated_items_map = {item["id"]: item for item in as_json["updatedItems"]}
for item in [item_1, item_2]:
updated_item_data = updated_items_map[str(item.id)]
assert item.note == updated_item_data["note"]
assert item.quantity == updated_item_data["quantity"]
assert updated_item_data["checked"]
def test_shopping_list_items_with_zero_quantity(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
):
# add a bunch of items, some with zero quantity, and make sure they persist
normal_items = [create_item(shopping_list.id) for _ in range(10)]
zero_qty_items = [create_item(shopping_list.id) for _ in range(10)]
for item in zero_qty_items:
item["quantity"] = 0
response = api_client.post(
api_routes.groups_shopping_items_create_bulk, json=normal_items + zero_qty_items, headers=unique_user.token
)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == len(normal_items + zero_qty_items)
# confirm the number of items on the list matches
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
created_items = as_json["listItems"]
assert len(created_items) == len(normal_items + zero_qty_items)
# add another zero quantity item so it merges into the existing item
new_item_to_merge = create_item(shopping_list.id)
new_item_to_merge["quantity"] = 0
target_item = random.choice(created_items)
new_item_to_merge["note"] = target_item["note"]
response = api_client.post(api_routes.groups_shopping_items, json=new_item_to_merge, headers=unique_user.token)
as_json = utils.assert_derserialize(response, 201)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == 1
assert len(as_json["deletedItems"]) == 0
updated_item = as_json["updatedItems"][0]
assert updated_item["id"] == target_item["id"]
assert updated_item["note"] == target_item["note"]
assert updated_item["quantity"] == target_item["quantity"]
# confirm the number of items on the list stayed the same
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items)
# update an existing item to zero quantity and make sure it merges into the existing item
update_item_to_merge, target_item = random.sample(as_json["listItems"], 2)
update_item_to_merge["note"] = target_item["note"]
update_item_to_merge["quantity"] = 0
response = api_client.put(
api_routes.groups_shopping_items_item_id(update_item_to_merge["id"]),
json=update_item_to_merge,
headers=unique_user.token,
)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["createdItems"]) == 0
assert len(as_json["updatedItems"]) == 1
assert len(as_json["deletedItems"]) == 1
assert as_json["deletedItems"][0]["id"] == update_item_to_merge["id"]
updated_item = as_json["updatedItems"][0]
assert updated_item["id"] == target_item["id"]
assert updated_item["note"] == target_item["note"]
assert updated_item["quantity"] == target_item["quantity"]
# confirm the number of items on the list shrunk by one
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items) - 1
def test_shopping_list_item_extras(
@ -213,7 +507,8 @@ def test_shopping_list_item_extras(
new_item_data["extras"] = {key_str_1: val_str_1}
response = api_client.post(api_routes.groups_shopping_items, json=new_item_data, headers=unique_user.token)
item_as_json = utils.assert_derserialize(response, 201)
collection = utils.assert_derserialize(response, 201)
item_as_json = collection["createdItems"][0]
# make sure the extra persists
extras = item_as_json["extras"]
@ -226,7 +521,8 @@ def test_shopping_list_item_extras(
response = api_client.put(
api_routes.groups_shopping_items_item_id(item_as_json["id"]), json=item_as_json, headers=unique_user.token
)
item_as_json = utils.assert_derserialize(response, 200)
collection = utils.assert_derserialize(response, 200)
item_as_json = collection["updatedItems"][0]
# make sure both the new extra and original extra persist
extras = item_as_json["extras"]

View file

@ -91,7 +91,6 @@ def test_shopping_lists_add_recipe(
recipe_ingredient_only: Recipe,
):
sample_list = random.choice(shopping_lists)
recipe = recipe_ingredient_only
response = api_client.post(
@ -99,24 +98,185 @@ def test_shopping_lists_add_recipe(
)
assert response.status_code == 200
# Get List and Check for Ingredients
# get list and verify items against ingredients
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(recipe.recipe_ingredient)
known_ingredients = {ingredient.note: ingredient for ingredient in recipe.recipe_ingredient}
for item in as_json["listItems"]:
assert item["note"] in known_ingredients
ingredient = known_ingredients[item["note"]]
assert item["quantity"] == (ingredient.quantity or 0)
# check recipe reference was added with quantity 1
refs = as_json["recipeReferences"]
assert len(refs) == 1
assert refs[0]["recipeId"] == str(recipe.id)
assert refs[0]["recipeQuantity"] == 1
# add the recipe again and check the resulting items
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id), headers=unique_user.token
)
assert response.status_code == 200
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(recipe.recipe_ingredient)
known_ingredients = [ingredient.note for ingredient in recipe.recipe_ingredient]
for item in as_json["listItems"]:
assert item["note"] in known_ingredients
# Check Recipe Reference was added with quantity 1
refs = item["recipeReferences"]
ingredient = known_ingredients[item["note"]]
assert item["quantity"] == (ingredient.quantity or 0) * 2
refs = as_json["recipeReferences"]
assert len(refs) == 1
assert refs[0]["recipeId"] == str(recipe.id)
assert refs[0]["recipeQuantity"] == 2
def test_shopping_lists_add_one_with_zero_quantity(
api_client: TestClient,
unique_user: TestUser,
shopping_lists: list[ShoppingListOut],
):
shopping_list = random.choice(shopping_lists)
# build a recipe that has some ingredients with a null quantity
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
recipe_slug = utils.assert_derserialize(response, 201)
response = api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token)
recipe_data = utils.assert_derserialize(response, 200)
ingredient_1 = {"quantity": random_int(1, 10), "note": random_string()}
ingredient_2 = {"quantity": random_int(1, 10), "note": random_string()}
ingredient_3_null_qty = {"quantity": None, "note": random_string()}
recipe_data["recipeIngredient"] = [ingredient_1, ingredient_2, ingredient_3_null_qty]
response = api_client.put(f"{api_routes.recipes}/{recipe_slug}", json=recipe_data, headers=unique_user.token)
utils.assert_derserialize(response, 200)
recipe = Recipe.parse_raw(api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token).content)
assert recipe.id
assert len(recipe.recipe_ingredient) == 3
# add the recipe to the list and make sure there are three list items
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
headers=unique_user.token,
)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
shopping_list_out = ShoppingListOut.parse_obj(utils.assert_derserialize(response, 200))
assert len(shopping_list_out.list_items) == 3
found = False
for item in shopping_list_out.list_items:
if item.note != ingredient_3_null_qty["note"]:
continue
found = True
assert item.quantity == 0
assert found
def test_shopping_list_ref_removes_itself(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut, recipe_ingredient_only: Recipe
):
# add a recipe to a list, then check off all recipe items and make sure the recipe ref is deleted
recipe = recipe_ingredient_only
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
headers=unique_user.token,
)
utils.assert_derserialize(response, 200)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
shopping_list_json = utils.assert_derserialize(response, 200)
assert len(shopping_list_json["listItems"]) == len(recipe.recipe_ingredient)
assert len(shopping_list_json["recipeReferences"]) == 1
for item in shopping_list_json["listItems"]:
item["checked"] = True
response = api_client.put(
api_routes.groups_shopping_items, json=shopping_list_json["listItems"], headers=unique_user.token
)
utils.assert_derserialize(response, 200)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
shopping_list_json = utils.assert_derserialize(response, 200)
assert len(shopping_list_json["recipeReferences"]) == 0
def test_shopping_lists_add_recipe_with_merge(
api_client: TestClient,
unique_user: TestUser,
shopping_lists: list[ShoppingListOut],
):
shopping_list = random.choice(shopping_lists)
# build a recipe that has some ingredients more than once
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
recipe_slug = utils.assert_derserialize(response, 201)
response = api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token)
recipe_data = utils.assert_derserialize(response, 200)
ingredient_1 = {"quantity": random_int(1, 10), "note": random_string()}
ingredient_2 = {"quantity": random_int(1, 10), "note": random_string()}
ingredient_duplicate_1 = {"quantity": random_int(1, 10), "note": random_string()}
ingredient_duplicate_2 = {"quantity": random_int(1, 10), "note": ingredient_duplicate_1["note"]}
recipe_data["recipeIngredient"] = [ingredient_1, ingredient_2, ingredient_duplicate_1, ingredient_duplicate_2]
response = api_client.put(f"{api_routes.recipes}/{recipe_slug}", json=recipe_data, headers=unique_user.token)
utils.assert_derserialize(response, 200)
recipe = Recipe.parse_raw(api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token).content)
assert recipe.id
assert len(recipe.recipe_ingredient) == 4
# add the recipe to the list and make sure there are only three list items, and their quantities/refs are correct
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
headers=unique_user.token,
)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
shopping_list_out = ShoppingListOut.parse_obj(utils.assert_derserialize(response, 200))
assert len(shopping_list_out.list_items) == 3
found_item_1 = False
found_item_2 = False
found_duplicate_item = False
for list_item in shopping_list_out.list_items:
assert len(list_item.recipe_references) == 1
ref = list_item.recipe_references[0]
assert ref.recipe_scale == 1
assert ref.recipe_quantity == list_item.quantity
if list_item.note == ingredient_1["note"]:
assert list_item.quantity == ingredient_1["quantity"]
found_item_1 = True
elif list_item.note == ingredient_2["note"]:
assert list_item.quantity == ingredient_2["quantity"]
found_item_2 = True
elif list_item.note == ingredient_duplicate_1["note"]:
combined_quantity = ingredient_duplicate_1["quantity"] + ingredient_duplicate_2["quantity"] # type: ignore
assert list_item.quantity == combined_quantity
found_duplicate_item = True
assert all([found_item_1, found_item_2, found_duplicate_item])
def test_shopping_list_add_recipe_scale(
@ -182,32 +342,49 @@ def test_shopping_lists_remove_recipe(
recipe_ingredient_only: Recipe,
):
sample_list = random.choice(shopping_lists)
recipe = recipe_ingredient_only
# add two instances of the recipe
payload = {"recipeIncrementQuantity": 2}
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id), headers=unique_user.token
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(sample_list.id, recipe.id),
json=payload,
headers=unique_user.token,
)
assert response.status_code == 200
# Get List and Check for Ingredients
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(recipe.recipe_ingredient)
known_ingredients = [ingredient.note for ingredient in recipe.recipe_ingredient]
for item in as_json["listItems"]:
assert item["note"] in known_ingredients
# Remove Recipe
# remove one instance of the recipe
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id_delete(sample_list.id, recipe.id),
headers=unique_user.token,
)
assert response.status_code == 200
# get list and verify items against ingredients
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == len(recipe.recipe_ingredient)
known_ingredients = {ingredient.note: ingredient for ingredient in recipe.recipe_ingredient}
for item in as_json["listItems"]:
assert item["note"] in known_ingredients
ingredient = known_ingredients[item["note"]]
assert item["quantity"] == (ingredient.quantity or 0)
# check recipe reference was reduced to 1
refs = as_json["recipeReferences"]
assert len(refs) == 1
assert refs[0]["recipeId"] == str(recipe.id)
assert refs[0]["recipeQuantity"] == 1
# remove the recipe again and check if the list is empty
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id_delete(sample_list.id, recipe.id),
headers=unique_user.token,
)
assert response.status_code == 200
# Get List and Check for Ingredients
response = api_client.get(api_routes.groups_shopping_lists_item_id(sample_list.id), headers=unique_user.token)
as_json = utils.assert_derserialize(response, 200)
assert len(as_json["listItems"]) == 0
@ -221,7 +398,6 @@ def test_shopping_lists_remove_recipe_multiple_quantity(
recipe_ingredient_only: Recipe,
):
sample_list = random.choice(shopping_lists)
recipe = recipe_ingredient_only
for _ in range(3):
@ -357,13 +533,14 @@ def test_recipe_decrement_max(
# next add a little bit more of one item
item_additional_quantity = random_int(1, 10)
item_json = as_json["listItems"][0]
item_json = random.choice(as_json["listItems"])
item_json["quantity"] += item_additional_quantity
response = api_client.put(
api_routes.groups_shopping_items_item_id(item["id"]), json=item_json, headers=unique_user.token
api_routes.groups_shopping_items_item_id(item_json["id"]), json=item_json, headers=unique_user.token
)
item_json = utils.assert_derserialize(response, 200)
as_json = utils.assert_derserialize(response, 200)
item_json = as_json["updatedItems"][0]
assert item_json["quantity"] == recipe_scale + item_additional_quantity
# now remove way too many instances of the recipe
@ -386,6 +563,105 @@ def test_recipe_decrement_max(
assert len(item["recipeReferences"]) == 0
def test_recipe_manipulation_with_zero_quantities(
api_client: TestClient,
unique_user: TestUser,
shopping_lists: list[ShoppingListOut],
):
shopping_list = random.choice(shopping_lists)
# create a recipe with one item that has a quantity of zero
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
recipe_slug = utils.assert_derserialize(response, 201)
response = api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token)
recipe_data = utils.assert_derserialize(response, 200)
note_with_zero_quantity = random_string()
recipe_data["recipeIngredient"] = [
{"quantity": random_int(1, 10), "note": random_string()},
{"quantity": random_int(1, 10), "note": random_string()},
{"quantity": random_int(1, 10), "note": random_string()},
{"quantity": 0, "note": note_with_zero_quantity},
]
response = api_client.put(f"{api_routes.recipes}/{recipe_slug}", json=recipe_data, headers=unique_user.token)
utils.assert_derserialize(response, 200)
recipe = Recipe.parse_raw(api_client.get(f"{api_routes.recipes}/{recipe_slug}", headers=unique_user.token).content)
assert recipe.id
assert len(recipe.recipe_ingredient) == 4
# add the recipe to the list twice and make sure the quantity is still zero
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
headers=unique_user.token,
)
utils.assert_derserialize(response, 200)
response = api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id(shopping_list.id, recipe.id),
headers=unique_user.token,
)
utils.assert_derserialize(response, 200)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.parse_raw(response.content)
assert len(updated_list.list_items) == 4
found = False
for item in updated_list.list_items:
if item.note != note_with_zero_quantity:
continue
assert item.quantity == 0
recipe_ref = item.recipe_references[0]
assert recipe_ref.recipe_scale == 2
found = True
break
if not found:
raise Exception("Did not find item with no quantity in shopping list")
# remove the recipe once and make sure the item is still on the list
api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id_delete(shopping_list.id, recipe.id),
headers=unique_user.token,
)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.parse_raw(response.content)
assert len(updated_list.list_items) == 4
found = False
for item in updated_list.list_items:
if item.note != note_with_zero_quantity:
continue
assert item.quantity == 0
recipe_ref = item.recipe_references[0]
assert recipe_ref.recipe_scale == 1
found = True
break
if not found:
raise Exception("Did not find item with no quantity in shopping list")
# remove the recipe one more time and make sure the item is gone and the list is empty
api_client.post(
api_routes.groups_shopping_lists_item_id_recipe_recipe_id_delete(shopping_list.id, recipe.id),
headers=unique_user.token,
)
response = api_client.get(api_routes.groups_shopping_lists_item_id(shopping_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.parse_raw(response.content)
assert len(updated_list.list_items) == 0
def test_shopping_list_extras(
api_client: TestClient,
unique_user: TestUser,