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:
parent
3415a9c310
commit
617cc1fdfb
18 changed files with 1398 additions and 576 deletions
|
@ -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"]
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue