Skip to content

Commit 21cea10

Browse files
gregjkalkarimarie67
authored andcommitted
Fix bug with stale news moderation link and add tests
1 parent 6dbfb01 commit 21cea10

File tree

3 files changed

+107
-4
lines changed

3 files changed

+107
-4
lines changed

justfile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ alias shell := console
5353
@test_pytest: ## runs pytest
5454
-docker compose run --rm -e DEBUG_TOOLBAR="False" web pytest -s --create-db
5555

56+
@test_pytest_lf: ## runs last failed pytest tests
57+
-docker compose run --rm -e DEBUG_TOOLBAR="False" web pytest -s --create-db --lf
58+
5659
@test_pytest_asciidoctor: ## runs asciidoctor tests
5760
-docker compose run --rm -e DEBUG_TOOLBAR="False" web pytest -m asciidoctor -s --create-db
5861

news/tests/test_views.py

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,16 @@
55

66
from PIL import Image
77
import pytest
8+
from django.conf import settings
9+
from django.contrib.messages import get_messages
810
from django.core import mail
11+
from django.urls import reverse
912
from django.utils.text import slugify
1013
from django.utils.timezone import now
14+
from itsdangerous import URLSafeTimedSerializer, SignatureExpired
1115
from model_bakery import baker
1216

17+
from ..constants import NEWS_APPROVAL_SALT
1318
from ..forms import BlogPostForm, LinkForm, NewsForm, PollForm, VideoForm
1419
from ..models import NEWS_MODELS, BlogPost, Entry, Link, News, Poll, Video
1520
from ..notifications import (
@@ -847,3 +852,95 @@ def test_news_delete(tp, make_entry, moderator_user, model_class):
847852
tp.response_200(response)
848853
tp.assertRedirects(response, tp.reverse("news"))
849854
assert Entry.objects.filter(pk=entry.pk).count() == 0
855+
856+
857+
@pytest.mark.django_db
858+
@pytest.mark.parametrize(
859+
"already_approved, expected_message_substring",
860+
[
861+
(False, "approved"),
862+
(True, "already been approved"),
863+
],
864+
)
865+
def test_magic_link_valid_token(
866+
tp, make_entry, moderator_user, already_approved, expected_message_substring
867+
):
868+
"""Valid magic link approves unapproved entries or warns if already approved."""
869+
870+
entry = make_entry(approved=already_approved)
871+
serializer = URLSafeTimedSerializer(settings.SECRET_KEY)
872+
token = serializer.dumps(
873+
{"entry_slug": entry.slug, "moderator_id": moderator_user.id},
874+
salt=NEWS_APPROVAL_SALT,
875+
)
876+
877+
url = f"/news/moderate/magic/{token}/"
878+
response = tp.get(url)
879+
entry.refresh_from_db()
880+
881+
if already_approved:
882+
assert entry.is_approved
883+
assert entry.moderator != moderator_user
884+
else:
885+
assert entry.is_approved
886+
assert entry.moderator == moderator_user
887+
888+
tp.assertRedirects(response, entry.get_absolute_url())
889+
890+
msgs = [(m.level_tag, m.message) for m in get_messages(response.wsgi_request)]
891+
assert any(expected_message_substring in message for _, message in msgs)
892+
893+
894+
@pytest.mark.django_db
895+
@pytest.mark.parametrize(
896+
"authenticated, expected_redirect",
897+
[
898+
(
899+
False,
900+
lambda tp: f"{reverse('account_login')}?next={reverse('news-moderate')}",
901+
),
902+
(
903+
True,
904+
lambda tp: reverse("news-moderate"),
905+
),
906+
],
907+
)
908+
def test_magic_link_expired_token(
909+
tp, make_entry, moderator_user, monkeypatch, authenticated, expected_redirect
910+
):
911+
"""Expired magic link redirects appropriately for authenticated vs. anonymous users."""
912+
913+
entry = make_entry(approved=False)
914+
serializer = URLSafeTimedSerializer(settings.SECRET_KEY)
915+
token = serializer.dumps(
916+
{"entry_slug": entry.slug, "moderator_id": moderator_user.id},
917+
salt=NEWS_APPROVAL_SALT,
918+
)
919+
920+
monkeypatch.setattr(
921+
"news.views.URLSafeTimedSerializer.loads",
922+
lambda *a, **k: (_ for _ in ()).throw(SignatureExpired("expired")),
923+
)
924+
925+
url = f"/news/moderate/magic/{token}/"
926+
if authenticated:
927+
with tp.login(moderator_user):
928+
response = tp.get(url, follow=True)
929+
else:
930+
response = tp.get(url, follow=True)
931+
932+
tp.assertRedirects(response, expected_redirect(tp))
933+
msgs = [(m.level_tag, m.message) for m in get_messages(response.wsgi_request)]
934+
assert any("expired" in message.lower() for _, message in msgs)
935+
936+
937+
@pytest.mark.django_db
938+
def test_magic_link_invalid_token_returns_403(tp):
939+
"""Invalid magic link returns HTTP 403 Forbidden with an error message."""
940+
941+
invalid_token = "not-a-valid-token"
942+
url = reverse("news-magic-approve", args=[invalid_token])
943+
response = tp.get(url, follow=False)
944+
945+
tp.response_403(response)
946+
assert "Invalid magic link" in response.content.decode()

news/views.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,10 @@ def get_context_data(self, **kwargs):
150150
context["next_url"] = next_url
151151
context["next"] = get_published_or_none(self.object.get_next_by_publish_at)
152152
context["prev"] = get_published_or_none(self.object.get_previous_by_publish_at)
153-
category_kwarg = {f"{self.object.tag}__isnull": False}
153+
if self.object.tag:
154+
category_kwarg = {f"{self.object.tag}__isnull": False}
155+
else:
156+
category_kwarg = {}
154157
context["next_in_category"] = get_published_or_none(
155158
partial(self.object.get_next_by_publish_at, **category_kwarg)
156159
)
@@ -180,10 +183,10 @@ def get(self, request, token, *args, **kwargs):
180183
moderator = User.objects.get(id=moderator_id)
181184
except SignatureExpired:
182185
message = _("This link has expired.")
183-
if not request.user.is_authenticated():
186+
if not request.user.is_authenticated:
184187
message += _(" Please login to continue.")
185188
messages.warning(request, message)
186-
return redirect(reverse_lazy("news-moderate"), permanent=True)
189+
return redirect(reverse_lazy("news-moderate"))
187190
except (BadData, User.DoesNotExist):
188191
return HttpResponseForbidden("Invalid magic link.")
189192

@@ -195,7 +198,7 @@ def get(self, request, token, *args, **kwargs):
195198
except Entry.AlreadyApprovedError:
196199
messages.warning(request, _("This entry has already been approved."))
197200

198-
return redirect(entry, permanent=True)
201+
return redirect(entry)
199202

200203

201204
class EntryCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):

0 commit comments

Comments
 (0)