1
0
Fork 0
mirror of https://github.com/mealie-recipes/mealie.git synced 2025-08-06 05:55:23 +02:00

feat: Add Households to Mealie (#3970)

This commit is contained in:
Michael Genson 2024-08-22 10:14:32 -05:00 committed by GitHub
parent 0c29cef17d
commit eb170cc7e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
315 changed files with 6975 additions and 3577 deletions

View file

@ -0,0 +1,127 @@
import random
from dataclasses import dataclass
from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from pydantic import UUID4
from mealie.schema.cookbook.cookbook import ReadCookBook, SaveCookBook
from tests import utils
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
def get_page_data(group_id: UUID | str, household_id: UUID4 | str):
name_and_slug = random_string(10)
return {
"name": name_and_slug,
"slug": name_and_slug,
"description": "",
"position": 0,
"categories": [],
"group_id": str(group_id),
"household_id": str(household_id),
}
@dataclass
class TestCookbook:
id: UUID4
slug: str
name: str
data: dict
@pytest.fixture(scope="function")
def cookbooks(unique_user: TestUser) -> list[TestCookbook]:
database = unique_user.repos
data: list[ReadCookBook] = []
yield_data: list[TestCookbook] = []
for _ in range(3):
cb = database.cookbooks.create(SaveCookBook(**get_page_data(unique_user.group_id, unique_user.household_id)))
data.append(cb)
yield_data.append(TestCookbook(id=cb.id, slug=cb.slug, name=cb.name, data=cb.model_dump()))
yield yield_data
for cb in yield_data:
try:
database.cookbooks.delete(cb.id)
except Exception:
pass
def test_create_cookbook(api_client: TestClient, unique_user: TestUser):
page_data = get_page_data(unique_user.group_id, unique_user.household_id)
response = api_client.post(api_routes.households_cookbooks, json=page_data, headers=unique_user.token)
assert response.status_code == 201
def test_read_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
sample = random.choice(cookbooks)
response = api_client.get(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
assert response.status_code == 200
page_data = response.json()
assert page_data["id"] == str(sample.id)
assert page_data["slug"] == sample.slug
assert page_data["name"] == sample.name
assert page_data["groupId"] == str(unique_user.group_id)
def test_update_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
cookbook = random.choice(cookbooks)
update_data = get_page_data(unique_user.group_id, unique_user.household_id)
update_data["name"] = random_string(10)
response = api_client.put(
api_routes.households_cookbooks_item_id(cookbook.id), json=update_data, headers=unique_user.token
)
assert response.status_code == 200
response = api_client.get(api_routes.households_cookbooks_item_id(cookbook.id), headers=unique_user.token)
assert response.status_code == 200
page_data = response.json()
assert page_data["name"] == update_data["name"]
assert page_data["slug"] == update_data["name"]
def test_update_cookbooks_many(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
pages = [x.data for x in cookbooks]
reverse_order = sorted(pages, key=lambda x: x["position"], reverse=True)
for x, page in enumerate(reverse_order):
page["position"] = x
page["group_id"] = str(unique_user.group_id)
response = api_client.put(
api_routes.households_cookbooks, json=utils.jsonify(reverse_order), headers=unique_user.token
)
assert response.status_code == 200
response = api_client.get(api_routes.households_cookbooks, headers=unique_user.token)
assert response.status_code == 200
known_ids = [x.id for x in cookbooks]
server_ids = [x["id"] for x in response.json()["items"]]
for know in known_ids: # Hacky check, because other tests don't cleanup after themselves :(
assert str(know) in server_ids
def test_delete_cookbook(api_client: TestClient, unique_user: TestUser, cookbooks: list[TestCookbook]):
sample = random.choice(cookbooks)
response = api_client.delete(api_routes.households_cookbooks_item_id(sample.id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(api_routes.households_cookbooks_item_id(sample.slug), headers=unique_user.token)
assert response.status_code == 404

View file

@ -0,0 +1,76 @@
import pytest
from fastapi.testclient import TestClient
from tests.utils import api_routes
from tests.utils.factories import user_registration_factory
from tests.utils.fixture_schemas import TestUser
@pytest.fixture(scope="function")
def invite(api_client: TestClient, unique_user: TestUser) -> None:
# Test Creation
r = api_client.post(api_routes.households_invitations, json={"uses": 2}, headers=unique_user.token)
assert r.status_code == 201
invitation = r.json()
return invitation["token"]
def test_get_all_invitation(api_client: TestClient, unique_user: TestUser, invite: str) -> None:
# Get All Invites
r = api_client.get(api_routes.households_invitations, headers=unique_user.token)
assert r.status_code == 200
items = r.json()
assert len(items) == 1
for item in items:
assert item["groupId"] == unique_user.group_id
assert item["householdId"] == unique_user.household_id
assert item["token"] == invite
def register_user(api_client: TestClient, invite: str):
# Test User can Join Group
registration = user_registration_factory()
registration.group = ""
registration.household = ""
registration.group_token = invite
response = api_client.post(api_routes.users_register, json=registration.model_dump(by_alias=True))
return registration, response
def test_group_invitation_link(api_client: TestClient, unique_user: TestUser, invite: str):
registration, r = register_user(api_client, invite)
assert r.status_code == 201
# Login as new User
form_data = {"username": registration.email, "password": registration.password}
r = api_client.post(api_routes.auth_token, data=form_data)
assert r.status_code == 200
token = r.json().get("access_token")
assert token is not None
# Check user Group and Household match
r = api_client.get(api_routes.users_self, headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
assert r.json()["groupId"] == unique_user.group_id
assert r.json()["householdId"] == unique_user.household_id
def test_group_invitation_delete_after_uses(api_client: TestClient, invite: str) -> None:
# Register First User
_, r = register_user(api_client, invite)
assert r.status_code == 201
# Register Second User
_, r = register_user(api_client, invite)
assert r.status_code == 201
# Check Group Invitation is Deleted
_, r = register_user(api_client, invite)
assert r.status_code == 400

View file

@ -0,0 +1,169 @@
from datetime import datetime, timedelta, timezone
from fastapi.testclient import TestClient
from mealie.schema.meal_plan.new_meal import CreatePlanEntry
from tests.utils import api_routes
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
def route_all_slice(page: int, perPage: int, start_date: str, end_date: str):
return (
f"{api_routes.households_mealplans}?page={page}&perPage={perPage}&start_date={start_date}&end_date={end_date}"
)
def test_create_mealplan_no_recipe(api_client: TestClient, unique_user: TestUser):
title = random_string(length=25)
text = random_string(length=25)
new_plan = CreatePlanEntry(
date=datetime.now(timezone.utc).date(), entry_type="breakfast", title=title, text=text
).model_dump()
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
response = api_client.post(api_routes.households_mealplans, json=new_plan, headers=unique_user.token)
assert response.status_code == 201
response_json = response.json()
assert response_json["title"] == title
assert response_json["text"] == text
def test_create_mealplan_with_recipe(api_client: TestClient, unique_user: TestUser):
recipe_name = random_string(length=25)
response = api_client.post(api_routes.recipes, json={"name": recipe_name}, headers=unique_user.token)
assert response.status_code == 201
response = api_client.get(api_routes.recipes_slug(recipe_name), headers=unique_user.token)
recipe = response.json()
recipe_id = recipe["id"]
new_plan = CreatePlanEntry(
date=datetime.now(timezone.utc).date(), entry_type="dinner", recipe_id=recipe_id
).model_dump(by_alias=True)
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
new_plan["recipeId"] = str(recipe_id)
response = api_client.post(api_routes.households_mealplans, json=new_plan, headers=unique_user.token)
response_json = response.json()
assert response.status_code == 201
assert response_json["recipe"]["slug"] == recipe_name
def test_crud_mealplan(api_client: TestClient, unique_user: TestUser):
new_plan = CreatePlanEntry(
date=datetime.now(timezone.utc).date(),
entry_type="breakfast",
title=random_string(),
text=random_string(),
).model_dump()
# Create
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
response = api_client.post(api_routes.households_mealplans, json=new_plan, headers=unique_user.token)
response_json = response.json()
assert response.status_code == 201
plan_id = response_json["id"]
# Update
response_json["title"] = random_string()
response_json["text"] = random_string()
response = api_client.put(
api_routes.households_mealplans_item_id(plan_id), headers=unique_user.token, json=response_json
)
assert response.status_code == 200
assert response.json()["title"] == response_json["title"]
assert response.json()["text"] == response_json["text"]
# Delete
response = api_client.delete(api_routes.households_mealplans_item_id(plan_id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(api_routes.households_mealplans_item_id(plan_id), headers=unique_user.token)
assert response.status_code == 404
def test_get_all_mealplans(api_client: TestClient, unique_user: TestUser):
for _ in range(3):
new_plan = CreatePlanEntry(
date=datetime.now(timezone.utc).date(),
entry_type="breakfast",
title=random_string(),
text=random_string(),
).model_dump()
new_plan["date"] = datetime.now(timezone.utc).date().strftime("%Y-%m-%d")
response = api_client.post(api_routes.households_mealplans, json=new_plan, headers=unique_user.token)
assert response.status_code == 201
response = api_client.get(
api_routes.households_mealplans, headers=unique_user.token, params={"page": 1, "perPage": -1}
)
assert response.status_code == 200
assert len(response.json()["items"]) >= 3
def test_get_slice_mealplans(api_client: TestClient, unique_user: TestUser):
# Make List of 10 dates from now to +10 days
dates = [datetime.now(timezone.utc).date() + timedelta(days=x) for x in range(10)]
# Make a list of 10 meal plans
meal_plans = [
CreatePlanEntry(date=date, entry_type="breakfast", title=random_string(), text=random_string()).model_dump()
for date in dates
]
# Add the meal plans to the database
for meal_plan in meal_plans:
meal_plan["date"] = meal_plan["date"].strftime("%Y-%m-%d")
response = api_client.post(api_routes.households_mealplans, json=meal_plan, headers=unique_user.token)
assert response.status_code == 201
# Get meal slice of meal plans from database
slices = [dates, dates[1:2], dates[2:3], dates[3:4], dates[4:5]]
for date_range in slices:
start_date = date_range[0].strftime("%Y-%m-%d")
end_date = date_range[-1].strftime("%Y-%m-%d")
response = api_client.get(route_all_slice(1, -1, start_date, end_date), headers=unique_user.token)
assert response.status_code == 200
response_json = response.json()
for meal_plan in response_json["items"]:
assert meal_plan["date"] in [date.strftime("%Y-%m-%d") for date in date_range]
def test_get_mealplan_today(api_client: TestClient, unique_user: TestUser):
# Create Meal Plans for today
test_meal_plans = [
CreatePlanEntry(
date=datetime.now(timezone.utc).date(), entry_type="breakfast", title=random_string(), text=random_string()
).model_dump()
for _ in range(3)
]
# Add the meal plans to the database
for meal_plan in test_meal_plans:
meal_plan["date"] = meal_plan["date"].strftime("%Y-%m-%d")
response = api_client.post(api_routes.households_mealplans, json=meal_plan, headers=unique_user.token)
assert response.status_code == 201
# Get meal plan for today
response = api_client.get(api_routes.households_mealplans_today, headers=unique_user.token)
assert response.status_code == 200
response_json = response.json()
for meal_plan in response_json:
assert meal_plan["date"] == datetime.now(timezone.utc).date().strftime("%Y-%m-%d")

View file

@ -0,0 +1,133 @@
from uuid import UUID
import pytest
from fastapi.testclient import TestClient
from mealie.schema.meal_plan.plan_rules import PlanRulesOut
from mealie.schema.recipe.recipe import RecipeCategory
from mealie.schema.recipe.recipe_category import CategorySave
from tests import utils
from tests.utils import api_routes
from tests.utils.fixture_schemas import TestUser
@pytest.fixture(scope="function")
def category(unique_user: TestUser):
database = unique_user.repos
slug = utils.random_string(length=10)
model = database.categories.create(CategorySave(group_id=unique_user.group_id, slug=slug, name=slug))
yield model
try:
database.categories.delete(model.slug)
except Exception:
pass
@pytest.fixture(scope="function")
def plan_rule(api_client: TestClient, unique_user: TestUser):
payload = {
"groupId": unique_user.group_id,
"householdId": unique_user.household_id,
"day": "monday",
"entryType": "breakfast",
"categories": [],
}
response = api_client.post(
api_routes.households_mealplans_rules, json=utils.jsonify(payload), headers=unique_user.token
)
assert response.status_code == 201
plan_rule = PlanRulesOut.model_validate(response.json())
yield plan_rule
# cleanup
api_client.delete(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
def test_group_mealplan_rules_create(api_client: TestClient, unique_user: TestUser, category: RecipeCategory):
database = unique_user.repos
payload = {
"groupId": unique_user.group_id,
"householdId": unique_user.household_id,
"day": "monday",
"entryType": "breakfast",
"categories": [category.model_dump()],
}
response = api_client.post(
api_routes.households_mealplans_rules, json=utils.jsonify(payload), headers=unique_user.token
)
assert response.status_code == 201
# Validate the response data
response_data = response.json()
assert response_data["groupId"] == str(unique_user.group_id)
assert response_data["householdId"] == str(unique_user.household_id)
assert response_data["day"] == "monday"
assert response_data["entryType"] == "breakfast"
assert len(response_data["categories"]) == 1
assert response_data["categories"][0]["slug"] == category.slug
# Validate database entry
rule = database.group_meal_plan_rules.get_one(UUID(response_data["id"]))
assert rule
assert str(rule.group_id) == unique_user.group_id
assert str(rule.household_id) == unique_user.household_id
assert rule.day == "monday"
assert rule.entry_type == "breakfast"
assert len(rule.categories) == 1
assert rule.categories[0].slug == category.slug
# Cleanup
database.group_meal_plan_rules.delete(rule.id)
def test_group_mealplan_rules_read(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
response = api_client.get(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
assert response.status_code == 200
# Validate the response data
response_data = response.json()
assert response_data["id"] == str(plan_rule.id)
assert response_data["groupId"] == str(unique_user.group_id)
assert response_data["householdId"] == str(unique_user.household_id)
assert response_data["day"] == "monday"
assert response_data["entryType"] == "breakfast"
assert len(response_data["categories"]) == 0
def test_group_mealplan_rules_update(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
payload = {
"groupId": unique_user.group_id,
"householdId": unique_user.household_id,
"day": "tuesday",
"entryType": "lunch",
}
response = api_client.put(
api_routes.households_mealplans_rules_item_id(plan_rule.id), json=payload, headers=unique_user.token
)
assert response.status_code == 200
# Validate the response data
response_data = response.json()
assert response_data["id"] == str(plan_rule.id)
assert response_data["groupId"] == str(unique_user.group_id)
assert response_data["householdId"] == str(unique_user.household_id)
assert response_data["day"] == "tuesday"
assert response_data["entryType"] == "lunch"
assert len(response_data["categories"]) == 0
def test_group_mealplan_rules_delete(api_client: TestClient, unique_user: TestUser, plan_rule: PlanRulesOut):
response = api_client.get(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.delete(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(api_routes.households_mealplans_rules_item_id(plan_rule.id), headers=unique_user.token)
assert response.status_code == 404

View file

@ -0,0 +1,178 @@
from fastapi.testclient import TestClient
from mealie.schema.household.group_events import GroupEventNotifierCreate, GroupEventNotifierOptions
from mealie.services.event_bus_service.event_bus_listeners import AppriseEventListener
from mealie.services.event_bus_service.event_bus_service import Event
from mealie.services.event_bus_service.event_types import (
EventBusMessage,
EventDocumentDataBase,
EventDocumentType,
EventOperation,
EventTypes,
)
from tests.utils import api_routes
from tests.utils.assertion_helpers import assert_ignore_keys
from tests.utils.factories import random_bool, random_email, random_int, random_string
from tests.utils.fixture_schemas import TestUser
def preferences_generator():
return GroupEventNotifierOptions(
recipe_created=random_bool(),
recipe_updated=random_bool(),
recipe_deleted=random_bool(),
user_signup=random_bool(),
data_migrations=random_bool(),
data_export=random_bool(),
data_import=random_bool(),
mealplan_entry_created=random_bool(),
shopping_list_created=random_bool(),
shopping_list_updated=random_bool(),
shopping_list_deleted=random_bool(),
cookbook_created=random_bool(),
cookbook_updated=random_bool(),
cookbook_deleted=random_bool(),
tag_created=random_bool(),
tag_updated=random_bool(),
tag_deleted=random_bool(),
category_created=random_bool(),
category_updated=random_bool(),
category_deleted=random_bool(),
).model_dump(by_alias=True)
def notifier_generator():
return GroupEventNotifierCreate(
name=random_string(),
apprise_url=random_string(),
).model_dump(by_alias=True)
def event_generator():
return Event(
message=EventBusMessage(title=random_string(), body=random_string()),
event_type=EventTypes.test_message,
integration_id=random_string(),
document_data=EventDocumentDataBase(document_type=EventDocumentType.generic, operation=EventOperation.info),
)
def test_create_notification(api_client: TestClient, unique_user: TestUser):
payload = notifier_generator()
response = api_client.post(api_routes.households_events_notifications, json=payload, headers=unique_user.token)
assert response.status_code == 201
payload_as_dict = response.json()
assert payload_as_dict["name"] == payload["name"]
assert payload_as_dict["enabled"] is True
# Ensure Apprise URL Stays Private
assert "apprise_url" not in payload_as_dict
# Cleanup
response = api_client.delete(
api_routes.households_events_notifications_item_id(payload_as_dict["id"]), headers=unique_user.token
)
def test_ensure_apprise_url_is_secret(api_client: TestClient, unique_user: TestUser):
payload = notifier_generator()
response = api_client.post(api_routes.households_events_notifications, json=payload, headers=unique_user.token)
assert response.status_code == 201
payload_as_dict = response.json()
# Ensure Apprise URL Staysa Private
assert "apprise_url" not in payload_as_dict
def test_update_apprise_notification(api_client: TestClient, unique_user: TestUser):
payload = notifier_generator()
response = api_client.post(api_routes.households_events_notifications, json=payload, headers=unique_user.token)
assert response.status_code == 201
update_payload = response.json()
# Set Update Values
update_payload["name"] = random_string()
update_payload["enabled"] = random_bool()
update_payload["options"] = preferences_generator()
response = api_client.put(
api_routes.households_events_notifications_item_id(update_payload["id"]),
json=update_payload,
headers=unique_user.token,
)
assert response.status_code == 200
# Re-Get The Item
response = api_client.get(
api_routes.households_events_notifications_item_id(update_payload["id"]), headers=unique_user.token
)
assert response.status_code == 200
# Validate Updated Values
updated_payload = response.json()
assert updated_payload["name"] == update_payload["name"]
assert updated_payload["enabled"] == update_payload["enabled"]
assert_ignore_keys(updated_payload["options"], update_payload["options"])
# Cleanup
response = api_client.delete(
api_routes.households_events_notifications_item_id(update_payload["id"]), headers=unique_user.token
)
def test_delete_apprise_notification(api_client: TestClient, unique_user: TestUser):
payload = notifier_generator()
response = api_client.post(api_routes.households_events_notifications, json=payload, headers=unique_user.token)
assert response.status_code == 201
payload_as_dict = response.json()
response = api_client.delete(
api_routes.households_events_notifications_item_id(payload_as_dict["id"]), headers=unique_user.token
)
assert response.status_code == 204
response = api_client.get(
api_routes.households_events_notifications_item_id(payload_as_dict["id"]), headers=unique_user.token
)
assert response.status_code == 404
def test_apprise_event_bus_listener_functions():
test_event = event_generator()
test_standard_urls = [
"a" + random_string(),
f"ses://{random_email()}/{random_string()}/{random_string()}/us-east-1/",
f"pBUL://{random_string()}/{random_email()}",
]
test_custom_urls = [
"JSON://" + random_string(),
f"jsons://{random_string()}:my/pass/word@{random_string()}.com/{random_string()}",
"form://" + random_string(),
"fORMS://" + str(random_int()),
"xml:" + str(random_int()),
"xmls://" + random_string(),
]
# Validate all standard urls are not considered custom
responses = [AppriseEventListener.is_custom_url(url) for url in test_standard_urls]
assert not any(responses)
# Validate all custom urls are actually considered custom
responses = [AppriseEventListener.is_custom_url(url) for url in test_custom_urls]
assert all(responses)
updated_standard_urls = AppriseEventListener.update_urls_with_event_data(test_standard_urls, test_event)
updated_custom_urls = AppriseEventListener.update_urls_with_event_data(test_custom_urls, test_event)
# Validate that no URLs are lost when updating them
assert len(updated_standard_urls) == len(test_standard_urls)
assert len(updated_custom_urls) == len(updated_custom_urls)

View file

@ -0,0 +1,126 @@
import pytest
from fastapi.testclient import TestClient
from mealie.schema.household.group_recipe_action import (
CreateGroupRecipeAction,
GroupRecipeActionOut,
GroupRecipeActionType,
)
from tests.utils import api_routes, assert_deserialize
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def new_link_action() -> CreateGroupRecipeAction:
return CreateGroupRecipeAction(
action_type=GroupRecipeActionType.link,
title=random_string(),
url=random_string(),
)
def test_group_recipe_actions_create_one(api_client: TestClient, unique_user: TestUser):
action_in = new_link_action()
response = api_client.post(
api_routes.households_recipe_actions,
json=action_in.model_dump(),
headers=unique_user.token,
)
data = assert_deserialize(response, 201)
action_out = GroupRecipeActionOut(**data)
assert action_out.id
assert str(action_out.group_id) == unique_user.group_id
assert str(action_out.household_id) == unique_user.household_id
assert action_out.action_type == action_in.action_type
assert action_out.title == action_in.title
assert action_out.url == action_in.url
def test_group_recipe_actions_get_all(api_client: TestClient, unique_user: TestUser):
expected_ids: set[str] = set()
for _ in range(random_int(3, 5)):
response = api_client.post(
api_routes.households_recipe_actions,
json=new_link_action().model_dump(),
headers=unique_user.token,
)
data = assert_deserialize(response, 201)
expected_ids.add(data["id"])
response = api_client.get(api_routes.households_recipe_actions, headers=unique_user.token)
data = assert_deserialize(response, 200)
fetched_ids = {item["id"] for item in data["items"]}
for expected_id in expected_ids:
assert expected_id in fetched_ids
@pytest.mark.parametrize("is_own_group", [True, False])
def test_group_recipe_actions_get_one(
api_client: TestClient, unique_user: TestUser, g2_user: TestUser, is_own_group: bool
):
action_in = new_link_action()
response = api_client.post(
api_routes.households_recipe_actions,
json=action_in.model_dump(),
headers=unique_user.token,
)
data = assert_deserialize(response, 201)
expected_action_out = GroupRecipeActionOut(**data)
if is_own_group:
fetch_user = unique_user
else:
fetch_user = g2_user
response = api_client.get(
api_routes.households_recipe_actions_item_id(expected_action_out.id),
headers=fetch_user.token,
)
if not is_own_group:
assert response.status_code == 404
return
data = assert_deserialize(response, 200)
action_out = GroupRecipeActionOut(**data)
assert action_out == expected_action_out
def test_group_recipe_actions_update_one(api_client: TestClient, unique_user: TestUser):
action_in = new_link_action()
response = api_client.post(
api_routes.households_recipe_actions,
json=action_in.model_dump(),
headers=unique_user.token,
)
data = assert_deserialize(response, 201)
action_id = data["id"]
new_title = random_string()
data["title"] = new_title
response = api_client.put(
api_routes.households_recipe_actions_item_id(action_id),
json=data,
headers=unique_user.token,
)
data = assert_deserialize(response, 200)
updated_action = GroupRecipeActionOut(**data)
assert updated_action.title == new_title
def test_group_recipe_actions_delete_one(api_client: TestClient, unique_user: TestUser):
action_in = new_link_action()
response = api_client.post(
api_routes.households_recipe_actions,
json=action_in.model_dump(),
headers=unique_user.token,
)
data = assert_deserialize(response, 201)
action_id = data["id"]
response = api_client.delete(api_routes.households_recipe_actions_item_id(action_id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(api_routes.households_recipe_actions_item_id(action_id), headers=unique_user.token)
assert response.status_code == 404

View file

@ -0,0 +1,683 @@
import random
from math import ceil, floor
from uuid import uuid4
import pytest
from fastapi.testclient import TestClient
from pydantic import UUID4
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.household.group_shopping_list import ShoppingListItemOut, ShoppingListOut
from mealie.schema.recipe.recipe_ingredient import SaveIngredientFood
from tests import utils
from tests.utils import api_routes
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def create_item(list_id: UUID4, **kwargs) -> dict:
return {
"shopping_list_id": str(list_id),
"note": random_string(10),
"quantity": random_int(1, 10),
**kwargs,
}
def serialize_list_items(list_items: list[ShoppingListItemOut]) -> list:
as_dict = []
for item in list_items:
item_dict = item.model_dump(by_alias=True)
item_dict["shoppingListId"] = str(item.shopping_list_id)
item_dict["id"] = str(item.id)
as_dict.append(item_dict)
# the default serializer fails on certain complex objects, so we use FastAPI's serliazer first
as_dict = utils.jsonify(as_dict)
return as_dict
def test_shopping_list_items_create_one(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
) -> None:
item = create_item(shopping_list.id)
response = api_client.post(api_routes.households_shopping_items, json=item, headers=unique_user.token)
as_json = utils.assert_deserialize(response, 201)
assert len(as_json["createdItems"]) == 1
# Test Item is Getable
created_item_id = as_json["createdItems"][0]["id"]
response = api_client.get(
api_routes.households_shopping_items_item_id(created_item_id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(response, 200)
# Ensure List Id is Set
assert as_json["shoppingListId"] == str(shopping_list.id)
# Test Item In List
response = api_client.get(
api_routes.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
response_list = utils.assert_deserialize(response, 200)
assert len(response_list["listItems"]) == 1
# 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.households_shopping_items_create_bulk,
json=items,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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_auto_assign_label_with_food_without_label(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
):
database = unique_user.repos
food = database.ingredient_foods.create(SaveIngredientFood(name=random_string(10), group_id=unique_user.group_id))
item = create_item(shopping_list.id, food_id=str(food.id))
response = api_client.post(api_routes.households_shopping_items, json=item, headers=unique_user.token)
as_json = utils.assert_deserialize(response, 201)
assert len(as_json["createdItems"]) == 1
item_out = ShoppingListItemOut.model_validate(as_json["createdItems"][0])
assert item_out.label_id is None
assert item_out.label is None
def test_shopping_list_items_auto_assign_label_with_food_with_label(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
):
database = unique_user.repos
label = database.group_multi_purpose_labels.create({"name": random_string(10), "group_id": unique_user.group_id})
food = database.ingredient_foods.create(
SaveIngredientFood(name=random_string(10), group_id=unique_user.group_id, label_id=label.id)
)
item = create_item(shopping_list.id, food_id=str(food.id))
response = api_client.post(api_routes.households_shopping_items, json=item, headers=unique_user.token)
as_json = utils.assert_deserialize(response, 201)
assert len(as_json["createdItems"]) == 1
item_out = ShoppingListItemOut.model_validate(as_json["createdItems"][0])
assert item_out.label_id == label.id
assert item_out.label
assert item_out.label.id == label.id
@pytest.mark.parametrize("use_fuzzy_name", [True, False])
def test_shopping_list_items_auto_assign_label_with_food_search(
api_client: TestClient,
unique_user: TestUser,
shopping_list: ShoppingListOut,
use_fuzzy_name: bool,
):
database = unique_user.repos
label = database.group_multi_purpose_labels.create({"name": random_string(10), "group_id": unique_user.group_id})
food = database.ingredient_foods.create(
SaveIngredientFood(name=random_string(20), group_id=unique_user.group_id, label_id=label.id)
)
item = create_item(shopping_list.id)
name = food.name
if use_fuzzy_name:
name = name + random_string(2)
item["note"] = name
response = api_client.post(api_routes.households_shopping_items, json=item, headers=unique_user.token)
as_json = utils.assert_deserialize(response, 201)
assert len(as_json["createdItems"]) == 1
item_out = ShoppingListItemOut.model_validate(as_json["createdItems"][0])
assert item_out.label_id == label.id
assert item_out.label
assert item_out.label.id == label.id
def test_shopping_list_items_get_one(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
for _ in range(3):
item = random.choice(list_with_items.list_items)
response = api_client.get(api_routes.households_shopping_items_item_id(item.id), headers=unique_user.token)
assert response.status_code == 200
def test_shopping_list_items_get_all(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
params = {
"page": 1,
"perPage": -1,
"queryFilter": f"shopping_list_id={list_with_items.id}",
}
response = api_client.get(api_routes.households_shopping_items, params=params, headers=unique_user.token)
pagination_json = utils.assert_deserialize(response, 200)
assert len(pagination_json["items"]) == len(list_with_items.list_items)
def test_shopping_list_items_get_one_404(api_client: TestClient, unique_user: TestUser) -> None:
response = api_client.get(api_routes.households_shopping_items_item_id(uuid4()), headers=unique_user.token)
assert response.status_code == 404
def test_shopping_list_items_update_one(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
for _ in range(3):
item = random.choice(list_with_items.list_items)
item.note = random_string(10)
update_data = create_item(list_with_items.id)
update_data["id"] = str(item.id)
response = api_client.put(
api_routes.households_shopping_items_item_id(item.id),
json=update_data,
headers=unique_user.token,
)
item_json = utils.assert_deserialize(response, 200)
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.households_shopping_lists_item_id(list_with_items.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items_create_bulk,
json=items,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items,
json=as_json["createdItems"],
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.model_dump(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.households_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.households_shopping_lists_item_id(list_with_items.id),
headers=unique_user.token,
)
response_list = utils.assert_deserialize(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(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
item = random.choice(list_with_items.list_items)
# Delete Item
response = api_client.delete(api_routes.households_shopping_items_item_id(item.id), headers=unique_user.token)
assert response.status_code == 200
# Validate Get Item Returns 404
response = api_client.get(api_routes.households_shopping_items_item_id(item.id), headers=unique_user.token)
assert response.status_code == 404
def test_shopping_list_items_update_many_consolidates_common_items(
api_client: TestClient,
unique_user: TestUser,
list_with_items: ShoppingListOut,
) -> None:
list_items = list_with_items.list_items
master_note = random_string(10)
# set quantity and note to trigger consolidation
for li in list_items:
li.quantity = 1
li.note = master_note
# update list
response = api_client.put(
api_routes.households_shopping_items,
json=serialize_list_items(list_items),
headers=unique_user.token,
)
assert response.status_code == 200
# retrieve list and check positions against list
response = api_client.get(
api_routes.households_shopping_lists_item_id(list_with_items.id),
headers=unique_user.token,
)
response_list = utils.assert_deserialize(response, 200)
assert len(response_list["listItems"]) == 1
assert response_list["listItems"][0]["quantity"] == len(list_items)
assert response_list["listItems"][0]["note"] == master_note
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.households_shopping_items_create_bulk,
json=items + duplicate_items,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items, json=new_item, headers=unique_user.token)
item_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
list_json = utils.assert_deserialize(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.model_dump() for item in list_with_items.list_items])
response = api_client.put(api_routes.households_shopping_items, json=payload, headers=unique_user.token)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(list_with_items.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items, json=payload, headers=unique_user.token)
as_json = utils.assert_deserialize(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.households_shopping_items_item_id(checked_item.id),
json=utils.jsonify(checked_item.model_dump()),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items_item_id(reference_item.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(response, 200)
reference_item_get = ShoppingListItemOut.model_validate(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.households_shopping_lists_item_id(list_with_items.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(response, 200)
updated_list = ShoppingListOut.model_validate(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.households_shopping_items,
json=utils.jsonify([item_1.model_dump(), item_2.model_dump()]),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items_create_bulk,
json=normal_items + zero_qty_items,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items,
json=new_item_to_merge,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_items_item_id(update_item_to_merge["id"]),
json=update_item_to_merge,
headers=unique_user.token,
)
as_json = utils.assert_deserialize(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.households_shopping_lists_item_id(shopping_list.id),
headers=unique_user.token,
)
as_json = utils.assert_deserialize(response, 200)
assert len(as_json["listItems"]) == len(normal_items + zero_qty_items) - 1
def test_shopping_list_item_extras(
api_client: TestClient, unique_user: TestUser, shopping_list: ShoppingListOut
) -> None:
key_str_1 = random_string()
val_str_1 = random_string()
key_str_2 = random_string()
val_str_2 = random_string()
# create an item with extras
new_item_data = create_item(shopping_list.id)
new_item_data["extras"] = {key_str_1: val_str_1}
response = api_client.post(api_routes.households_shopping_items, json=new_item_data, headers=unique_user.token)
collection = utils.assert_deserialize(response, 201)
item_as_json = collection["createdItems"][0]
# make sure the extra persists
extras = item_as_json["extras"]
assert key_str_1 in extras
assert extras[key_str_1] == val_str_1
# add more extras to the item
item_as_json["extras"][key_str_2] = val_str_2
response = api_client.put(
api_routes.households_shopping_items_item_id(item_as_json["id"]),
json=item_as_json,
headers=unique_user.token,
)
collection = utils.assert_deserialize(response, 200)
item_as_json = collection["updatedItems"][0]
# make sure both the new extra and original extra persist
extras = item_as_json["extras"]
assert key_str_1 in extras
assert key_str_2 in extras
assert extras[key_str_1] == val_str_1
assert extras[key_str_2] == val_str_2

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,86 @@
from datetime import datetime, timezone
import pytest
from fastapi.testclient import TestClient
from tests.utils import api_routes, assert_deserialize, jsonify
from tests.utils.fixture_schemas import TestUser
@pytest.fixture()
def webhook_data():
return {
"enabled": True,
"name": "Test-Name",
"url": "https://my-fake-url.com",
"time": "00:00",
"scheduledTime": datetime.now(timezone.utc),
}
def test_create_webhook(api_client: TestClient, unique_user: TestUser, webhook_data):
response = api_client.post(
api_routes.households_webhooks,
json=jsonify(webhook_data),
headers=unique_user.token,
)
assert response.status_code == 201
def test_read_webhook(api_client: TestClient, unique_user: TestUser, webhook_data):
response = api_client.post(
api_routes.households_webhooks,
json=jsonify(webhook_data),
headers=unique_user.token,
)
item_id = response.json()["id"]
response = api_client.get(api_routes.households_webhooks_item_id(item_id), headers=unique_user.token)
webhook = assert_deserialize(response, 200)
assert webhook["id"] == item_id
assert webhook["name"] == webhook_data["name"]
assert webhook["url"] == webhook_data["url"]
assert webhook["scheduledTime"] == str(webhook_data["scheduledTime"].astimezone(timezone.utc).time())
assert webhook["enabled"] == webhook_data["enabled"]
def test_update_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
response = api_client.post(
api_routes.households_webhooks,
json=jsonify(webhook_data),
headers=unique_user.token,
)
item_dict = assert_deserialize(response, 201)
item_id = item_dict["id"]
webhook_data["name"] = "My New Name"
webhook_data["url"] = "https://my-new-fake-url.com"
webhook_data["enabled"] = False
response = api_client.put(
api_routes.households_webhooks_item_id(item_id),
json=jsonify(webhook_data),
headers=unique_user.token,
)
updated_webhook = assert_deserialize(response, 200)
assert updated_webhook["name"] == webhook_data["name"]
assert updated_webhook["url"] == webhook_data["url"]
assert updated_webhook["enabled"] == webhook_data["enabled"]
def test_delete_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
response = api_client.post(
api_routes.households_webhooks,
json=jsonify(webhook_data),
headers=unique_user.token,
)
item_dict = assert_deserialize(response, 201)
item_id = item_dict["id"]
response = api_client.delete(api_routes.households_webhooks_item_id(item_id), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(api_routes.households_webhooks_item_id(item_id), headers=unique_user.token)
assert response.status_code == 404

View file

@ -0,0 +1,47 @@
from fastapi.testclient import TestClient
from mealie.schema.household.household_preferences import UpdateHouseholdPreferences
from tests.utils import api_routes
from tests.utils.assertion_helpers import assert_ignore_keys
from tests.utils.factories import random_bool
from tests.utils.fixture_schemas import TestUser
def test_get_preferences(api_client: TestClient, unique_user: TestUser) -> None:
response = api_client.get(api_routes.households_preferences, headers=unique_user.token)
assert response.status_code == 200
preferences = response.json()
assert preferences["recipePublic"] in {True, False}
assert preferences["recipeShowNutrition"] in {True, False}
def test_preferences_in_household(api_client: TestClient, unique_user: TestUser) -> None:
response = api_client.get(api_routes.households_self, headers=unique_user.token)
assert response.status_code == 200
household = response.json()
assert household["preferences"] is not None
# Spot Check
assert household["preferences"]["recipePublic"] in {True, False}
assert household["preferences"]["recipeShowNutrition"] in {True, False}
def test_update_preferences(api_client: TestClient, unique_user: TestUser) -> None:
new_data = UpdateHouseholdPreferences(recipe_public=random_bool(), recipe_show_nutrition=random_bool())
response = api_client.put(api_routes.households_preferences, json=new_data.model_dump(), headers=unique_user.token)
assert response.status_code == 200
preferences = response.json()
assert preferences is not None
assert preferences["recipePublic"] == new_data.recipe_public
assert preferences["recipeShowNutrition"] == new_data.recipe_show_nutrition
assert_ignore_keys(new_data.model_dump(by_alias=True), preferences, ["id", "householdId"])

View file

@ -0,0 +1,87 @@
from uuid import uuid4
from fastapi.testclient import TestClient
from tests.utils import api_routes
from tests.utils.factories import random_bool
from tests.utils.fixture_schemas import TestUser
def get_permissions_payload(user_id: str, can_manage=None) -> dict:
return {
"user_id": user_id,
"can_manage": random_bool() if can_manage is None else can_manage,
"can_invite": random_bool(),
"can_organize": random_bool(),
}
def test_set_member_permissions(api_client: TestClient, user_tuple: list[TestUser]):
usr_1, usr_2 = user_tuple
# Set Acting User
acting_user = usr_1.repos.users.get_one(usr_1.user_id)
assert acting_user
acting_user.can_manage = True
usr_1.repos.users.update(acting_user.id, acting_user)
payload = get_permissions_payload(str(usr_2.user_id))
# Test
response = api_client.put(api_routes.households_permissions, json=payload, headers=usr_1.token)
assert response.status_code == 200
def test_set_member_permissions_unauthorized(api_client: TestClient, unique_user: TestUser):
database = unique_user.repos
# Setup
user = database.users.get_one(unique_user.user_id)
assert user
user.can_manage = False
database.users.update(user.id, user)
payload = get_permissions_payload(str(user.id))
payload = {
"user_id": str(user.id),
"can_manage": True,
"can_invite": True,
"can_organize": True,
}
# Test
response = api_client.put(api_routes.households_permissions, json=payload, headers=unique_user.token)
assert response.status_code == 403
def test_set_member_permissions_other_household(
api_client: TestClient,
unique_user: TestUser,
h2_user: TestUser,
):
database = unique_user.repos
user = database.users.get_one(unique_user.user_id)
assert user
user.can_manage = True
database.users.update(user.id, user)
payload = get_permissions_payload(str(h2_user.user_id))
response = api_client.put(api_routes.households_permissions, json=payload, headers=unique_user.token)
assert response.status_code == 403
def test_set_member_permissions_no_user(
api_client: TestClient,
unique_user: TestUser,
):
database = unique_user.repos
user = database.users.get_one(unique_user.user_id)
assert user
user.can_manage = True
database.users.update(user.id, user)
payload = get_permissions_payload(str(uuid4()))
response = api_client.put(api_routes.households_permissions, json=payload, headers=unique_user.token)
assert response.status_code == 404

View file

@ -0,0 +1,20 @@
from fastapi.testclient import TestClient
from tests.utils import api_routes
from tests.utils.fixture_schemas import TestUser
def test_get_household_members(api_client: TestClient, user_tuple: list[TestUser], h2_user: TestUser):
usr_1, usr_2 = user_tuple
response = api_client.get(api_routes.households_members, headers=usr_1.token)
assert response.status_code == 200
members = response.json()
assert len(members) >= 2
all_ids = [x["id"] for x in members]
assert str(usr_1.user_id) in all_ids
assert str(usr_2.user_id) in all_ids
assert str(h2_user.user_id) not in all_ids

View file

@ -0,0 +1,234 @@
import random
from fastapi.testclient import TestClient
from mealie.repos.repository_factory import AllRepositories
from mealie.schema.household.group_shopping_list import ShoppingListOut
from mealie.schema.labels.multi_purpose_label import MultiPurposeLabelOut
from mealie.services.seeder.seeder_service import SeederService
from tests.utils import api_routes, jsonify
from tests.utils.factories import random_int, random_string
from tests.utils.fixture_schemas import TestUser
def create_labels(api_client: TestClient, unique_user: TestUser, count: int = 10) -> list[MultiPurposeLabelOut]:
labels: list[MultiPurposeLabelOut] = []
for _ in range(count):
response = api_client.post(api_routes.groups_labels, json={"name": random_string()}, headers=unique_user.token)
labels.append(MultiPurposeLabelOut.model_validate(response.json()))
return labels
def test_new_list_creates_list_labels(api_client: TestClient, unique_user: TestUser):
labels = create_labels(api_client, unique_user)
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
assert len(new_list.label_settings) == len(labels)
label_settings_label_ids = [setting.label_id for setting in new_list.label_settings]
for label in labels:
assert label.id in label_settings_label_ids
def test_new_label_creates_list_labels(api_client: TestClient, unique_user: TestUser):
# create a list with some labels
create_labels(api_client, unique_user)
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
existing_label_settings = new_list.label_settings
# create more labels and make sure they were added to the list's label settings
new_labels = create_labels(api_client, unique_user)
response = api_client.get(api_routes.households_shopping_lists_item_id(new_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.model_validate(response.json())
updated_label_settings = updated_list.label_settings
assert len(updated_label_settings) == len(existing_label_settings) + len(new_labels)
label_settings_ids = [setting.id for setting in updated_list.label_settings]
for label_setting in existing_label_settings:
assert label_setting.id in label_settings_ids
label_settings_label_ids = [setting.label_id for setting in updated_list.label_settings]
for label in new_labels:
assert label.id in label_settings_label_ids
def test_new_label_creates_list_labels_in_all_households(
api_client: TestClient, unique_user: TestUser, h2_user: TestUser
):
# unique_user and h2_user are in the same group, so these labels should be for both of them
create_labels(api_client, unique_user)
# create a list with some labels for each user
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list_h1 = ShoppingListOut.model_validate(response.json())
existing_label_settings_h1 = new_list_h1.label_settings
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=h2_user.token
)
new_list_h2 = ShoppingListOut.model_validate(response.json())
existing_label_settings_h2 = new_list_h2.label_settings
# create more labels and make sure they were added to both lists' label settings
new_labels = create_labels(api_client, unique_user)
for user, new_list, existing_label_settings in [
(unique_user, new_list_h1, existing_label_settings_h1),
(h2_user, new_list_h2, existing_label_settings_h2),
]:
response = api_client.get(api_routes.households_shopping_lists_item_id(new_list.id), headers=user.token)
updated_list = ShoppingListOut.model_validate(response.json())
updated_label_settings = updated_list.label_settings
assert len(updated_label_settings) == len(existing_label_settings) + len(new_labels)
label_settings_ids = [setting.id for setting in updated_list.label_settings]
for label_setting in existing_label_settings:
assert label_setting.id in label_settings_ids
label_settings_label_ids = [setting.label_id for setting in updated_list.label_settings]
for label in new_labels:
assert label.id in label_settings_label_ids
def test_seed_label_creates_list_labels(api_client: TestClient, unique_user: TestUser):
CREATED_LABELS = 21
database = unique_user.repos
# create a list with some labels
create_labels(api_client, unique_user)
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
existing_label_settings = new_list.label_settings
# seed labels and make sure they were added to the list's label settings
group = database.groups.get_one(unique_user.group_id)
assert group
database = AllRepositories(database.session, group_id=group.id)
seeder = SeederService(database)
seeder.seed_labels("en-US")
response = api_client.get(api_routes.households_shopping_lists_item_id(new_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.model_validate(response.json())
updated_label_settings = updated_list.label_settings
assert len(updated_label_settings) == len(existing_label_settings) + CREATED_LABELS
label_settings_ids = [setting.id for setting in updated_list.label_settings]
for label_setting in existing_label_settings:
assert label_setting.id in label_settings_ids
def test_delete_label_deletes_list_labels(api_client: TestClient, unique_user: TestUser):
new_labels = create_labels(api_client, unique_user)
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
existing_label_settings = new_list.label_settings
label_to_delete = random.choice(new_labels)
api_client.delete(api_routes.groups_labels_item_id(label_to_delete.id), headers=unique_user.token)
response = api_client.get(api_routes.households_shopping_lists_item_id(new_list.id), headers=unique_user.token)
updated_list = ShoppingListOut.model_validate(response.json())
assert len(updated_list.label_settings) == len(existing_label_settings) - 1
label_settings_label_ids = [setting.label_id for setting in updated_list.label_settings]
for label in new_labels:
if label.id == label_to_delete.id:
assert label.id not in label_settings_label_ids
else:
assert label.id in label_settings_label_ids
def test_update_list_doesnt_change_list_labels(api_client: TestClient, unique_user: TestUser):
create_labels(api_client, unique_user)
original_name = random_string()
updated_name = random_string()
response = api_client.post(
api_routes.households_shopping_lists, json={"name": original_name}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
assert new_list.name == original_name
assert new_list.label_settings
updated_list_data = new_list.model_dump()
updated_list_data.pop("created_at", None)
updated_list_data.pop("updated_at", None)
updated_list_data["name"] = updated_name
updated_list_data["label_settings"][0]["position"] = random_int(999, 9999)
response = api_client.put(
api_routes.households_shopping_lists_item_id(new_list.id),
json=jsonify(updated_list_data),
headers=unique_user.token,
)
updated_list = ShoppingListOut.model_validate(response.json())
assert updated_list.name == updated_name
assert updated_list.label_settings == new_list.label_settings
def test_update_list_labels(api_client: TestClient, unique_user: TestUser):
create_labels(api_client, unique_user)
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
changed_setting = random.choice(new_list.label_settings)
changed_setting.position = random_int(999, 9999)
response = api_client.put(
api_routes.households_shopping_lists_item_id_label_settings(new_list.id),
json=jsonify(new_list.label_settings),
headers=unique_user.token,
)
updated_list = ShoppingListOut.model_validate(response.json())
original_settings_by_id = {setting.id: setting for setting in new_list.label_settings}
for setting in updated_list.label_settings:
assert setting.id in original_settings_by_id
assert original_settings_by_id[setting.id].shopping_list_id == setting.shopping_list_id
assert original_settings_by_id[setting.id].label_id == setting.label_id
if setting.id == changed_setting.id:
assert setting.position == changed_setting.position
else:
assert original_settings_by_id[setting.id].position == setting.position
def test_list_label_order(api_client: TestClient, unique_user: TestUser):
response = api_client.post(
api_routes.households_shopping_lists, json={"name": random_string()}, headers=unique_user.token
)
new_list = ShoppingListOut.model_validate(response.json())
for i, setting in enumerate(new_list.label_settings):
if not i:
continue
assert setting.position > new_list.label_settings[i - 1].position
random.shuffle(new_list.label_settings)
response = api_client.put(
api_routes.households_shopping_lists_item_id_label_settings(new_list.id),
json=jsonify(new_list.label_settings),
headers=unique_user.token,
)
updated_list = ShoppingListOut.model_validate(response.json())
for i, setting in enumerate(updated_list.label_settings):
if not i:
continue
assert setting.position > updated_list.label_settings[i - 1].position