Files
Kyle DammannandClaude Opus 5.5 9256e94286 Add phase 3: comments and cooking mode
- Comments on visible recipes; author and admin may delete; per-user
  hourly rate limit (COMMENT_RATE_LIMIT_PER_HOUR)
- Cooking mode with large text, tick-off checklists, serving scaling and
  Screen Wake Lock to keep the display on
- Serving calculator auto-submits on change
- Privacy tests cover comment and cooking-mode URLs

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:39:15 +01:00

151 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Acceptance: a private recipe is reachable by its owner only – in no view and under no URL,
including image URLs. Other users and admins get 404 (indistinguishable from "does not exist")."""
import pytest
from django.test import Client
from django.urls import reverse
from recipes.models import Comment, Favorite, Recipe, RecipeImage
from .recipe_helpers import image_upload, make_recipe
pytestmark = pytest.mark.django_db
SECRET_TITLE = "Geheimrezept Omas Gulasch"
SECRET_INGREDIENT = "Zauberpaprika"
SECRET_TAG = "geheimtag"
@pytest.fixture
def private_recipe(user):
recipe = make_recipe(user, title=SECRET_TITLE, visibility=Recipe.Visibility.PRIVATE,
ingredients=f"500 g {SECRET_INGREDIENT}", tags=(SECRET_TAG,))
owner_client(user).post(reverse("recipes:photo_upload", args=[recipe.pk]), {"photos": [image_upload()]})
assert recipe.images.count() == 1
Comment.objects.create(recipe=recipe, author=user, text="Eigene Notiz im Kommentar")
return recipe
@pytest.fixture
def image(private_recipe):
return RecipeImage.objects.get(recipe=private_recipe)
@pytest.fixture(params=["other", "admin"])
def outsider(request, other_user, admin_user):
c = Client()
c.force_login(other_user if request.param == "other" else admin_user)
return c
def owner_client(user):
c = Client()
c.force_login(user)
return c
def get_urls(recipe, image):
return [
reverse("recipes:detail", args=[recipe.pk]),
reverse("recipes:detail", args=[recipe.pk]) + "?portionen=8",
reverse("recipes:edit", args=[recipe.pk]),
reverse("recipes:delete", args=[recipe.pk]),
reverse("recipes:photo", args=[image.uuid, "gross"]),
reverse("recipes:photo", args=[image.uuid, "klein"]),
*extra_get_urls(recipe, image),
]
def post_urls(recipe, image):
return [
reverse("recipes:edit", args=[recipe.pk]),
reverse("recipes:delete", args=[recipe.pk]),
reverse("recipes:favorite", args=[recipe.pk]),
reverse("recipes:photo_upload", args=[recipe.pk]),
reverse("recipes:photo_cover", args=[recipe.pk, image.pk]),
reverse("recipes:photo_delete", args=[recipe.pk, image.pk]),
*extra_post_urls(recipe, image),
]
def extra_get_urls(recipe, image):
return [reverse("recipes:cook", args=[recipe.pk]), reverse("recipes:cook", args=[recipe.pk]) + "?portionen=2"]
def extra_post_urls(recipe, image):
comment = recipe.comments.first()
return [
reverse("recipes:comment_create", args=[recipe.pk]),
reverse("recipes:comment_delete", args=[recipe.pk, comment.pk]),
]
def test_owner_sees_everything(user, private_recipe, image):
c = owner_client(user)
for url in get_urls(private_recipe, image):
assert c.get(url).status_code == 200, url
assert SECRET_TITLE in c.get(reverse("recipes:list")).content.decode()
def test_outsider_gets_404_on_every_get_url(outsider, private_recipe, image):
for url in get_urls(private_recipe, image):
assert outsider.get(url).status_code == 404, url
def test_outsider_gets_404_on_every_post_url(outsider, private_recipe, image):
for url in post_urls(private_recipe, image):
response = outsider.post(url, {"title": "gehackt", "servings": 1, "visibility": "public", "text": "x"})
assert response.status_code == 404, url
private_recipe.refresh_from_db()
assert private_recipe.title == SECRET_TITLE and not private_recipe.is_public
assert RecipeImage.objects.filter(pk=image.pk).exists()
assert not Favorite.objects.exists()
assert list(private_recipe.comments.values_list("text", flat=True)) == ["Eigene Notiz im Kommentar"]
@pytest.mark.parametrize(
"query",
[
{},
{"q": "Geheimrezept"},
{"q": SECRET_INGREDIENT},
{"q": SECRET_TAG},
{"zutat": SECRET_INGREDIENT},
{"tag": SECRET_TAG},
{"ansicht": "favoriten"},
],
)
def test_private_recipe_absent_from_list_and_search(outsider, private_recipe, image, query):
html = outsider.get(reverse("recipes:list"), query).content.decode()
assert SECRET_TITLE not in html
assert str(image.uuid) not in html
assert reverse("recipes:detail", args=[private_recipe.pk]) not in html
def test_private_tag_not_listed(outsider, private_recipe):
assert f"#{SECRET_TAG}" not in outsider.get(reverse("recipes:list")).content.decode()
def test_existing_favorite_disappears_when_recipe_turns_private(user, other_user):
recipe = make_recipe(user, title="Bald privat")
Favorite.objects.create(user=other_user, recipe=recipe)
recipe.visibility = Recipe.Visibility.PRIVATE
recipe.save()
c = owner_client(other_user)
assert "Bald privat" not in c.get(reverse("recipes:list"), {"ansicht": "favoriten"}).content.decode()
assert c.get(reverse("recipes:detail", args=[recipe.pk])).status_code == 404
def test_anonymous_gets_redirected_everywhere(private_recipe, image):
c = Client()
for url in get_urls(private_recipe, image):
response = c.get(url)
assert response.status_code == 302 and "/anmelden/" in response["Location"], url
def test_visible_to_queryset_rule(user, other_user, admin_user, private_recipe):
public = make_recipe(user, title="Öffentlich")
assert set(Recipe.objects.visible_to(user)) == {private_recipe, public}
assert set(Recipe.objects.visible_to(other_user)) == {public}
assert set(Recipe.objects.visible_to(admin_user)) == {public}