mirror of
https://github.com/mealie-recipes/mealie.git
synced 2025-07-09 03:04:54 -04:00
feat: Cross-Household Recipes (#4089)
This commit is contained in:
parent
7ef2e91ecf
commit
9acf9ec27c
@ -21,31 +21,23 @@
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
<div v-if="!open" class="custom-btn-group ma-1">
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="mx-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<RecipeFavoriteBadge v-if="loggedIn" class="ml-1" color="info" button-style :recipe-id="recipe.id" show-always />
|
||||
<RecipeTimelineBadge v-if="loggedIn" button-style class="ml-1" :slug="recipe.slug" :recipe-name="recipe.name" />
|
||||
<div v-if="loggedIn">
|
||||
<v-tooltip v-if="!locked" bottom color="info">
|
||||
<v-tooltip v-if="canEdit" bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-btn fab small class="ml-1" color="info" v-bind="attrs" v-on="on" @click="$emit('edit', true)">
|
||||
<v-icon> {{ $globals.icons.edit }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span>{{ $t("general.edit") }}</span>
|
||||
</v-tooltip>
|
||||
<v-tooltip v-else bottom color="info">
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn fab small class="mx-1" color="info" v-bind="attrs" v-on="on">
|
||||
<v-icon> {{ $globals.icons.lock }} </v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<span> {{ $t("recipe.locked-by-owner") }} </span>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<RecipeTimerMenu
|
||||
fab
|
||||
color="info"
|
||||
class="mr-1"
|
||||
class="ml-1"
|
||||
/>
|
||||
|
||||
<RecipeContextMenu
|
||||
@ -72,6 +64,7 @@
|
||||
share: loggedIn,
|
||||
recipeActions: true,
|
||||
}"
|
||||
class="ml-1"
|
||||
@print="$emit('print')"
|
||||
/>
|
||||
</div>
|
||||
@ -135,7 +128,7 @@ export default defineComponent({
|
||||
required: true,
|
||||
type: String,
|
||||
},
|
||||
locked: {
|
||||
canEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -45,7 +45,7 @@
|
||||
:recipe="recipe"
|
||||
:slug="recipe.slug"
|
||||
:recipe-scale="recipeScale"
|
||||
:locked="isOwnGroup && user.id !== recipe.userId && recipe.settings.locked"
|
||||
:can-edit="canEditRecipe"
|
||||
:name="recipe.name"
|
||||
:logged-in="isOwnGroup"
|
||||
:open="isEditMode"
|
||||
@ -64,6 +64,7 @@
|
||||
<script lang="ts">
|
||||
import { defineComponent, useContext, computed, ref, watch } from "@nuxtjs/composition-api";
|
||||
import { useLoggedInState } from "~/composables/use-logged-in-state";
|
||||
import { useRecipePermissions } from "~/composables/recipes";
|
||||
import RecipeRating from "~/components/Domain/Recipe/RecipeRating.vue";
|
||||
import RecipeLastMade from "~/components/Domain/Recipe/RecipeLastMade.vue";
|
||||
import RecipeActionMenu from "~/components/Domain/Recipe/RecipeActionMenu.vue";
|
||||
@ -99,6 +100,7 @@ export default defineComponent({
|
||||
const { imageKey, pageMode, editMode, setMode, toggleEditMode, isEditMode } = usePageState(props.recipe.slug);
|
||||
const { user } = usePageUser();
|
||||
const { isOwnGroup } = useLoggedInState();
|
||||
const { canEditRecipe } = useRecipePermissions(props.recipe, user);
|
||||
|
||||
function printRecipe() {
|
||||
window.print();
|
||||
@ -125,6 +127,7 @@ export default defineComponent({
|
||||
setMode,
|
||||
toggleEditMode,
|
||||
recipeImage,
|
||||
canEditRecipe,
|
||||
imageKey,
|
||||
user,
|
||||
PageMode,
|
||||
|
@ -4,3 +4,4 @@ export { useRecipes, recentRecipes, allRecipes, useLazyRecipes } from "./use-rec
|
||||
export { parseIngredientText, useParsedIngredientText } from "./use-recipe-ingredients";
|
||||
export { useNutritionLabels } from "./use-recipe-nutrition";
|
||||
export { useTools } from "./use-recipe-tools";
|
||||
export { useRecipePermissions } from "./use-recipe-permissions";
|
||||
|
81
frontend/composables/recipes/use-recipe-permissions.test.ts
Normal file
81
frontend/composables/recipes/use-recipe-permissions.test.ts
Normal file
@ -0,0 +1,81 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { useRecipePermissions } from "./use-recipe-permissions";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
describe("test use recipe permissions", () => {
|
||||
const commonUserId = "my-user-id";
|
||||
const commonGroupId = "my-group-id";
|
||||
const commonHouseholdId = "my-household-id";
|
||||
|
||||
const createRecipe = (overrides: Partial<Recipe>, isLocked = false): Recipe => ({
|
||||
id: "my-recipe-id",
|
||||
userId: commonUserId,
|
||||
groupId: commonGroupId,
|
||||
householdId: commonHouseholdId,
|
||||
settings: {
|
||||
locked: isLocked,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const createUser = (overrides: Partial<UserOut>): UserOut => ({
|
||||
id: commonUserId,
|
||||
groupId: commonGroupId,
|
||||
groupSlug: "my-group",
|
||||
group: "my-group",
|
||||
householdId: commonHouseholdId,
|
||||
householdSlug: "my-household",
|
||||
household: "my-household",
|
||||
email: "bender.rodriguez@example.com",
|
||||
cacheKey: "1234",
|
||||
...overrides,
|
||||
});
|
||||
|
||||
test("when user is null, cannot edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}), null);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is recipe owner, can edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}), createUser({}));
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, is correct group and household, and recipe is unlocked, can edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id" }),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and user is other group, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id", groupId: "other-group-id"}),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and user is other household, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}),
|
||||
createUser({ id: "other-user-id", householdId: "other-household-id" }),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is not recipe owner, and recipe is locked, cannot edit", () => {
|
||||
const result = useRecipePermissions(
|
||||
createRecipe({}, true),
|
||||
createUser({ id: "other-user-id"}),
|
||||
);
|
||||
expect(result.canEditRecipe.value).toBe(false);
|
||||
});
|
||||
|
||||
test("when user is recipe owner, and recipe is locked, can edit", () => {
|
||||
const result = useRecipePermissions(createRecipe({}, true), createUser({}));
|
||||
expect(result.canEditRecipe.value).toBe(true);
|
||||
});
|
||||
});
|
34
frontend/composables/recipes/use-recipe-permissions.ts
Normal file
34
frontend/composables/recipes/use-recipe-permissions.ts
Normal file
@ -0,0 +1,34 @@
|
||||
import { computed } from "@nuxtjs/composition-api";
|
||||
import { Recipe } from "~/lib/api/types/recipe";
|
||||
import { UserOut } from "~/lib/api/types/user";
|
||||
|
||||
export function useRecipePermissions(recipe: Recipe, user: UserOut | null) {
|
||||
const canEditRecipe = computed(() => {
|
||||
// Check recipe owner
|
||||
if (!user?.id) {
|
||||
return false;
|
||||
}
|
||||
if (user.id === recipe.userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check group and household
|
||||
if (user.groupId !== recipe.groupId) {
|
||||
return false;
|
||||
}
|
||||
if (user.householdId !== recipe.householdId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check recipe
|
||||
if (recipe.settings?.locked) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
return {
|
||||
canEditRecipe,
|
||||
}
|
||||
}
|
@ -84,8 +84,10 @@ export const useLazyRecipes = function (publicGroupSlug: string | null = null) {
|
||||
};
|
||||
|
||||
export const useRecipes = (
|
||||
all = false, fetchRecipes = true,
|
||||
all = false,
|
||||
fetchRecipes = true,
|
||||
loadFood = false,
|
||||
queryFilter: string | null = null,
|
||||
publicGroupSlug: string | null = null
|
||||
) => {
|
||||
const api = publicGroupSlug ? usePublicExploreApi(publicGroupSlug).explore : useUserApi();
|
||||
@ -108,7 +110,7 @@ export const useRecipes = (
|
||||
})();
|
||||
|
||||
async function refreshRecipes() {
|
||||
const { data } = await api.recipes.getAll(page, perPage, { loadFood, orderBy: "created_at" });
|
||||
const { data } = await api.recipes.getAll(page, perPage, { loadFood, orderBy: "created_at", queryFilter });
|
||||
if (data) {
|
||||
recipes.value = data.items;
|
||||
}
|
||||
|
@ -27,8 +27,7 @@ export default defineComponent({
|
||||
async function fetchHousehold() {
|
||||
const { data } = await api.households.getCurrentUserHousehold();
|
||||
if (data) {
|
||||
// TODO: once users are able to fetch other households' recipes, remove the household filter
|
||||
queryFilter.value = `recipe.group_id="${data.groupId}" AND recipe.household_id="${data.id}"`;
|
||||
queryFilter.value = `recipe.group_id="${data.groupId}"`;
|
||||
groupName.value = data.group;
|
||||
}
|
||||
|
||||
|
@ -178,10 +178,8 @@ export default defineComponent({
|
||||
components: { RecipeDataTable, RecipeOrganizerSelector, GroupExportData, RecipeSettingsSwitches },
|
||||
scrollToTop: true,
|
||||
setup() {
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true);
|
||||
|
||||
const { $globals, i18n } = useContext();
|
||||
|
||||
const { $auth, $globals, i18n } = useContext();
|
||||
const { getAllRecipes, refreshRecipes } = useRecipes(true, true, false, `householdId=${$auth.user?.householdId || ""}`);
|
||||
const selected = ref<Recipe[]>([]);
|
||||
|
||||
function resetAll() {
|
||||
|
@ -32,6 +32,7 @@ from mealie.core.dependencies import (
|
||||
from mealie.core.security import create_recipe_slug_token
|
||||
from mealie.db.models.household.cookbook import CookBook
|
||||
from mealie.pkgs import cache
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.repos.repository_recipes import RepositoryRecipes
|
||||
from mealie.routes._base import BaseCrudController, controller
|
||||
@ -94,9 +95,13 @@ class JSONBytes(JSONResponse):
|
||||
|
||||
class BaseRecipeController(BaseCrudController):
|
||||
@cached_property
|
||||
def repo(self) -> RepositoryRecipes:
|
||||
def recipes(self) -> RepositoryRecipes:
|
||||
return self.repos.recipes
|
||||
|
||||
@cached_property
|
||||
def group_recipes(self) -> RepositoryRecipes:
|
||||
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
||||
|
||||
@cached_property
|
||||
def cookbooks_repo(self) -> RepositoryGeneric[ReadCookBook, CookBook]:
|
||||
return self.repos.cookbooks
|
||||
@ -107,7 +112,7 @@ class BaseRecipeController(BaseCrudController):
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
return HttpRepo[CreateRecipe, Recipe, Recipe](self.repo, self.logger)
|
||||
return HttpRepo[CreateRecipe, Recipe, Recipe](self.recipes, self.logger)
|
||||
|
||||
|
||||
class FormatResponse(BaseModel):
|
||||
@ -331,8 +336,9 @@ class RecipeController(BaseRecipeController):
|
||||
if cookbook_data is None:
|
||||
raise HTTPException(status_code=404, detail="cookbook not found")
|
||||
|
||||
# we use the repo by user so we can sort favorites correctly
|
||||
pagination_response = self.repos.recipes.by_user(self.user.id).page_all(
|
||||
# We use "group_recipes" here so we can return all recipes regardless of household. The query filter can include
|
||||
# a household_id to filter by household. We use the "by_user" so we can sort favorites correctly.
|
||||
pagination_response = self.group_recipes.by_user(self.user.id).page_all(
|
||||
pagination=q,
|
||||
cookbook=cookbook_data,
|
||||
categories=categories,
|
||||
@ -362,7 +368,7 @@ class RecipeController(BaseRecipeController):
|
||||
def get_one(self, slug: str = Path(..., description="A recipe's slug or id")):
|
||||
"""Takes in a recipe's slug or id and returns all data for a recipe"""
|
||||
try:
|
||||
recipe = self.service.get_one_by_slug_or_id(slug)
|
||||
recipe = self.service.get_one(slug)
|
||||
except Exception as e:
|
||||
self.handle_exceptions(e)
|
||||
return None
|
||||
@ -534,7 +540,7 @@ class RecipeController(BaseRecipeController):
|
||||
data_service = RecipeDataService(recipe.id)
|
||||
data_service.write_image(image, extension)
|
||||
|
||||
new_version = self.repo.update_image(slug, extension)
|
||||
new_version = self.recipes.update_image(slug, extension)
|
||||
return UpdateImageResponse(image=new_version)
|
||||
|
||||
@router.post("/{slug}/assets", response_model=RecipeAsset, tags=["Recipe: Images and Assets"])
|
||||
|
@ -4,6 +4,7 @@ from functools import cached_property
|
||||
from fastapi import Depends, File, Form, HTTPException
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import BaseCrudController, controller
|
||||
from mealie.routes._base.mixins import HttpRepo
|
||||
from mealie.routes._base.routers import MealieCrudRoute, UserAPIRouter
|
||||
@ -31,8 +32,8 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
return self.repos.recipe_timeline_events
|
||||
|
||||
@cached_property
|
||||
def recipes_repo(self):
|
||||
return self.repos.recipes
|
||||
def group_recipes(self):
|
||||
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
||||
|
||||
@cached_property
|
||||
def mixins(self):
|
||||
@ -57,7 +58,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
# if the user id is not specified, use the currently-authenticated user
|
||||
data.user_id = data.user_id or self.user.id
|
||||
|
||||
recipe = self.recipes_repo.get_one(data.recipe_id, "id")
|
||||
recipe = self.group_recipes.get_one(data.recipe_id, "id")
|
||||
if not recipe:
|
||||
raise HTTPException(status_code=404, detail="recipe not found")
|
||||
|
||||
@ -87,7 +88,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
@events_router.put("/{item_id}", response_model=RecipeTimelineEventOut)
|
||||
def update_one(self, item_id: UUID4, data: RecipeTimelineEventUpdate):
|
||||
event = self.mixins.patch_one(data, item_id)
|
||||
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
|
||||
recipe = self.group_recipes.get_one(event.recipe_id, "id")
|
||||
if recipe:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
@ -114,7 +115,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
|
||||
recipe = self.group_recipes.get_one(event.recipe_id, "id")
|
||||
if recipe:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
@ -144,7 +145,7 @@ class RecipeTimelineEventsController(BaseCrudController):
|
||||
if event.image != TimelineEventImage.has_image.value:
|
||||
event.image = TimelineEventImage.has_image
|
||||
event = self.mixins.patch_one(event.cast(RecipeTimelineEventUpdate), event.id)
|
||||
recipe = self.recipes_repo.get_one(event.recipe_id, "id")
|
||||
recipe = self.group_recipes.get_one(event.recipe_id, "id")
|
||||
if recipe:
|
||||
self.publish_event(
|
||||
event_type=EventTypes.recipe_updated,
|
||||
|
@ -1,8 +1,10 @@
|
||||
from functools import cached_property
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import HTTPException, status
|
||||
from pydantic import UUID4
|
||||
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.routes._base import BaseUserController, controller
|
||||
from mealie.routes._base.routers import UserAPIRouter
|
||||
from mealie.routes.users._helpers import assert_user_change_allowed
|
||||
@ -14,6 +16,10 @@ router = UserAPIRouter()
|
||||
|
||||
@controller(router)
|
||||
class UserRatingsController(BaseUserController):
|
||||
@cached_property
|
||||
def group_recipes(self):
|
||||
return get_repositories(self.session, group_id=self.group_id, household_id=None).recipes
|
||||
|
||||
def get_recipe_or_404(self, slug_or_id: str | UUID):
|
||||
"""Fetches a recipe by slug or id, or raises a 404 error if not found."""
|
||||
if isinstance(slug_or_id, str):
|
||||
@ -22,11 +28,10 @@ class UserRatingsController(BaseUserController):
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
recipes_repo = self.repos.recipes
|
||||
if isinstance(slug_or_id, UUID):
|
||||
recipe = recipes_repo.get_one(slug_or_id, key="id")
|
||||
recipe = self.group_recipes.get_one(slug_or_id, key="id")
|
||||
else:
|
||||
recipe = recipes_repo.get_one(slug_or_id, key="slug")
|
||||
recipe = self.group_recipes.get_one(slug_or_id, key="slug")
|
||||
|
||||
if not recipe:
|
||||
raise HTTPException(
|
||||
|
@ -16,6 +16,7 @@ from mealie.core.config import get_app_settings
|
||||
from mealie.core.dependencies.dependencies import get_temporary_path
|
||||
from mealie.lang.providers import Translator
|
||||
from mealie.pkgs import cache
|
||||
from mealie.repos.all_repositories import get_repositories
|
||||
from mealie.repos.repository_factory import AllRepositories
|
||||
from mealie.repos.repository_generic import RepositoryGeneric
|
||||
from mealie.schema.household.household import HouseholdInDB
|
||||
@ -46,6 +47,9 @@ class RecipeServiceBase(BaseService):
|
||||
if repos.household_id != user.household_id != household.id:
|
||||
raise Exception("household ids do not match")
|
||||
|
||||
self.group_recipes = get_repositories(repos.session, group_id=repos.group_id, household_id=None).recipes
|
||||
"""Recipes repo without a Household filter"""
|
||||
|
||||
self.translator = translator
|
||||
self.t = translator.t
|
||||
|
||||
@ -54,7 +58,7 @@ class RecipeServiceBase(BaseService):
|
||||
|
||||
class RecipeService(RecipeServiceBase):
|
||||
def _get_recipe(self, data: str | UUID, key: str | None = None) -> Recipe:
|
||||
recipe = self.repos.recipes.get_one(data, key)
|
||||
recipe = self.group_recipes.get_one(data, key)
|
||||
if recipe is None:
|
||||
raise exceptions.NoEntryFound("Recipe not found.")
|
||||
return recipe
|
||||
@ -62,7 +66,18 @@ class RecipeService(RecipeServiceBase):
|
||||
def can_update(self, recipe: Recipe) -> bool:
|
||||
if recipe.settings is None:
|
||||
raise exceptions.UnexpectedNone("Recipe Settings is None")
|
||||
return recipe.settings.locked is False or self.user.id == recipe.user_id
|
||||
|
||||
# Check if this user owns the recipe
|
||||
if self.user.id == recipe.user_id:
|
||||
return True
|
||||
|
||||
# Check if this user has permission to edit this recipe
|
||||
if self.household.id != recipe.household_id:
|
||||
return False
|
||||
if recipe.settings.locked:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def can_lock_unlock(self, recipe: Recipe) -> bool:
|
||||
return recipe.user_id == self.user.id
|
||||
@ -120,7 +135,7 @@ class RecipeService(RecipeServiceBase):
|
||||
|
||||
return Recipe(**additional_attrs)
|
||||
|
||||
def get_one_by_slug_or_id(self, slug_or_id: str | UUID) -> Recipe | None:
|
||||
def get_one(self, slug_or_id: str | UUID) -> Recipe | None:
|
||||
if isinstance(slug_or_id, str):
|
||||
try:
|
||||
slug_or_id = UUID(slug_or_id)
|
||||
@ -393,9 +408,10 @@ class RecipeService(RecipeServiceBase):
|
||||
return new_data
|
||||
|
||||
def update_last_made(self, slug: str, timestamp: datetime) -> Recipe:
|
||||
# we bypass the pre update check since any user can update a recipe's last made date, even if it's locked
|
||||
# we bypass the pre update check since any user can update a recipe's last made date, even if it's locked,
|
||||
# or if the user belongs to a different household
|
||||
recipe = self._get_recipe(slug)
|
||||
return self.repos.recipes.patch(recipe.slug, {"last_made": timestamp})
|
||||
return self.group_recipes.patch(recipe.slug, {"last_made": timestamp})
|
||||
|
||||
def delete_one(self, slug) -> Recipe:
|
||||
recipe = self._get_recipe(slug)
|
||||
|
@ -124,3 +124,20 @@ def test_admin_can_delete(
|
||||
response = api_client.get(api_routes.comments_item_id(comment_id), headers=admin_user.token)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
def test_user_can_comment_on_other_household(api_client: TestClient, unique_recipe: Recipe, h2_user: TestUser):
|
||||
# Create Comment
|
||||
create_data = random_comment(unique_recipe.id)
|
||||
response = api_client.post(api_routes.comments, json=create_data, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
|
||||
# Delete Comment
|
||||
comment_id = response.json()["id"]
|
||||
response = api_client.delete(api_routes.comments_item_id(comment_id), headers=h2_user.token)
|
||||
assert response.status_code == 200
|
||||
|
||||
# Validate Deletion
|
||||
response = api_client.get(api_routes.comments_item_id(comment_id), headers=h2_user.token)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
@ -0,0 +1,186 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from tests.utils import api_routes
|
||||
from tests.utils.factories import random_string
|
||||
from tests.utils.fixture_schemas import TestUser
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
def test_duplicate_recipe_changes_household(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
source_recipe_name = random_string()
|
||||
duplicate_recipe_name = random_string()
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": source_recipe_name}, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
recipe = unique_user.repos.recipes.get_one(response.json())
|
||||
assert recipe
|
||||
assert recipe.name == source_recipe_name
|
||||
assert str(recipe.household_id) == unique_user.household_id
|
||||
|
||||
response = api_client.post(
|
||||
api_routes.recipes_slug_duplicate(recipe.slug), json={"name": duplicate_recipe_name}, headers=h2_user.token
|
||||
)
|
||||
assert response.status_code == 201
|
||||
duplicate_recipe = h2_user.repos.recipes.get_one(response.json()["slug"])
|
||||
assert duplicate_recipe
|
||||
assert duplicate_recipe.name == duplicate_recipe_name
|
||||
assert str(duplicate_recipe.household_id) == h2_user.household_id != unique_user.household_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
def test_get_all_recipes_includes_all_households(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
recipe = unique_user.repos.recipes.get_one(response.json())
|
||||
assert recipe and recipe.id
|
||||
recipe_id = recipe.id
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||
assert h2_recipe and h2_recipe.id
|
||||
h2_recipe_id = h2_recipe.id
|
||||
|
||||
response = api_client.get(api_routes.recipes, params={"page": 1, "perPage": -1}, headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
response_ids = {recipe["id"] for recipe in response.json()["items"]}
|
||||
assert str(recipe_id) in response_ids
|
||||
assert str(h2_recipe_id) in response_ids
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
def test_get_one_recipe_from_another_household(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||
assert h2_recipe and h2_recipe.id
|
||||
h2_recipe_id = h2_recipe.id
|
||||
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == str(h2_recipe_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
@pytest.mark.parametrize("use_patch", [True, False])
|
||||
def test_prevent_updates_to_recipes_from_other_households(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool, use_patch: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
original_name = random_string()
|
||||
response = api_client.post(api_routes.recipes, json={"name": original_name}, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||
assert h2_recipe and h2_recipe.id
|
||||
h2_recipe_id = h2_recipe.id
|
||||
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
recipe = response.json()
|
||||
assert recipe["id"] == str(h2_recipe_id)
|
||||
|
||||
updated_name = random_string()
|
||||
recipe["name"] = updated_name
|
||||
client_func = api_client.patch if use_patch else api_client.put
|
||||
response = client_func(api_routes.recipes_slug(recipe["slug"]), json=recipe, headers=unique_user.token)
|
||||
assert response.status_code == 403
|
||||
|
||||
# confirm the recipe is unchanged
|
||||
response = api_client.get(api_routes.recipes_slug(recipe["slug"]), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
updated_recipe = response.json()
|
||||
assert updated_recipe["name"] == original_name != updated_name
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
def test_prevent_deletes_to_recipes_from_other_households(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||
assert h2_recipe and h2_recipe.id
|
||||
h2_recipe_id = str(h2_recipe.id)
|
||||
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
recipe_json = response.json()
|
||||
assert recipe_json["id"] == h2_recipe_id
|
||||
|
||||
response = api_client.delete(api_routes.recipes_slug(recipe_json["slug"]), headers=unique_user.token)
|
||||
assert response.status_code == 403
|
||||
|
||||
# confirm the recipe still exists
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_id), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == h2_recipe_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize("is_private_household", [True, False])
|
||||
def test_user_can_update_last_made_on_other_household(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, is_private_household: bool
|
||||
):
|
||||
household = unique_user.repos.households.get_one(h2_user.household_id)
|
||||
assert household and household.preferences
|
||||
household.preferences.private_household = is_private_household
|
||||
unique_user.repos.household_preferences.update(household.id, household.preferences)
|
||||
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=h2_user.token)
|
||||
assert response.status_code == 201
|
||||
h2_recipe = h2_user.repos.recipes.get_one(response.json())
|
||||
assert h2_recipe and h2_recipe.id
|
||||
h2_recipe_id = h2_recipe.id
|
||||
h2_recipe_slug = h2_recipe.slug
|
||||
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_slug), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
recipe = response.json()
|
||||
assert recipe["id"] == str(h2_recipe_id)
|
||||
old_last_made = recipe["lastMade"]
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
||||
response = api_client.patch(
|
||||
api_routes.recipes_slug_last_made(h2_recipe_slug), json={"timestamp": now}, headers=unique_user.token
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# confirm the last made date was updated
|
||||
response = api_client.get(api_routes.recipes_slug(h2_recipe_slug), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
recipe = response.json()
|
||||
assert recipe["id"] == str(h2_recipe_id)
|
||||
new_last_made = recipe["lastMade"]
|
||||
assert new_last_made == now != old_last_made
|
@ -368,3 +368,47 @@ def test_recipe_rating_is_readonly(
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["rating"] == rating.rating
|
||||
|
||||
|
||||
def test_user_can_rate_recipes_in_other_households(api_client: TestClient, unique_user: TestUser, h2_user: TestUser):
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
recipe = unique_user.repos.recipes.get_one(response.json())
|
||||
assert recipe and recipe.id
|
||||
|
||||
rating = UserRatingUpdate(rating=random.uniform(1, 5), is_favorite=True)
|
||||
response = api_client.post(
|
||||
api_routes.users_id_ratings_slug(h2_user.user_id, recipe.slug),
|
||||
json=rating.model_dump(),
|
||||
headers=h2_user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get(api_routes.users_self_ratings_recipe_id(recipe.id), headers=h2_user.token)
|
||||
data = response.json()
|
||||
assert data["recipeId"] == str(recipe.id)
|
||||
assert data["rating"] == rating.rating
|
||||
assert data["isFavorite"] is True
|
||||
|
||||
|
||||
def test_average_recipe_rating_includes_all_households(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser
|
||||
):
|
||||
response = api_client.post(api_routes.recipes, json={"name": random_string()}, headers=unique_user.token)
|
||||
assert response.status_code == 201
|
||||
recipe = unique_user.repos.recipes.get_one(response.json())
|
||||
assert recipe
|
||||
|
||||
user_ratings = (UserRatingUpdate(rating=5), UserRatingUpdate(rating=2))
|
||||
for i, user in enumerate([unique_user, h2_user]):
|
||||
response = api_client.post(
|
||||
api_routes.users_id_ratings_slug(user.user_id, recipe.slug),
|
||||
json=user_ratings[i].model_dump(),
|
||||
headers=user.token,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = api_client.get(api_routes.recipes_slug(recipe.slug), headers=unique_user.token)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["rating"] == 3.5
|
||||
|
@ -35,11 +35,19 @@ def recipes(api_client: TestClient, unique_user: TestUser):
|
||||
response = api_client.delete(f"{api_routes.recipes}/{slug}", headers=unique_user.token)
|
||||
|
||||
|
||||
def test_create_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_create_timeline_event(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
recipe = recipes[0]
|
||||
new_event = {
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
@ -48,41 +56,53 @@ def test_create_timeline_event(api_client: TestClient, unique_user: TestUser, re
|
||||
event_response = api_client.post(
|
||||
api_routes.recipes_timeline_events,
|
||||
json=new_event,
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
assert event_response.status_code == 201
|
||||
|
||||
event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert event.recipe_id == recipe.id
|
||||
assert str(event.user_id) == str(unique_user.user_id)
|
||||
assert str(event.user_id) == str(user.user_id)
|
||||
|
||||
|
||||
def test_get_all_timeline_events(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_get_all_timeline_events(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# create some events
|
||||
recipe = recipes[0]
|
||||
events_data = [
|
||||
{
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
}
|
||||
for _ in range(10)
|
||||
]
|
||||
events_data: list[dict] = []
|
||||
for user in [unique_user, h2_user]:
|
||||
events_data.extend(
|
||||
[
|
||||
{
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
}
|
||||
for _ in range(10)
|
||||
]
|
||||
)
|
||||
|
||||
events: list[RecipeTimelineEventOut] = []
|
||||
for event_data in events_data:
|
||||
params: dict = {"queryFilter": f"recipe_id={event_data['recipe_id']}"}
|
||||
event_response = api_client.post(
|
||||
api_routes.recipes_timeline_events, params=params, json=event_data, headers=unique_user.token
|
||||
api_routes.recipes_timeline_events, params=params, json=event_data, headers=user.token
|
||||
)
|
||||
events.append(RecipeTimelineEventOut.model_validate(event_response.json()))
|
||||
|
||||
# check that we see them all
|
||||
params = {"page": 1, "perPage": -1}
|
||||
|
||||
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=unique_user.token)
|
||||
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=user.token)
|
||||
events_pagination = RecipeTimelineEventPagination.model_validate(events_response.json())
|
||||
|
||||
event_ids = [event.id for event in events]
|
||||
@ -93,12 +113,20 @@ def test_get_all_timeline_events(api_client: TestClient, unique_user: TestUser,
|
||||
assert event_id in paginated_event_ids
|
||||
|
||||
|
||||
def test_get_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_get_timeline_event(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# create an event
|
||||
recipe = recipes[0]
|
||||
new_event_data = {
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
@ -107,19 +135,27 @@ def test_get_timeline_event(api_client: TestClient, unique_user: TestUser, recip
|
||||
event_response = api_client.post(
|
||||
api_routes.recipes_timeline_events,
|
||||
json=new_event_data,
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
new_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
|
||||
# fetch the new event
|
||||
event_response = api_client.get(api_routes.recipes_timeline_events_item_id(new_event.id), headers=unique_user.token)
|
||||
event_response = api_client.get(api_routes.recipes_timeline_events_item_id(new_event.id), headers=user.token)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert event == new_event
|
||||
|
||||
|
||||
def test_update_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_update_timeline_event(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
old_subject = random_string()
|
||||
new_subject = random_string()
|
||||
|
||||
@ -127,12 +163,12 @@ def test_update_timeline_event(api_client: TestClient, unique_user: TestUser, re
|
||||
recipe = recipes[0]
|
||||
new_event_data = {
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": old_subject,
|
||||
"event_type": "info",
|
||||
}
|
||||
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=unique_user.token)
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=user.token)
|
||||
new_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert new_event.subject == old_subject
|
||||
|
||||
@ -142,7 +178,7 @@ def test_update_timeline_event(api_client: TestClient, unique_user: TestUser, re
|
||||
event_response = api_client.put(
|
||||
api_routes.recipes_timeline_events_item_id(new_event.id),
|
||||
json=updated_event_data,
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
@ -152,42 +188,54 @@ def test_update_timeline_event(api_client: TestClient, unique_user: TestUser, re
|
||||
assert updated_event.timestamp == new_event.timestamp
|
||||
|
||||
|
||||
def test_delete_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_delete_timeline_event(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# create an event
|
||||
recipe = recipes[0]
|
||||
new_event_data = {
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
}
|
||||
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=unique_user.token)
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=user.token)
|
||||
new_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
|
||||
# delete the event
|
||||
event_response = api_client.delete(
|
||||
api_routes.recipes_timeline_events_item_id(new_event.id), headers=unique_user.token
|
||||
)
|
||||
event_response = api_client.delete(api_routes.recipes_timeline_events_item_id(new_event.id), headers=user.token)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
deleted_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert deleted_event.id == new_event.id
|
||||
|
||||
# try to get the event
|
||||
event_response = api_client.get(
|
||||
api_routes.recipes_timeline_events_item_id(deleted_event.id), headers=unique_user.token
|
||||
)
|
||||
event_response = api_client.get(api_routes.recipes_timeline_events_item_id(deleted_event.id), headers=user.token)
|
||||
assert event_response.status_code == 404
|
||||
|
||||
|
||||
def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_timeline_event_message_alias(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# create an event using aliases
|
||||
recipe = recipes[0]
|
||||
new_event_data = {
|
||||
"recipeId": str(recipe.id),
|
||||
"userId": str(unique_user.user_id),
|
||||
"userId": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"eventType": "info",
|
||||
"eventMessage": random_string(), # eventMessage is the correct alias for the message
|
||||
@ -196,7 +244,7 @@ def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestU
|
||||
event_response = api_client.post(
|
||||
api_routes.recipes_timeline_events,
|
||||
json=new_event_data,
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
new_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert str(new_event.user_id) == new_event_data["userId"]
|
||||
@ -204,7 +252,7 @@ def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestU
|
||||
assert new_event.message == new_event_data["eventMessage"]
|
||||
|
||||
# fetch the new event
|
||||
event_response = api_client.get(api_routes.recipes_timeline_events_item_id(new_event.id), headers=unique_user.token)
|
||||
event_response = api_client.get(api_routes.recipes_timeline_events_item_id(new_event.id), headers=user.token)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
@ -218,7 +266,7 @@ def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestU
|
||||
event_response = api_client.put(
|
||||
api_routes.recipes_timeline_events_item_id(new_event.id),
|
||||
json=updated_event_data,
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
@ -227,20 +275,27 @@ def test_timeline_event_message_alias(api_client: TestClient, unique_user: TestU
|
||||
assert updated_event.message == new_message
|
||||
|
||||
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_timeline_event_update_image(
|
||||
api_client: TestClient, unique_user: TestUser, recipes: list[Recipe], test_image_jpg: str
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
test_image_jpg: str,
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# create an event
|
||||
recipe = recipes[0]
|
||||
new_event_data = {
|
||||
"recipe_id": str(recipe.id),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"message": random_string(),
|
||||
"event_type": "info",
|
||||
}
|
||||
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=unique_user.token)
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=user.token)
|
||||
new_event = RecipeTimelineEventOut.model_validate(event_response.json())
|
||||
assert new_event.image == TimelineEventImage.does_not_have_image.value
|
||||
|
||||
@ -249,7 +304,7 @@ def test_timeline_event_update_image(
|
||||
api_routes.recipes_timeline_events_item_id_image(new_event.id),
|
||||
files={"image": ("test_image_jpg.jpg", f, "image/jpeg")},
|
||||
data={"extension": "jpg"},
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
@ -258,7 +313,7 @@ def test_timeline_event_update_image(
|
||||
|
||||
event_response = api_client.get(
|
||||
api_routes.recipes_timeline_events_item_id(new_event.id),
|
||||
headers=unique_user.token,
|
||||
headers=user.token,
|
||||
)
|
||||
assert event_response.status_code == 200
|
||||
|
||||
@ -269,23 +324,35 @@ def test_timeline_event_update_image(
|
||||
assert updated_event.image == TimelineEventImage.has_image.value
|
||||
|
||||
|
||||
def test_create_recipe_with_timeline_event(api_client: TestClient, unique_user: TestUser, recipes: list[Recipe]):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_create_recipe_with_timeline_event(
|
||||
api_client: TestClient,
|
||||
unique_user: TestUser,
|
||||
recipes: list[Recipe],
|
||||
h2_user: TestUser,
|
||||
use_other_household_user: bool,
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
# make sure when the recipes fixture was created that all recipes have at least one event
|
||||
for recipe in recipes:
|
||||
params = {"queryFilter": f"recipe_id={recipe.id}"}
|
||||
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=unique_user.token)
|
||||
events_response = api_client.get(api_routes.recipes_timeline_events, params=params, headers=user.token)
|
||||
events_pagination = RecipeTimelineEventPagination.model_validate(events_response.json())
|
||||
assert events_pagination.items
|
||||
|
||||
|
||||
def test_invalid_recipe_id(api_client: TestClient, unique_user: TestUser):
|
||||
@pytest.mark.parametrize("use_other_household_user", [True, False])
|
||||
def test_invalid_recipe_id(
|
||||
api_client: TestClient, unique_user: TestUser, h2_user: TestUser, use_other_household_user: bool
|
||||
):
|
||||
user = h2_user if use_other_household_user else unique_user
|
||||
new_event_data = {
|
||||
"recipe_id": str(uuid4()),
|
||||
"user_id": str(unique_user.user_id),
|
||||
"user_id": str(user.user_id),
|
||||
"subject": random_string(),
|
||||
"event_type": "info",
|
||||
"message": random_string(),
|
||||
}
|
||||
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=unique_user.token)
|
||||
event_response = api_client.post(api_routes.recipes_timeline_events, json=new_event_data, headers=user.token)
|
||||
assert event_response.status_code == 404
|
||||
|
Loading…
x
Reference in New Issue
Block a user