mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-08-05 13:35:23 +02:00
Refactor/conver to controllers (#923)
* add dependency injection for get_repositories * convert events api to controller * update generic typing * add abstract controllers * update test naming * migrate admin services to controllers * add additional admin route tests * remove print * add public shared dependencies * add types * fix typo * add static variables for recipe json keys * add coverage gutters config * update controller routers * add generic success response * add category/tag/tool tests * add token refresh test * add coverage utilities * covert comments to controller * add todo * add helper properties * delete old service * update test notes * add unit test for pretty_stats * remove dead code from post_webhooks * update group routes to use controllers * add additional group test coverage * abstract common permission checks * convert ingredient parser to controller * update recipe crud to use controller * remove dead-code * add class lifespan tracker for debugging * convert bulk export to controller * migrate tools router to controller * update recipe share to controller * move customer router to _base * ignore prints in flake8 * convert units and foods to new controllers * migrate user routes to controllers * centralize error handling * fix invalid ref * reorder fields * update routers to share common handling * update tests * remove prints * fix cookbooks delete * fix cookbook get * add controller for mealplanner * cover report routes to controller * remove __future__ imports * remove dead code * remove all base_http children and remove dead code
This commit is contained in:
parent
5823a32daf
commit
c4540f1395
164 changed files with 3111 additions and 3213 deletions
|
@ -0,0 +1,160 @@
|
|||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import sqlalchemy
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.core.dependencies.dependencies import validate_file_token
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.recipe.recipe_bulk_actions import ExportTypes
|
||||
from mealie.schema.recipe.recipe_category import TagIn
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
create_recipes = "/api/recipes"
|
||||
|
||||
bulk_tag = "api/recipes/bulk-actions/tag"
|
||||
bulk_categorize = "api/recipes/bulk-actions/categorize"
|
||||
bulk_delete = "api/recipes/bulk-actions/delete"
|
||||
|
||||
bulk_export = "api/recipes/bulk-actions/export"
|
||||
bulk_export_download = bulk_export + "/download"
|
||||
bulk_export_purge = bulk_export + "/purge"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ten_slugs(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> list[str]:
|
||||
|
||||
slugs = []
|
||||
|
||||
for _ in range(10):
|
||||
payload = {"name": random_string(length=20)}
|
||||
response = api_client.post(Routes.create_recipes, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
response_data = response.json()
|
||||
slugs.append(response_data)
|
||||
|
||||
yield slugs
|
||||
|
||||
for slug in slugs:
|
||||
try:
|
||||
database.recipes.delete(slug)
|
||||
except sqlalchemy.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
|
||||
def test_bulk_tag_recipes(
|
||||
api_client: TestClient, unique_user: TestUser, database: AllRepositories, ten_slugs: list[str]
|
||||
):
|
||||
# Setup Tags
|
||||
tags = []
|
||||
for _ in range(3):
|
||||
tag_name = random_string()
|
||||
tag = database.tags.create(TagIn(name=tag_name))
|
||||
tags.append(tag.dict())
|
||||
|
||||
payload = {"recipes": ten_slugs, "tags": tags}
|
||||
|
||||
response = api_client.post(Routes.bulk_tag, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate Recipes are Tagged
|
||||
for slug in ten_slugs:
|
||||
recipe = database.recipes.get_one(slug)
|
||||
|
||||
for tag in recipe.tags:
|
||||
assert tag.slug in [x["slug"] for x in tags]
|
||||
|
||||
|
||||
def test_bulk_categorize_recipes(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
ten_slugs: list[str],
|
||||
):
|
||||
# Setup Tags
|
||||
categories = []
|
||||
for _ in range(3):
|
||||
cat_name = random_string()
|
||||
cat = database.tags.create(TagIn(name=cat_name))
|
||||
categories.append(cat.dict())
|
||||
|
||||
payload = {"recipes": ten_slugs, "categories": categories}
|
||||
|
||||
response = api_client.post(Routes.bulk_categorize, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate Recipes are Categorized
|
||||
for slug in ten_slugs:
|
||||
recipe = database.recipes.get_one(slug)
|
||||
|
||||
for cat in recipe.recipe_category:
|
||||
assert cat.slug in [x["slug"] for x in categories]
|
||||
|
||||
|
||||
def test_bulk_delete_recipes(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
ten_slugs: list[str],
|
||||
):
|
||||
|
||||
payload = {"recipes": ten_slugs}
|
||||
|
||||
response = api_client.post(Routes.bulk_delete, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate Recipes are Tagged
|
||||
for slug in ten_slugs:
|
||||
recipe = database.recipes.get_one(slug)
|
||||
assert recipe is None
|
||||
|
||||
|
||||
def test_bulk_export_recipes(api_client: TestClient, unique_user: TestUser, ten_slugs: list[str]):
|
||||
payload = {
|
||||
"recipes": ten_slugs,
|
||||
"export_type": ExportTypes.JSON.value,
|
||||
}
|
||||
|
||||
response = api_client.post(Routes.bulk_export, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 202
|
||||
|
||||
# Get All Exports Available
|
||||
response = api_client.get(Routes.bulk_export, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 1
|
||||
|
||||
export_path = response_data[0]["path"]
|
||||
|
||||
# Get Export Token
|
||||
response = api_client.get(f"{Routes.bulk_export_download}?path={export_path}", headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
assert validate_file_token(response_data["fileToken"]) == Path(export_path)
|
||||
|
||||
# Use Export Token to donwload export
|
||||
response = api_client.get("/api/utils/download?token=" + response_data["fileToken"])
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Smoke Test to check that a file was downloaded
|
||||
assert response.headers["Content-Type"] == "application/octet-stream"
|
||||
assert len(response.content) > 0
|
||||
|
||||
# Purge Export
|
||||
response = api_client.delete(Routes.bulk_export_purge, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate Export was purged
|
||||
response = api_client.get(Routes.bulk_export, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 0
|
|
@ -152,3 +152,17 @@ def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
|
|||
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
|
||||
response = api_client.delete(recipe_url, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_recipe_crud_404(api_client: TestClient, api_routes: AppRoutes, unique_user: TestUser):
|
||||
response = api_client.put(api_routes.recipes_recipe_slug("test"), json={"test": "stest"}, headers=unique_user.token)
|
||||
assert response.status_code == 404
|
||||
|
||||
response = api_client.get(api_routes.recipes_recipe_slug("test"), headers=unique_user.token)
|
||||
assert response.status_code == 404
|
||||
|
||||
response = api_client.delete(api_routes.recipes_recipe_slug("test"), headers=unique_user.token)
|
||||
assert response.status_code == 404
|
||||
|
||||
response = api_client.patch(api_routes.recipes_create_url, json={"test": "stest"}, headers=unique_user.token)
|
||||
assert response.status_code == 404
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import pytest
|
||||
import sqlalchemy
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
create_recipes = "/api/recipes"
|
||||
|
||||
def base(item_id: int) -> str:
|
||||
return f"api/users/{item_id}/favorites"
|
||||
|
||||
def toggle(item_id: int, slug: str) -> str:
|
||||
return f"{Routes.base(item_id)}/{slug}"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def ten_slugs(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> list[str]:
|
||||
|
||||
slugs = []
|
||||
|
||||
for _ in range(10):
|
||||
payload = {"name": random_string(length=20)}
|
||||
response = api_client.post(Routes.create_recipes, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
response_data = response.json()
|
||||
slugs.append(response_data)
|
||||
|
||||
yield slugs
|
||||
|
||||
for slug in slugs:
|
||||
try:
|
||||
database.recipes.delete(slug)
|
||||
except sqlalchemy.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
|
||||
def test_recipe_favorites(api_client: TestClient, unique_user: TestUser, ten_slugs: list[str]):
|
||||
# Check that the user has no favorites
|
||||
response = api_client.get(Routes.base(unique_user.user_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["favoriteRecipes"] == []
|
||||
|
||||
# Add a few recipes to the user's favorites
|
||||
for slug in ten_slugs:
|
||||
response = api_client.post(Routes.toggle(unique_user.user_id, slug), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check that the user has the recipes in their favorites
|
||||
response = api_client.get(Routes.base(unique_user.user_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()["favoriteRecipes"]) == 10
|
||||
|
||||
# Remove a few recipes from the user's favorites
|
||||
for slug in ten_slugs[:5]:
|
||||
response = api_client.delete(Routes.toggle(unique_user.user_id, slug), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check that the user has the recipes in their favorites
|
||||
response = api_client.get(Routes.base(unique_user.user_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()["favoriteRecipes"]) == 5
|
|
@ -0,0 +1,47 @@
|
|||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.schema.recipe.recipe_ingredient import RegisteredParser
|
||||
from tests.unit_tests.test_ingredient_parser import TestIngredient, crf_exists, test_ingredients
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
ingredient = "/api/parser/ingredient"
|
||||
ingredients = "/api/parser/ingredients"
|
||||
|
||||
|
||||
def assert_ingredient(api_response: dict, test_ingredient: TestIngredient):
|
||||
assert api_response["ingredient"]["quantity"] == test_ingredient.quantity
|
||||
assert api_response["ingredient"]["unit"]["name"] == test_ingredient.unit
|
||||
assert api_response["ingredient"]["food"]["name"] == test_ingredient.food
|
||||
assert api_response["ingredient"]["note"] == test_ingredient.comments
|
||||
|
||||
|
||||
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
|
||||
@pytest.mark.parametrize("test_ingredient", test_ingredients)
|
||||
def test_recipe_ingredient_parser_nlp(api_client: TestClient, test_ingredient: TestIngredient, unique_user: TestUser):
|
||||
payload = {"parser": RegisteredParser.nlp, "ingredient": test_ingredient.input}
|
||||
response = api_client.post(Routes.ingredient, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert_ingredient(response.json(), test_ingredient)
|
||||
|
||||
|
||||
@pytest.mark.skipif(not crf_exists(), reason="CRF++ not installed")
|
||||
def test_recipe_ingredients_parser_nlp(api_client: TestClient, unique_user: TestUser):
|
||||
payload = {"parser": RegisteredParser.nlp, "ingredients": [x.input for x in test_ingredients]}
|
||||
response = api_client.post(Routes.ingredients, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
for api_ingredient, test_ingredient in zip(response.json(), test_ingredients):
|
||||
assert_ingredient(api_ingredient, test_ingredient)
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: Implement")
|
||||
def test_recipe_ingredient_parser_brute(api_client: TestClient):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skip("TODO: Implement")
|
||||
def test_recipe_ingredients_parser_brute(api_client: TestClient):
|
||||
pass
|
|
@ -0,0 +1,125 @@
|
|||
import pytest
|
||||
import sqlalchemy
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.schema.recipe.recipe_share_token import RecipeShareTokenSave
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
base = "/api/shared/recipes"
|
||||
create_recipes = "/api/recipes"
|
||||
|
||||
@staticmethod
|
||||
def item(item_id: str):
|
||||
return f"{Routes.base}/{item_id}"
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def slug(api_client: TestClient, unique_user: TestUser, database: AllRepositories) -> str:
|
||||
|
||||
payload = {"name": random_string(length=20)}
|
||||
response = api_client.post(Routes.create_recipes, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
yield response_data
|
||||
|
||||
try:
|
||||
database.recipes.delete(response_data)
|
||||
except sqlalchemy.exc.NoResultFound:
|
||||
pass
|
||||
|
||||
|
||||
def test_recipe_share_tokens_get_all(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
slug: str,
|
||||
):
|
||||
# Create 5 Tokens
|
||||
recipe = database.recipes.get_one(slug)
|
||||
tokens = []
|
||||
for _ in range(5):
|
||||
token = database.recipe_share_tokens.create(
|
||||
RecipeShareTokenSave(recipe_id=recipe.id, group_id=unique_user.group_id)
|
||||
)
|
||||
tokens.append(token)
|
||||
|
||||
# Get All Tokens
|
||||
response = api_client.get(Routes.base, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
assert len(response_data) == 5
|
||||
|
||||
|
||||
def test_recipe_share_tokens_get_all_with_id(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
slug: str,
|
||||
):
|
||||
# Create 5 Tokens
|
||||
recipe = database.recipes.get_one(slug)
|
||||
tokens = []
|
||||
for _ in range(3):
|
||||
token = database.recipe_share_tokens.create(
|
||||
RecipeShareTokenSave(recipe_id=recipe.id, group_id=unique_user.group_id)
|
||||
)
|
||||
tokens.append(token)
|
||||
|
||||
response = api_client.get(Routes.base + "?recipe_id=" + str(recipe.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
|
||||
assert len(response_data) == 3
|
||||
|
||||
|
||||
def test_recipe_share_tokens_create_and_get_one(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
slug: str,
|
||||
):
|
||||
recipe = database.recipes.get_one(slug)
|
||||
|
||||
payload = {
|
||||
"recipe_id": recipe.id,
|
||||
}
|
||||
|
||||
response = api_client.post(Routes.base, json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
response = api_client.get(Routes.item(response.json()["id"]), json=payload, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
response_data = response.json()
|
||||
assert response_data["recipe"]["id"] == recipe.id
|
||||
|
||||
|
||||
def test_recipe_share_tokens_delete_one(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
database: AllRepositories,
|
||||
slug: str,
|
||||
):
|
||||
# Create Token
|
||||
recipe = database.recipes.get_one(slug)
|
||||
|
||||
token = database.recipe_share_tokens.create(
|
||||
RecipeShareTokenSave(recipe_id=recipe.id, group_id=unique_user.group_id)
|
||||
)
|
||||
|
||||
# Delete Token
|
||||
response = api_client.delete(Routes.item(token.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Get Token
|
||||
token = database.recipe_share_tokens.get_one(token.id)
|
||||
|
||||
assert token is None
|
|
@ -1,120 +0,0 @@
|
|||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
class Routes:
|
||||
base = "/api/tools"
|
||||
recipes = "/api/recipes"
|
||||
|
||||
def item(item_id: int) -> str:
|
||||
return f"{Routes.base}/{item_id}"
|
||||
|
||||
def recipe(recipe_id: int) -> str:
|
||||
return f"{Routes.recipes}/{recipe_id}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestRecipeTool:
|
||||
id: int
|
||||
name: str
|
||||
slug: str
|
||||
on_hand: bool
|
||||
recipes: list
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def tool(api_client: TestClient, unique_user: TestUser) -> TestRecipeTool:
|
||||
data = {"name": random_string(10)}
|
||||
|
||||
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
as_json = response.json()
|
||||
|
||||
yield TestRecipeTool(
|
||||
id=as_json["id"],
|
||||
name=data["name"],
|
||||
slug=as_json["slug"],
|
||||
on_hand=as_json["onHand"],
|
||||
recipes=[],
|
||||
)
|
||||
|
||||
try:
|
||||
response = api_client.delete(Routes.item(response.json()["id"]), headers=unique_user.token)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def test_create_tool(api_client: TestClient, unique_user: TestUser):
|
||||
data = {"name": random_string(10)}
|
||||
|
||||
response = api_client.post(Routes.base, json=data, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
|
||||
def test_read_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
||||
response = api_client.get(Routes.item(tool.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
as_json = response.json()
|
||||
|
||||
assert as_json["id"] == tool.id
|
||||
assert as_json["name"] == tool.name
|
||||
|
||||
|
||||
def test_update_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
||||
update_data = {
|
||||
"id": tool.id,
|
||||
"name": random_string(10),
|
||||
"slug": tool.slug,
|
||||
"on_hand": True,
|
||||
}
|
||||
|
||||
response = api_client.put(Routes.item(tool.id), json=update_data, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
as_json = response.json()
|
||||
|
||||
assert as_json["id"] == tool.id
|
||||
assert as_json["name"] == update_data["name"]
|
||||
|
||||
|
||||
def test_delete_tool(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
||||
response = api_client.delete(Routes.item(tool.id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_recipe_tool_association(api_client: TestClient, tool: TestRecipeTool, unique_user: TestUser):
|
||||
# Setup Recipe
|
||||
recipe_data = {"name": random_string(10)}
|
||||
|
||||
response = api_client.post(Routes.recipes, json=recipe_data, headers=unique_user.token)
|
||||
|
||||
slug = response.json()
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
# Get Recipe Data
|
||||
response = api_client.get(Routes.recipe(slug), headers=unique_user.token)
|
||||
|
||||
as_json = response.json()
|
||||
|
||||
as_json["tools"] = [{"id": tool.id, "name": tool.name, "slug": tool.slug}]
|
||||
|
||||
# Update Recipe
|
||||
response = api_client.put(Routes.recipe(slug), json=as_json, headers=unique_user.token)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
# Get Recipe Data
|
||||
response = api_client.get(Routes.recipe(slug), headers=unique_user.token)
|
||||
|
||||
as_json = response.json()
|
||||
|
||||
assert as_json["tools"][0]["id"] == tool.id
|
Loading…
Add table
Add a link
Reference in a new issue