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]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
6f7e4f92ca
commit
9256e94286
@@ -0,0 +1,96 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from recipes.models import Comment
|
||||
|
||||
from .recipe_helpers import make_recipe
|
||||
|
||||
pytestmark = pytest.mark.django_db
|
||||
|
||||
|
||||
def post_comment(client, recipe, text="Sehr lecker!"):
|
||||
return client.post(reverse("recipes:comment_create", args=[recipe.pk]), {"text": text})
|
||||
|
||||
|
||||
def test_user_comments_on_public_recipe(client, user, other_user):
|
||||
recipe = make_recipe(user)
|
||||
client.force_login(other_user)
|
||||
response = post_comment(client, recipe)
|
||||
comment = Comment.objects.get()
|
||||
assert response["Location"].endswith(f"#kommentar-{comment.pk}")
|
||||
assert comment.author == other_user
|
||||
assert "Sehr lecker!" in client.get(reverse("recipes:detail", args=[recipe.pk])).content.decode()
|
||||
|
||||
|
||||
def test_empty_comment_rejected(user_client, user):
|
||||
recipe = make_recipe(user)
|
||||
assert post_comment(user_client, recipe, "").status_code == 400
|
||||
assert not Comment.objects.exists()
|
||||
|
||||
|
||||
def test_comment_too_long_rejected(user_client, user):
|
||||
recipe = make_recipe(user)
|
||||
assert post_comment(user_client, recipe, "x" * 2001).status_code == 400
|
||||
|
||||
|
||||
def test_author_and_admin_can_delete(client, user, other_user, admin_user):
|
||||
recipe = make_recipe(user)
|
||||
first = Comment.objects.create(recipe=recipe, author=other_user, text="eins")
|
||||
second = Comment.objects.create(recipe=recipe, author=other_user, text="zwei")
|
||||
client.force_login(other_user)
|
||||
client.post(reverse("recipes:comment_delete", args=[recipe.pk, first.pk]))
|
||||
client.force_login(admin_user)
|
||||
client.post(reverse("recipes:comment_delete", args=[recipe.pk, second.pk]))
|
||||
assert not Comment.objects.exists()
|
||||
|
||||
|
||||
def test_others_cannot_delete_comment(client, user, other_user):
|
||||
"""Not even the recipe owner may delete someone else's comment (spec: author and admin only)."""
|
||||
recipe = make_recipe(user)
|
||||
comment = Comment.objects.create(recipe=recipe, author=other_user, text="bleibt")
|
||||
client.force_login(user)
|
||||
assert client.post(reverse("recipes:comment_delete", args=[recipe.pk, comment.pk])).status_code == 404
|
||||
assert Comment.objects.exists()
|
||||
|
||||
|
||||
def test_delete_button_only_where_allowed(client, user, other_user):
|
||||
recipe = make_recipe(user)
|
||||
comment = Comment.objects.create(recipe=recipe, author=other_user, text="hallo")
|
||||
delete_url = reverse("recipes:comment_delete", args=[recipe.pk, comment.pk])
|
||||
client.force_login(user)
|
||||
assert delete_url not in client.get(reverse("recipes:detail", args=[recipe.pk])).content.decode()
|
||||
client.force_login(other_user)
|
||||
assert delete_url in client.get(reverse("recipes:detail", args=[recipe.pk])).content.decode()
|
||||
|
||||
|
||||
def test_comment_rate_limit(user_client, user, settings):
|
||||
settings.COMMENT_RATE_LIMIT_PER_HOUR = 2
|
||||
recipe = make_recipe(user)
|
||||
for i in range(3):
|
||||
post_comment(user_client, recipe, f"Kommentar {i}")
|
||||
assert Comment.objects.count() == 2
|
||||
|
||||
|
||||
def test_comment_text_is_escaped(user_client, user):
|
||||
recipe = make_recipe(user)
|
||||
post_comment(user_client, recipe, "<script>alert(1)</script>")
|
||||
html = user_client.get(reverse("recipes:detail", args=[recipe.pk])).content.decode()
|
||||
assert "<script>alert(1)</script>" not in html
|
||||
|
||||
|
||||
def test_cook_mode_scales(user_client, user):
|
||||
recipe = make_recipe(user, ingredients="200 g Mehl")
|
||||
html = user_client.get(reverse("recipes:cook", args=[recipe.pk]), {"portionen": 8}).content.decode()
|
||||
assert "400 g" in html and "data-wakelock" in html
|
||||
|
||||
|
||||
def test_detail_scaling(user_client, user):
|
||||
recipe = make_recipe(user, ingredients="600 g Kartoffeln\n1 ½ TL Salz")
|
||||
html = user_client.get(reverse("recipes:detail", args=[recipe.pk]), {"portionen": 8}).content.decode()
|
||||
assert "1,2 kg" in html and "3 TL" in html
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["abc", "-5", "0", "99999"])
|
||||
def test_scaling_with_bad_input(user_client, user, value):
|
||||
recipe = make_recipe(user)
|
||||
assert user_client.get(reverse("recipes:detail", args=[recipe.pk]), {"portionen": value}).status_code == 200
|
||||
@@ -5,7 +5,7 @@ import pytest
|
||||
from django.test import Client
|
||||
from django.urls import reverse
|
||||
|
||||
from recipes.models import Favorite, Recipe, RecipeImage
|
||||
from recipes.models import Comment, Favorite, Recipe, RecipeImage
|
||||
|
||||
from .recipe_helpers import image_upload, make_recipe
|
||||
|
||||
@@ -22,6 +22,7 @@ def private_recipe(user):
|
||||
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
|
||||
|
||||
|
||||
@@ -68,12 +69,15 @@ def post_urls(recipe, image):
|
||||
|
||||
|
||||
def extra_get_urls(recipe, image):
|
||||
"""Hook for later features (comments, cooking mode, …)."""
|
||||
return []
|
||||
return [reverse("recipes:cook", args=[recipe.pk]), reverse("recipes:cook", args=[recipe.pk]) + "?portionen=2"]
|
||||
|
||||
|
||||
def extra_post_urls(recipe, image):
|
||||
return []
|
||||
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):
|
||||
@@ -96,6 +100,7 @@ def test_outsider_gets_404_on_every_post_url(outsider, private_recipe, image):
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user