|
5 | 5 |
|
6 | 6 | from PIL import Image |
7 | 7 | import pytest |
| 8 | +from django.conf import settings |
| 9 | +from django.contrib.messages import get_messages |
8 | 10 | from django.core import mail |
| 11 | +from django.urls import reverse |
9 | 12 | from django.utils.text import slugify |
10 | 13 | from django.utils.timezone import now |
| 14 | +from itsdangerous import URLSafeTimedSerializer, SignatureExpired |
11 | 15 | from model_bakery import baker |
12 | 16 |
|
| 17 | +from ..constants import NEWS_APPROVAL_SALT |
13 | 18 | from ..forms import BlogPostForm, LinkForm, NewsForm, PollForm, VideoForm |
14 | 19 | from ..models import NEWS_MODELS, BlogPost, Entry, Link, News, Poll, Video |
15 | 20 | from ..notifications import ( |
@@ -847,3 +852,95 @@ def test_news_delete(tp, make_entry, moderator_user, model_class): |
847 | 852 | tp.response_200(response) |
848 | 853 | tp.assertRedirects(response, tp.reverse("news")) |
849 | 854 | 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() |
0 commit comments