fix(spec): enforce read-time budget and re-render invalid comment submissions
All checks were successful
CI / nightly-e2e (pull_request) Has been skipped
CI / ci (pull_request) Successful in 1m1s

This commit is contained in:
Mark
2026-02-28 17:36:34 +00:00
parent cfe0cbca62
commit c4fde90a9c
4 changed files with 52 additions and 9 deletions

View File

@@ -63,6 +63,34 @@ def test_comment_post_rejected_when_comments_disabled(client, home_page):
assert Comment.objects.count() == 0 assert Comment.objects.count() == 0
@pytest.mark.django_db
def test_invalid_comment_post_rerenders_form_with_errors(client, home_page):
cache.clear()
index = ArticleIndexPage(title="Articles", slug="articles")
home_page.add_child(instance=index)
author = AuthorFactory()
article = ArticlePage(title="A", slug="a", author=author, summary="s", body=[("rich_text", "<p>body</p>")])
index.add_child(instance=article)
article.save_revision().publish()
resp = client.post(
"/comments/post/",
{
"article_id": article.id,
"author_name": "Test",
"author_email": "test@example.com",
"body": " ",
"honeypot": "",
},
)
assert resp.status_code == 200
assert b'aria-label="Comment form errors"' in resp.content
assert b'value="Test"' in resp.content
assert b"test@example.com" in resp.content
assert Comment.objects.count() == 0
@pytest.mark.django_db @pytest.mark.django_db
def test_comment_reply_depth_is_enforced(client, home_page): def test_comment_reply_depth_is_enforced(client, home_page):
cache.clear() cache.clear()
@@ -100,7 +128,8 @@ def test_comment_reply_depth_is_enforced(client, home_page):
"honeypot": "", "honeypot": "",
}, },
) )
assert resp.status_code == 302 assert resp.status_code == 200
assert b"Reply depth exceeds the allowed limit" in resp.content
assert Comment.objects.count() == 2 assert Comment.objects.count() == 2

View File

@@ -5,7 +5,7 @@ from django.contrib import messages
from django.core.cache import cache from django.core.cache import cache
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect, render
from django.views import View from django.views import View
from apps.blog.models import ArticlePage from apps.blog.models import ArticlePage
@@ -23,6 +23,12 @@ def client_ip_from_request(request) -> str:
class CommentCreateView(View): class CommentCreateView(View):
def _render_article_with_errors(self, request, article, form):
context = article.get_context(request)
context["page"] = article
context["comment_form"] = form
return render(request, "blog/article_page.html", context, status=200)
def post(self, request): def post(self, request):
ip = client_ip_from_request(request) ip = client_ip_from_request(request)
key = f"comment-rate:{ip}" key = f"comment-rate:{ip}"
@@ -48,11 +54,10 @@ class CommentCreateView(View):
try: try:
comment.full_clean() comment.full_clean()
except ValidationError: except ValidationError:
messages.error(request, "Reply depth exceeds the allowed limit") form.add_error(None, "Reply depth exceeds the allowed limit")
return redirect(article.url) return self._render_article_with_errors(request, article, form)
comment.save() comment.save()
messages.success(request, "Your comment is awaiting moderation") messages.success(request, "Your comment is awaiting moderation")
return redirect(f"{article.url}?commented=1") return redirect(f"{article.url}?commented=1")
messages.error(request, "Please correct the form errors") return self._render_article_with_errors(request, article, form)
return redirect(article.url)

View File

@@ -72,3 +72,4 @@ def test_read_time_benchmark(benchmark):
result = benchmark(article._compute_read_time) result = benchmark(article._compute_read_time)
assert result >= 1 assert result >= 1
assert benchmark.stats.stats.mean < 0.05

View File

@@ -69,12 +69,20 @@
{% empty %} {% empty %}
<p>No comments yet.</p> <p>No comments yet.</p>
{% endfor %} {% endfor %}
{% if comment_form and comment_form.errors %}
<div aria-label="Comment form errors">
{{ comment_form.non_field_errors }}
{% for field in comment_form %}
{{ field.errors }}
{% endfor %}
</div>
{% endif %}
<form method="post" action="{% url 'comment_post' %}"> <form method="post" action="{% url 'comment_post' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="article_id" value="{{ page.id }}" /> <input type="hidden" name="article_id" value="{{ page.id }}" />
<input type="text" name="author_name" required /> <input type="text" name="author_name" value="{% if comment_form %}{{ comment_form.author_name.value|default:'' }}{% endif %}" required />
<input type="email" name="author_email" required /> <input type="email" name="author_email" value="{% if comment_form %}{{ comment_form.author_email.value|default:'' }}{% endif %}" required />
<textarea name="body" required></textarea> <textarea name="body" required>{% if comment_form %}{{ comment_form.body.value|default:'' }}{% endif %}</textarea>
<input type="text" name="honeypot" style="display:none" /> <input type="text" name="honeypot" style="display:none" />
<button type="submit">Post comment</button> <button type="submit">Post comment</button>
</form> </form>