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

feat(backend): start multi-tenant support (WIP) (#680)

* fix ts types

* feat(code-generation): ♻️ update code-generation formats

* new scope

* add step button

* fix linter error

* update code-generation tags

* feat(backend):  start multi-tenant support

* feat(backend):  group invitation token generation and signup

* refactor(backend): ♻️ move group admin actions to admin router

* set url base to include `/admin`

* feat(frontend):  generate user sign-up links

* test(backend):  refactor test-suite to further decouple tests (WIP)

* feat(backend): 🐛 assign owner on backup import for recipes

* fix(backend): 🐛 assign recipe owner on migration from other service

Co-authored-by: hay-kot <hay-kot@pm.me>
This commit is contained in:
Hayden 2021-09-09 08:51:29 -08:00 committed by GitHub
parent 3c504e7048
commit bdaf758712
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
90 changed files with 1793 additions and 949 deletions

View file

@ -11,6 +11,8 @@ from mealie.db.db_setup import SessionLocal, generate_session
from mealie.db.init_db import main
from tests.app_routes import AppRoutes
from tests.test_config import TEST_DATA
from tests.utils.factories import random_email, random_string, user_registration_factory
from tests.utils.fixture_schemas import TestUser
from tests.utils.recipe_data import get_raw_no_image, get_raw_recipe, get_recipe_test_cases
main()
@ -62,12 +64,46 @@ def admin_token(api_client: requests, api_routes: AppRoutes):
return login(form_data, api_client, api_routes)
@fixture(scope="session")
def g2_user(admin_token, api_client: requests, api_routes: AppRoutes):
# Create the user
create_data = {
"fullName": random_string(),
"username": random_string(),
"email": random_email(),
"password": "useruser",
"group": "New Group",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.groups, json={"name": "New Group"}, headers=admin_token)
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
assert response.status_code == 201
# Log in as this user
form_data = {"username": create_data["email"], "password": "useruser"}
token = login(form_data, api_client, api_routes)
self_response = api_client.get(api_routes.users_self, headers=token)
assert self_response.status_code == 200
user_id = json.loads(self_response.text).get("id")
group_id = json.loads(self_response.text).get("groupId")
return TestUser(user_id=user_id, group_id=group_id, token=token)
@fixture(scope="session")
def user_token(admin_token, api_client: requests, api_routes: AppRoutes):
# Create the user
create_data = {
"fullName": "User",
"email": "user@email.com",
"fullName": random_string(),
"username": random_string(),
"email": random_email(),
"password": "useruser",
"group": "Home",
"admin": False,
@ -79,7 +115,7 @@ def user_token(admin_token, api_client: requests, api_routes: AppRoutes):
assert response.status_code == 201
# Log in as this user
form_data = {"username": "user@email.com", "password": "useruser"}
form_data = {"username": create_data["email"], "password": "useruser"}
return login(form_data, api_client, api_routes)
@ -96,3 +132,45 @@ def raw_recipe_no_image():
@fixture(scope="session")
def recipe_store():
return get_recipe_test_cases()
@fixture(scope="module")
def unique_user(api_client: TestClient, api_routes: AppRoutes):
registration = user_registration_factory()
response = api_client.post("/api/users/register", json=registration.dict(by_alias=True))
assert response.status_code == 201
form_data = {"username": registration.username, "password": registration.password}
token = login(form_data, api_client, api_routes)
user_data = api_client.get(api_routes.users_self, headers=token).json()
assert token is not None
try:
yield TestUser(group_id=user_data.get("groupId"), user_id=user_data.get("id"), token=token)
finally:
# TODO: Delete User after test
pass
@fixture(scope="session")
def admin_user(api_client: TestClient, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": settings.DEFAULT_PASSWORD}
token = login(form_data, api_client, api_routes)
user_data = api_client.get(api_routes.users_self, headers=token).json()
assert token is not None
assert user_data.get("admin") is True
assert user_data.get("groupId") is not None
assert user_data.get("id") is not None
try:
yield TestUser(group_id=user_data.get("groupId"), user_id=user_data.get("id"), token=token)
finally:
# TODO: Delete User after test
pass

View file

@ -0,0 +1,91 @@
import json
from fastapi.testclient import TestClient
from tests.app_routes import AppRoutes
from tests.utils.fixture_schemas import TestUser
def test_init_superuser(api_client: TestClient, api_routes: AppRoutes, admin_token, admin_user: TestUser):
response = api_client.get(api_routes.users_id(1), headers=admin_token)
assert response.status_code == 200
admin_data = response.json()
assert admin_data["id"] == admin_user.user_id
assert admin_data["groupId"] == admin_user.group_id
assert admin_data["fullName"] == "Change Me"
assert admin_data["email"] == "changeme@email.com"
def test_create_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
create_data = {
"fullName": "My New User",
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
assert response.status_code == 201
user_data = response.json()
assert user_data["fullName"] == create_data["fullName"]
assert user_data["email"] == create_data["email"]
assert user_data["group"] == create_data["group"]
assert user_data["admin"] == create_data["admin"]
def test_create_user_as_non_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
create_data = {
"fullName": "My New User",
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=user_token)
assert response.status_code == 403
def test_update_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 200
assert json.loads(response.text).get("access_token")
def test_update_other_user_as_not_admin(api_client: TestClient, api_routes: AppRoutes, unique_user: TestUser):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=unique_user.token, json=update_data)
assert response.status_code == 403
def test_self_demote_admin(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": False}
response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 403
def test_self_promote_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"id": 3, "fullName": "Updated Name", "email": "user@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(2), headers=user_token, json=update_data)
assert response.status_code == 403
def test_delete_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.users_id(2), headers=admin_token)
assert response.status_code == 200

View file

@ -0,0 +1,46 @@
import json
from fastapi.testclient import TestClient
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/admin/groups"
def item(id: str) -> str:
return f"{Routes.base}/{id}"
def test_create_group(api_client: TestClient, admin_token):
response = api_client.post(Routes.base, json={"name": random_string()}, headers=admin_token)
assert response.status_code == 201
def test_user_cant_create_group(api_client: TestClient, unique_user: TestUser):
response = api_client.post(Routes.base, json={"name": random_string()}, headers=unique_user.token)
assert response.status_code == 403
def test_home_group_not_deletable(api_client: TestClient, admin_token):
response = api_client.delete(Routes.item(1), headers=admin_token)
assert response.status_code == 400
def test_delete_group(api_client: TestClient, admin_token):
response = api_client.post(Routes.base, json={"name": random_string()}, headers=admin_token)
assert response.status_code == 201
group_id = json.loads(response.text)["id"]
response = api_client.delete(Routes.item(group_id), headers=admin_token)
assert response.status_code == 200
# Ensure Group is Deleted
response = api_client.get(Routes.base, headers=admin_token)
for g in response.json():
assert g["id"] != group_id

View file

@ -1,60 +0,0 @@
import json
import pytest
from fastapi.testclient import TestClient
from tests.app_routes import AppRoutes
from tests.utils.assertion_helpers import assert_ignore_keys
@pytest.fixture
def group_data():
return {"name": "Test Group"}
def test_create_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.post(api_routes.groups, json={"name": "Test Group"}, headers=admin_token)
assert response.status_code == 201
def test_get_self_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.groups, headers=admin_token)
assert response.status_code == 200
assert len(json.loads(response.text)) >= 2
def test_update_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
new_data = {
"name": "New Group Name",
"id": 2,
"categories": [],
"webhooks": [],
"users": [],
"mealplans": [],
"shoppingLists": [],
}
# Test Update
response = api_client.put(api_routes.groups_id(2), json=new_data, headers=admin_token)
assert response.status_code == 200
# Validate Changes
response = api_client.get(api_routes.groups, headers=admin_token)
all_groups = json.loads(response.text)
id_2 = filter(lambda x: x["id"] == 2, all_groups)
assert_ignore_keys(new_data, next(id_2), ["preferences"])
def test_home_group_not_deletable(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.groups_id(1), headers=admin_token)
assert response.status_code == 400
def test_delete_group(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.groups_id(2), headers=admin_token)
assert response.status_code == 200

View file

@ -14,7 +14,7 @@ def backup_data():
"recipes": True,
"settings": False, # ! Broken
"groups": False, # ! Also Broken
"users": True,
"users": False,
}

View file

@ -1,103 +0,0 @@
import json
import pytest
from fastapi.testclient import TestClient
from tests.app_routes import AppRoutes
from tests.utils.recipe_data import RecipeSiteTestCase
def get_meal_plan_template(first=None, second=None):
return {
"group": "Home",
"startDate": "2021-01-18",
"endDate": "2021-01-19",
"planDays": [
{
"date": "2021-1-18",
"meals": [{"slug": first, "name": "", "description": ""}],
},
{
"date": "2021-1-19",
"meals": [{"slug": second, "name": "", "description": ""}],
},
],
}
@pytest.fixture(scope="session")
def slug_1(api_client: TestClient, api_routes: AppRoutes, admin_token, recipe_store: list[RecipeSiteTestCase]):
slug_1 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[0].url}, headers=admin_token)
slug_1 = json.loads(slug_1.content)
yield slug_1
api_client.delete(api_routes.recipes_recipe_slug(slug_1))
@pytest.fixture(scope="session")
def slug_2(api_client: TestClient, api_routes: AppRoutes, admin_token, recipe_store: list[RecipeSiteTestCase]):
slug_2 = api_client.post(api_routes.recipes_create_url, json={"url": recipe_store[1].url}, headers=admin_token)
slug_2 = json.loads(slug_2.content)
yield slug_2
api_client.delete(api_routes.recipes_recipe_slug(slug_2))
def test_create_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
meal_plan = get_meal_plan_template(slug_1, slug_2)
response = api_client.post(api_routes.meal_plans_create, json=meal_plan, headers=admin_token)
assert response.status_code == 201
def test_read_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
assert response.status_code == 200
meal_plan_template = get_meal_plan_template(slug_1, slug_2)
created_meal_plan = json.loads(response.text)
meals = created_meal_plan[0]["planDays"]
assert meals[0]["meals"][0]["slug"] == meal_plan_template["planDays"][0]["meals"][0]["slug"]
assert meals[1]["meals"][0]["slug"] == meal_plan_template["planDays"][1]["meals"][0]["slug"]
def test_update_mealplan(api_client: TestClient, api_routes: AppRoutes, slug_1, slug_2, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
# Swap
plan_uid = existing_mealplan.get("id")
existing_mealplan["planDays"][0]["meals"][0]["slug"] = slug_2
existing_mealplan["planDays"][1]["meals"][0]["slug"] = slug_1
response = api_client.put(api_routes.meal_plans_plan_id(plan_uid), json=existing_mealplan, headers=admin_token)
assert response.status_code == 200
response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
assert existing_mealplan["planDays"][0]["meals"][0]["slug"] == slug_2
assert existing_mealplan["planDays"][1]["meals"][0]["slug"] == slug_1
def test_delete_mealplan(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.get(api_routes.meal_plans_all, headers=admin_token)
assert response.status_code == 200
existing_mealplan = json.loads(response.text)
existing_mealplan = existing_mealplan[0]
plan_uid = existing_mealplan.get("id")
response = api_client.delete(api_routes.meal_plans_plan_id(plan_uid), headers=admin_token)
assert response.status_code == 200

View file

@ -1,58 +0,0 @@
import json
import pytest
from fastapi.testclient import TestClient
from mealie.schema.user import SignUpToken
from tests.app_routes import AppRoutes
@pytest.fixture()
def active_link(api_client: TestClient, api_routes: AppRoutes, admin_token):
data = {"name": "Fixture Token", "admin": True}
response = api_client.post(api_routes.users_sign_ups, json=data, headers=admin_token)
return SignUpToken(**json.loads(response.text))
@pytest.fixture()
def sign_up_user():
return {
"fullName": "Test User",
"email": "test_user@email.com",
"admin": True,
"group": "string",
"password": "MySecretPassword",
}
def test_create_sign_up_link(api_client: TestClient, api_routes: AppRoutes, admin_token):
data = {"name": "Test Token", "admin": False}
response = api_client.post(api_routes.users_sign_ups, json=data, headers=admin_token)
assert response.status_code == 200
def test_new_user_signup(api_client: TestClient, api_routes: AppRoutes, active_link: SignUpToken, sign_up_user):
# Creation
response = api_client.post(api_routes.users_sign_ups_token(active_link.token), json=sign_up_user)
assert response.status_code == 200
# Login
form_data = {"username": "test_user@email.com", "password": "MySecretPassword"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
def test_delete_sign_up_link(
api_client: TestClient, api_routes: AppRoutes, admin_token, active_link: SignUpToken, sign_up_user
):
response = api_client.delete(api_routes.users_sign_ups_token(active_link.token), headers=admin_token)
assert response.status_code == 200
# Validate admin_token is Gone
response = api_client.get(api_routes.users_sign_ups, headers=admin_token)
assert sign_up_user not in json.loads(response.content)

View file

@ -1,177 +0,0 @@
import json
from pathlib import Path
from fastapi.testclient import TestClient
from pytest import fixture
from mealie.core.config import app_dirs
from mealie.schema.user import UserOut
from tests.app_routes import AppRoutes
@fixture(scope="session")
def admin_user():
return UserOut(
id=1,
fullName="Change Me",
username="Change Me",
email="changeme@email.com",
group="Home",
admin=True,
tokens=[],
)
@fixture(scope="session")
def new_user():
return UserOut(
id=3,
fullName="My New User",
username="My New User",
email="newuser@email.com",
group="Home",
admin=False,
tokens=[],
)
def test_failed_login(api_client: TestClient, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": "WRONG_PASSWORD"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 401
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, admin_token):
form_data = {"username": "changeme@email.com", "password": "MyPassword"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
new_token = json.loads(response.text).get("access_token")
response = api_client.get(api_routes.users_self, headers=admin_token)
assert response.status_code == 200
return {"Authorization": f"Bearer {new_token}"}
def test_init_superuser(api_client: TestClient, api_routes: AppRoutes, admin_token, admin_user: UserOut):
response = api_client.get(api_routes.users_id(1), headers=admin_token)
assert response.status_code == 200
assert json.loads(response.text) == admin_user.dict(by_alias=True)
def test_create_user(api_client: TestClient, api_routes: AppRoutes, admin_token, new_user):
create_data = {
"fullName": "My New User",
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=admin_token)
assert response.status_code == 201
assert json.loads(response.text) == new_user.dict(by_alias=True)
def test_create_user_as_non_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
create_data = {
"fullName": "My New User",
"email": "newuser@email.com",
"password": "MyStrongPassword",
"group": "Home",
"admin": False,
"tokens": [],
}
response = api_client.post(api_routes.users, json=create_data, headers=user_token)
assert response.status_code == 403
def test_get_all_users(api_client: TestClient, api_routes: AppRoutes, admin_token, new_user, admin_user):
response = api_client.get(api_routes.users, headers=admin_token)
assert response.status_code == 200
all_users = json.loads(response.text)
assert admin_user.dict(by_alias=True) in all_users
assert new_user.dict(by_alias=True) in all_users
def test_update_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 200
assert json.loads(response.text).get("access_token")
def test_update_other_user_as_not_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"id": 1, "fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(1), headers=user_token, json=update_data)
assert response.status_code == 403
def test_update_self_as_not_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"fullName": "User fullname", "email": "user@email.com", "group": "Home", "admin": False}
response = api_client.put(api_routes.users_id(4), headers=user_token, json=update_data)
assert response.status_code == 200
def test_self_demote_admin(api_client: TestClient, api_routes: AppRoutes, admin_token):
update_data = {"fullName": "Updated Name", "email": "changeme@email.com", "group": "Home", "admin": False}
response = api_client.put(api_routes.users_id(1), headers=admin_token, json=update_data)
assert response.status_code == 403
def test_self_promote_admin(api_client: TestClient, api_routes: AppRoutes, user_token):
update_data = {"id": 3, "fullName": "Updated Name", "email": "user@email.com", "group": "Home", "admin": True}
response = api_client.put(api_routes.users_id(2), headers=user_token, json=update_data)
assert response.status_code == 403
def test_reset_user_password(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.put(api_routes.users_id_reset_password(3), headers=admin_token)
assert response.status_code == 200
form_data = {"username": "newuser@email.com", "password": "MyPassword"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
def test_delete_user(api_client: TestClient, api_routes: AppRoutes, admin_token):
response = api_client.delete(api_routes.users_id(2), headers=admin_token)
assert response.status_code == 200
def test_update_user_image(
api_client: TestClient, api_routes: AppRoutes, test_image_jpg: Path, test_image_png: Path, admin_token
):
response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_jpg.open("rb")}, headers=admin_token
)
assert response.status_code == 200
response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_png.open("rb")}, headers=admin_token
)
assert response.status_code == 200
directory = app_dirs.USER_DIR.joinpath("2")
assert directory.joinpath("profile_image.png").is_file()
# Old profile images are removed
assert 1 == len([file for file in directory.glob("profile_image.*") if file.is_file()])

View file

@ -0,0 +1,85 @@
import pytest
from fastapi.testclient import TestClient
from tests.utils.factories import user_registration_factory
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/groups/invitations"
auth_token = "/api/auth/token"
self = "/api/users/self"
register = "/api/users/register"
def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}"
@pytest.fixture(scope="function")
def invite(api_client: TestClient, unique_user: TestUser) -> None:
# Test Creation
r = api_client.post(Routes.base, 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(Routes.base, 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["token"] == invite
def register_user(api_client, invite):
# Test User can Join Group
registration = user_registration_factory()
registration.group = ""
registration.group_token = invite
response = api_client.post(Routes.register, json=registration.dict(by_alias=True))
print(response.json())
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(Routes.auth_token, form_data)
assert r.status_code == 200
token = r.json().get("access_token")
assert token is not None
# Check user Group is Same
r = api_client.get(Routes.self, headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 200
assert r.json()["groupId"] == unique_user.group_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

@ -2,6 +2,7 @@ from fastapi.testclient import TestClient
from mealie.schema.group.group_preferences import UpdateGroupPreferences
from tests.utils.assertion_helpers import assert_ignore_keys
from tests.utils.fixture_schemas import TestUser
class Routes:
@ -9,8 +10,8 @@ class Routes:
preferences = "/api/groups/preferences"
def test_get_preferences(api_client: TestClient, admin_token) -> None:
response = api_client.get(Routes.preferences, headers=admin_token)
def test_get_preferences(api_client: TestClient, unique_user: TestUser) -> None:
response = api_client.get(Routes.preferences, headers=unique_user.token)
assert response.status_code == 200
@ -21,8 +22,8 @@ def test_get_preferences(api_client: TestClient, admin_token) -> None:
assert preferences["recipeShowNutrition"] is False
def test_preferences_in_group(api_client: TestClient, admin_token) -> None:
response = api_client.get(Routes.base, headers=admin_token)
def test_preferences_in_group(api_client: TestClient, unique_user: TestUser) -> None:
response = api_client.get(Routes.base, headers=unique_user.token)
assert response.status_code == 200
@ -35,10 +36,10 @@ def test_preferences_in_group(api_client: TestClient, admin_token) -> None:
assert group["preferences"]["recipeShowNutrition"] is False
def test_update_preferences(api_client: TestClient, admin_token) -> None:
def test_update_preferences(api_client: TestClient, unique_user: TestUser) -> None:
new_data = UpdateGroupPreferences(recipe_public=False, recipe_show_nutrition=True)
response = api_client.put(Routes.preferences, json=new_data.dict(), headers=admin_token)
response = api_client.put(Routes.preferences, json=new_data.dict(), headers=unique_user.token)
assert response.status_code == 200

View file

@ -1,6 +1,6 @@
from fastapi.testclient import TestClient
from mealie.schema.user.registration import CreateUserRegistration
from tests.utils.factories import user_registration_factory
class Routes:
@ -9,21 +9,13 @@ class Routes:
def test_user_registration_new_group(api_client: TestClient):
registration = CreateUserRegistration(
group="New Group Name",
email="email@email.com",
username="fake-user-name",
password="fake-password",
password_confirm="fake-password",
advanced=False,
private=False,
)
registration = user_registration_factory()
response = api_client.post(Routes.base, json=registration.dict(by_alias=True))
assert response.status_code == 201
# Login
form_data = {"username": "email@email.com", "password": "fake-password"}
form_data = {"username": registration.email, "password": registration.password}
response = api_client.post(Routes.auth_token, form_data)
assert response.status_code == 200

View file

@ -0,0 +1,63 @@
import pytest
from fastapi.testclient import TestClient
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/groups/webhooks"
def item(item_id: int) -> str:
return f"{Routes.base}/{item_id}"
@pytest.fixture()
def webhook_data():
return {"enabled": True, "name": "Test-Name", "url": "https://my-fake-url.com", "time": "00:00"}
def test_create_webhook(api_client: TestClient, unique_user: TestUser, webhook_data):
response = api_client.post(Routes.base, json=webhook_data, headers=unique_user.token)
assert response.status_code == 200
def test_read_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
response = api_client.get(Routes.item(1), headers=unique_user.token)
webhook = response.json()
assert webhook["id"] == 1
assert webhook["name"] == webhook_data["name"]
assert webhook["url"] == webhook_data["url"]
assert webhook["time"] == webhook_data["time"]
assert webhook["enabled"] == webhook_data["enabled"]
def test_update_webhook(api_client: TestClient, webhook_data, unique_user: TestUser):
webhook_data["id"] = 1
webhook_data["name"] = "My New Name"
webhook_data["url"] = "https://my-new-fake-url.com"
webhook_data["time"] = "01:00"
webhook_data["enabled"] = False
response = api_client.put(Routes.item(1), json=webhook_data, headers=unique_user.token)
assert response.status_code == 200
updated_webhook = response.json()
assert updated_webhook["name"] == webhook_data["name"]
assert updated_webhook["url"] == webhook_data["url"]
assert updated_webhook["time"] == webhook_data["time"]
assert updated_webhook["enabled"] == webhook_data["enabled"]
assert response.status_code == 200
def test_delete_webhook(api_client: TestClient, unique_user: TestUser):
response = api_client.delete(Routes.item(1), headers=unique_user.token)
assert response.status_code == 200
response = api_client.get(Routes.item(1), headers=unique_user.token)
assert response.status_code == 404

View file

@ -5,25 +5,30 @@ from fastapi.testclient import TestClient
from slugify import slugify
from tests.app_routes import AppRoutes
from tests.utils.fixture_schemas import TestUser
from tests.utils.recipe_data import RecipeSiteTestCase, get_recipe_test_cases
recipe_test_data = get_recipe_test_cases()
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_create_by_url(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=user_token)
def test_create_by_url(
api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, unique_user: TestUser
):
api_client.delete(api_routes.recipes_recipe_slug(recipe_data.expected_slug), headers=unique_user.token)
response = api_client.post(api_routes.recipes_create_url, json={"url": recipe_data.url}, headers=user_token)
response = api_client.post(api_routes.recipes_create_url, json={"url": recipe_data.url}, headers=unique_user.token)
assert response.status_code == 201
assert json.loads(response.text) == recipe_data.expected_slug
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
def test_read_update(
api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, unique_user: TestUser
):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=user_token)
response = api_client.get(recipe_url, headers=unique_user.token)
assert response.status_code == 200
recipe = json.loads(response.text)
@ -39,12 +44,12 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data:
test_categories = ["one", "two", "three"]
recipe["recipeCategory"] = test_categories
response = api_client.put(recipe_url, json=recipe, headers=user_token)
response = api_client.put(recipe_url, json=recipe, headers=unique_user.token)
assert response.status_code == 200
assert json.loads(response.text).get("slug") == recipe_data.expected_slug
response = api_client.get(recipe_url, headers=user_token)
response = api_client.get(recipe_url, headers=unique_user.token)
assert response.status_code == 200
recipe = json.loads(response.text)
@ -53,9 +58,9 @@ def test_read_update(api_client: TestClient, api_routes: AppRoutes, recipe_data:
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, unique_user: TestUser):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.get(recipe_url, headers=user_token)
response = api_client.get(recipe_url, headers=unique_user.token)
assert response.status_code == 200
recipe = json.loads(response.text)
@ -63,7 +68,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
new_slug = slugify(new_name)
recipe["name"] = new_name
response = api_client.put(recipe_url, json=recipe, headers=user_token)
response = api_client.put(recipe_url, json=recipe, headers=unique_user.token)
assert response.status_code == 200
assert json.loads(response.text).get("slug") == new_slug
@ -72,7 +77,7 @@ def test_rename(api_client: TestClient, api_routes: AppRoutes, recipe_data: Reci
@pytest.mark.parametrize("recipe_data", recipe_test_data)
def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, user_token):
def test_delete(api_client: TestClient, api_routes: AppRoutes, recipe_data: RecipeSiteTestCase, unique_user: TestUser):
recipe_url = api_routes.recipes_recipe_slug(recipe_data.expected_slug)
response = api_client.delete(recipe_url, headers=user_token)
response = api_client.delete(recipe_url, headers=unique_user.token)
assert response.status_code == 200

View file

@ -0,0 +1,76 @@
from fastapi.testclient import TestClient
from tests.utils.factories import random_string
from tests.utils.fixture_schemas import TestUser
class Routes:
base = "/api/recipes"
user = "/api/users/self"
GROUP_ID = 1
ADMIN_ID = 1
USER_ID = 2
def test_ownership_on_new_with_admin(api_client: TestClient, admin_token):
recipe_name = random_string()
response = api_client.post(Routes.base, json={"name": recipe_name}, headers=admin_token)
assert response.status_code == 201
recipe = api_client.get(Routes.base + f"/{recipe_name}", headers=admin_token).json()
assert recipe["userId"] == ADMIN_ID
assert recipe["groupId"] == GROUP_ID
def test_ownership_on_new_with_user(api_client: TestClient, g2_user: TestUser):
recipe_name = random_string()
response = api_client.post(Routes.base, json={"name": recipe_name}, headers=g2_user.token)
assert response.status_code == 201
response = api_client.get(Routes.base + f"/{recipe_name}", headers=g2_user.token)
assert response.status_code == 200
recipe = response.json()
assert recipe["userId"] == g2_user.user_id
assert recipe["groupId"] == g2_user.group_id
def test_get_all_only_includes_group_recipes(api_client: TestClient, unique_user: TestUser):
for _ in range(5):
recipe_name = random_string()
response = api_client.post(Routes.base, json={"name": recipe_name}, headers=unique_user.token)
response = api_client.get(Routes.base, headers=unique_user.token)
assert response.status_code == 200
recipes = response.json()
assert len(recipes) == 5
for recipe in recipes:
assert recipe["groupId"] == unique_user.group_id
assert recipe["userId"] == unique_user.user_id
def test_unique_slug_by_group(api_client: TestClient, unique_user: TestUser, g2_user: TestUser) -> None:
create_data = {"name": random_string()}
response = api_client.post(Routes.base, json=create_data, headers=unique_user.token)
assert response.status_code == 201
response = api_client.post(Routes.base, json=create_data, headers=g2_user.token)
assert response.status_code == 201
# Try to create a recipe again with the same name
response = api_client.post(Routes.base, json=create_data, headers=g2_user.token)
assert response.status_code == 400

View file

@ -0,0 +1,28 @@
from pathlib import Path
from fastapi.testclient import TestClient
from mealie.core.config import app_dirs
from tests.app_routes import AppRoutes
def test_update_user_image(
api_client: TestClient, api_routes: AppRoutes, test_image_jpg: Path, test_image_png: Path, admin_token
):
response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_jpg.open("rb")}, headers=admin_token
)
assert response.status_code == 200
response = api_client.post(
api_routes.users_id_image(2), files={"profile_image": test_image_png.open("rb")}, headers=admin_token
)
assert response.status_code == 200
directory = app_dirs.USER_DIR.joinpath("2")
assert directory.joinpath("profile_image.png").is_file()
# Old profile images are removed
assert 1 == len([file for file in directory.glob("profile_image.*") if file.is_file()])

View file

@ -0,0 +1,25 @@
import json
from fastapi.testclient import TestClient
from tests.app_routes import AppRoutes
def test_failed_login(api_client: TestClient, api_routes: AppRoutes):
form_data = {"username": "changeme@email.com", "password": "WRONG_PASSWORD"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 401
def test_superuser_login(api_client: TestClient, api_routes: AppRoutes, admin_token):
form_data = {"username": "changeme@email.com", "password": "MyPassword"}
response = api_client.post(api_routes.auth_token, form_data)
assert response.status_code == 200
new_token = json.loads(response.text).get("access_token")
response = api_client.get(api_routes.users_self, headers=admin_token)
assert response.status_code == 200
return {"Authorization": f"Bearer {new_token}"}

View file

@ -6,9 +6,11 @@ from tests.utils.recipe_data import RecipeSiteTestCase, get_recipe_test_cases
test_cases = get_recipe_test_cases()
"""
These tests are skipped by default and only really used when troubleshooting the parser
directly. If you are working on improve the parser you can add test cases to the `get_recipe_test_cases` function
and then use this test case by removing the `@pytest.mark.skip` and than testing your results.
"""

24
tests/utils/factories.py Normal file
View file

@ -0,0 +1,24 @@
import random
import string
from mealie.schema.user.registration import CreateUserRegistration
def random_string(length=10) -> str:
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)).strip()
def random_email(length=10) -> str:
return "".join(random.choice(string.ascii_lowercase + string.digits) for _ in range(length)) + "@fake.com"
def user_registration_factory() -> CreateUserRegistration:
return CreateUserRegistration(
group=random_string(),
email=random_email(),
username=random_string(),
password="fake-password",
password_confirm="fake-password",
advanced=False,
private=False,
)

View file

@ -0,0 +1,9 @@
from dataclasses import dataclass
from typing import Any
@dataclass
class TestUser:
user_id: int
group_id: int
token: Any