mirror of
https://github.com/boostorg/website-v2.git
synced 2026-01-19 04:42:17 +00:00
947 lines
32 KiB
Python
947 lines
32 KiB
Python
import os
|
|
import uuid
|
|
from datetime import date, timedelta
|
|
from io import BytesIO
|
|
|
|
from PIL import Image
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.contrib.messages import get_messages
|
|
from django.core import mail
|
|
from django.urls import reverse
|
|
from django.utils.text import slugify
|
|
from django.utils.timezone import now
|
|
from itsdangerous import URLSafeTimedSerializer, SignatureExpired
|
|
from model_bakery import baker
|
|
|
|
from ..constants import NEWS_APPROVAL_SALT
|
|
from ..forms import BlogPostForm, LinkForm, NewsForm, PollForm, VideoForm
|
|
from ..models import NEWS_MODELS, BlogPost, Entry, Link, News, Poll, Video
|
|
from ..notifications import (
|
|
send_email_news_approved,
|
|
send_email_news_needs_moderation,
|
|
send_email_news_posted,
|
|
)
|
|
from ..views import datefilter, display_publish_at
|
|
|
|
|
|
def test_display_publish_at_now():
|
|
since = now()
|
|
# Now or future
|
|
assert display_publish_at(since, since) == "now"
|
|
assert display_publish_at(since + timedelta(seconds=1), since) == "now"
|
|
assert display_publish_at(since - timedelta(seconds=1), since) == "now"
|
|
assert display_publish_at(since - timedelta(minutes=30), since) == "now"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"publish_at, expected",
|
|
[
|
|
# An hour ago (up to 24 hours)
|
|
(timedelta(minutes=31), "an hour ago"),
|
|
(timedelta(minutes=59), "an hour ago"),
|
|
(timedelta(hours=1), "an hour ago"),
|
|
(timedelta(hours=1, seconds=1), "an hour ago"),
|
|
(timedelta(hours=1, minutes=31), "2 hours ago"),
|
|
(timedelta(hours=1, minutes=59), "2 hours ago"),
|
|
(timedelta(hours=1, minutes=60), "2 hours ago"),
|
|
(timedelta(hours=23, minutes=29), "23 hours ago"),
|
|
# 3 days ago (up to 7 days)
|
|
(timedelta(hours=23, minutes=31), "1 day ago"),
|
|
(timedelta(hours=24, minutes=00), "1 day ago"),
|
|
(timedelta(days=1), "1 day ago"),
|
|
(timedelta(days=1, hours=24), "2 days ago"),
|
|
(timedelta(days=6, hours=23, minutes=29), "6 days ago"),
|
|
],
|
|
)
|
|
def test_display_publish_at_days_ago(publish_at, expected):
|
|
since = now()
|
|
assert display_publish_at(since - publish_at, since) == expected
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"publish_at",
|
|
[
|
|
timedelta(days=6, hours=24),
|
|
timedelta(days=7),
|
|
timedelta(days=7, seconds=1),
|
|
timedelta(days=20),
|
|
],
|
|
)
|
|
def test_display_publish_at_datefilter(publish_at):
|
|
since = now()
|
|
# June 13th, 2023 (after 7 days)
|
|
target = since - publish_at
|
|
assert display_publish_at(target, since) == datefilter(target, "M jS, Y")
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url_name, model_class",
|
|
[
|
|
("news", Entry),
|
|
("news-blogpost-list", BlogPost),
|
|
("news-link-list", Link),
|
|
("news-news-list", News),
|
|
("news-poll-list", Poll),
|
|
("news-video-list", Video),
|
|
],
|
|
)
|
|
def test_entry_list(
|
|
tp, make_entry, regular_user, url_name, model_class, authenticated=False
|
|
):
|
|
"""List published news for non authenticated users."""
|
|
not_approved_news = make_entry(
|
|
model_class, approved=False, title="needs moderation"
|
|
)
|
|
yesterday_news = make_entry(
|
|
model_class,
|
|
approved=True,
|
|
title="old news",
|
|
publish_at=now() - timedelta(days=1),
|
|
)
|
|
today_news = make_entry(
|
|
model_class, approved=True, title="current news", publish_at=now()
|
|
)
|
|
tomorrow_news = make_entry(
|
|
model_class,
|
|
approved=True,
|
|
title="future news",
|
|
publish_at=now() + timedelta(days=1),
|
|
)
|
|
|
|
if authenticated:
|
|
tp.login(regular_user)
|
|
|
|
# 10 queries if authenticated, less otherwise
|
|
response = tp.assertGoodView(
|
|
tp.reverse(url_name), test_query_count=10, verbose=True
|
|
)
|
|
|
|
expected = [today_news, yesterday_news]
|
|
assert list(response.context.get("entry_list", [])) == expected
|
|
|
|
content = str(response.content)
|
|
for entry in expected:
|
|
assert entry.title in content
|
|
formatted_date = str(display_publish_at(entry.publish_at))
|
|
assert formatted_date in content
|
|
# link_with_date = f'<a href="{entry.get_absolute_url()}">{formatted_date}</a>'
|
|
# tp.assertResponseContains(link_with_date, response)
|
|
if entry.tag:
|
|
assert entry.tag in content # this is the tag
|
|
|
|
assert not_approved_news.get_absolute_url() not in content
|
|
assert not_approved_news.title not in content
|
|
assert tomorrow_news.get_absolute_url() not in content
|
|
assert tomorrow_news.title not in content
|
|
|
|
# If user is not authenticated, the Create News link should not be shown
|
|
assert (tp.reverse("news-create") in content) == authenticated
|
|
|
|
|
|
@pytest.mark.skip(
|
|
reason="Fails; prefer to skip test functions and not the whole test suite"
|
|
)
|
|
def test_entry_list_queries(tp, make_entry):
|
|
expected = [
|
|
make_entry(model_class)
|
|
for model_class in NEWS_MODELS
|
|
for i in range(len(model_class.__name__))
|
|
]
|
|
|
|
# 4 queries
|
|
response = tp.assertGoodView(tp.reverse("news"), test_query_count=29, verbose=True)
|
|
|
|
entry_list = response.context.get("entry_list", [])
|
|
assert set(e.id for e in entry_list) == set(e.id for e in expected)
|
|
|
|
content = str(response.content)
|
|
for entry in expected:
|
|
assert entry.get_absolute_url() in content
|
|
assert entry.title in content
|
|
news_link = f'href="/news/{entry.tag}/"' if entry.tag else 'href="/news/"'
|
|
assert news_link in content
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url_name, model_class",
|
|
[
|
|
("news", Entry),
|
|
("news-blogpost-list", BlogPost),
|
|
("news-link-list", Link),
|
|
("news-news-list", News),
|
|
("news-poll-list", Poll),
|
|
("news-video-list", Video),
|
|
],
|
|
)
|
|
def test_entry_list_authenticated(tp, make_entry, url_name, model_class, regular_user):
|
|
test_entry_list(
|
|
tp, make_entry, regular_user, url_name, model_class, authenticated=True
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(
|
|
reason="Fails; prefer to skip test functions and not the whole test suite"
|
|
)
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
@pytest.mark.parametrize("with_image", [False, True])
|
|
def test_news_detail(tp, make_entry, model_class, with_image):
|
|
"""Browse details for a given news entry."""
|
|
a_past_date = now() - timedelta(hours=10)
|
|
news = make_entry(
|
|
model_class,
|
|
approved=True,
|
|
publish_at=a_past_date,
|
|
_fill_optional=True,
|
|
_create_files=with_image,
|
|
)
|
|
url = tp.reverse("news-detail", news.slug)
|
|
|
|
response = tp.get(url)
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert news.title in content
|
|
assert news.content in content
|
|
if with_image:
|
|
assert news.image
|
|
assert f'<img src="{news.image.url}"' in content
|
|
else:
|
|
assert not news.image
|
|
assert '<img src="' not in content
|
|
assert tp.reverse("news-approve", news.slug) not in content
|
|
assert tp.reverse("news-delete", news.slug) not in content
|
|
assert tp.reverse("news-update", news.slug) not in content
|
|
|
|
# no next nor prev links
|
|
assert "newer entries" not in content.lower()
|
|
assert "older entries" not in content.lower()
|
|
|
|
# create an older news, likely different type
|
|
older_date = a_past_date - timedelta(hours=1)
|
|
older = make_entry(approved=True, publish_at=older_date)
|
|
|
|
response = tp.get(url)
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert "newer entries" not in content.lower()
|
|
assert "older entries" in content.lower()
|
|
assert older.get_absolute_url() in content
|
|
|
|
# create a newer news, but still older than now so it's shown
|
|
newer_date = a_past_date + timedelta(hours=1)
|
|
assert newer_date < now()
|
|
newer = make_entry(approved=True, publish_at=newer_date)
|
|
|
|
response = tp.get(url)
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert "newer entries" in content.lower()
|
|
assert "older entries" in content.lower()
|
|
assert newer.get_absolute_url() in content
|
|
|
|
|
|
def test_news_detail_404(tp):
|
|
"""No news is good news."""
|
|
url = tp.reverse("news-detail", "not-there")
|
|
response = tp.get(url)
|
|
tp.response_404(response)
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_detail_404_if_not_published(tp, make_entry, regular_user, model_class):
|
|
"""Details for a news entry are available if published or authored."""
|
|
news = make_entry(model_class, published=False)
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_404(response)
|
|
|
|
# even if logged in, a regular user can not access the unpublished news
|
|
with tp.login(regular_user):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_404(response)
|
|
|
|
# but the entry author can access it even if unpublished
|
|
with tp.login(news.author):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_200(response)
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_detail_actions_author(tp, make_entry, model_class):
|
|
"""News entry is updatable by authors (if not approved)."""
|
|
news = make_entry(model_class, approved=False) # not approved entry
|
|
with tp.login(news.author):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert tp.reverse("news-approve", news.slug) not in content
|
|
assert tp.reverse("news-delete", news.slug) not in content
|
|
assert tp.reverse("news-update", news.slug) in content
|
|
|
|
news.approve(baker.make("users.User"))
|
|
with tp.login(news.author):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert tp.reverse("news-approve", news.slug) not in content
|
|
assert tp.reverse("news-delete", news.slug) not in content
|
|
assert tp.reverse("news-update", news.slug) not in content
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_detail_actions_moderator(tp, make_entry, moderator_user, model_class):
|
|
"""Moderators can update, delete and approve a news entry."""
|
|
news = make_entry(model_class, approved=False) # not approved entry
|
|
with tp.login(moderator_user):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert tp.reverse("news-approve", news.slug) in content
|
|
assert tp.reverse("news-delete", news.slug) in content
|
|
assert tp.reverse("news-update", news.slug) in content
|
|
|
|
news.approve(baker.make("users.User"))
|
|
with tp.login(moderator_user):
|
|
response = tp.get(news.get_absolute_url())
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert tp.reverse("news-approve", news.slug) not in content
|
|
assert tp.reverse("news-delete", news.slug) in content
|
|
assert tp.reverse("news-update", news.slug) in content
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_detail_next_url(tp, make_entry, moderator_user, model_class):
|
|
news = make_entry(model_class, approved=False)
|
|
with tp.login(moderator_user):
|
|
response = tp.get(news.get_absolute_url() + "?next=/foo")
|
|
tp.response_200(response)
|
|
tp.assertContext("next_url", "/foo")
|
|
tp.assertResponseContains(
|
|
'<input type="hidden" name="next" value="/foo" />', response
|
|
)
|
|
|
|
# unsafe URLs are not put in the context for future redirection
|
|
with tp.login(moderator_user):
|
|
response = tp.get(news.get_absolute_url() + "?next=http://example.com")
|
|
tp.response_200(response)
|
|
tp.assertNotIn("next_url", response.context)
|
|
tp.assertResponseNotContains(
|
|
'<input type="hidden" name="next" value="http://example.com" />', response
|
|
)
|
|
|
|
|
|
@pytest.mark.skip(
|
|
reason="Fails; prefer to skip test functions and not the whole test suite"
|
|
)
|
|
@pytest.mark.parametrize("user_type", ["user", "moderator_user"])
|
|
def test_news_create_multiplexer(tp, user_type, request):
|
|
url_name = "news-create"
|
|
url = tp.reverse(url_name)
|
|
tp.assertLoginRequired(url_name)
|
|
|
|
user = request.getfixturevalue(user_type)
|
|
with tp.login(user):
|
|
tp.assertGoodView(url, test_query_count=5, verbose=True)
|
|
|
|
expected = [BlogPostForm, LinkForm, NewsForm, VideoForm]
|
|
if "moderator" in user_type:
|
|
expected.append(PollForm)
|
|
items = tp.get_context("items")
|
|
assert len(items) == len(expected)
|
|
|
|
for item, form_class in zip(items, expected):
|
|
assert isinstance(item["form"], form_class)
|
|
model_class = form_class.Meta.model
|
|
# assert item["add_url_name"] == f"news-{model_class.news_type}-create"
|
|
# assert model_class.__name__ in item["add_label"]
|
|
assert model_class.__name__ == item["model_name"]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"has_image, has_display_name, should_redirect",
|
|
[
|
|
(True, True, False), # Has image, display name
|
|
(False, True, True), # Missing image
|
|
(True, False, True), # Missing names
|
|
(False, False, True), # Missing everything
|
|
],
|
|
)
|
|
def test_news_create_requirements(
|
|
tp, user, has_image, has_display_name, should_redirect
|
|
):
|
|
"""Users must have a profile photo and at least one of the names: first or last."""
|
|
url_name = "news-create"
|
|
url = tp.reverse(url_name)
|
|
|
|
# Setup user based on parameters
|
|
if has_image:
|
|
file = BytesIO()
|
|
filename = "test_image.jpg"
|
|
file.name = filename
|
|
image = Image.new("RGB", size=(100, 100), color=(155, 0, 0))
|
|
image.save(file, "jpeg")
|
|
file.seek(0)
|
|
user.profile_image.save(filename, file)
|
|
else:
|
|
user.profile_image = None
|
|
|
|
user.display_name = "Test User" if has_display_name else ""
|
|
user.save()
|
|
|
|
with tp.login(user):
|
|
response = tp.get(url)
|
|
|
|
if should_redirect:
|
|
tp.response_302(response)
|
|
assert response.url == tp.reverse("profile-account")
|
|
else:
|
|
tp.response_200(response)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"method", ["DELETE", "HEAD", "OPTIONS", "PATCH", "POST", "PUT"]
|
|
)
|
|
def test_news_create_multiplexer_method_not_allowed(tp, user, method):
|
|
# Any HTTP method other than GET is a 405
|
|
with tp.login(user):
|
|
response = tp.request(method_name=method.lower(), url_name="news-create")
|
|
|
|
tp.response_405(response)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"url_name, form_class",
|
|
[
|
|
("news-news-create", NewsForm),
|
|
("news-blogpost-create", BlogPostForm),
|
|
("news-link-create", LinkForm),
|
|
("news-poll-create", PollForm),
|
|
("news-video-create", VideoForm),
|
|
],
|
|
)
|
|
def test_news_create_get(tp, regular_user, url_name, form_class):
|
|
# assertLoginRequired expects a non resolved URL, that is an URL name
|
|
# see https://github.com/revsys/django-test-plus/issues/202
|
|
tp.assertLoginRequired(url_name)
|
|
|
|
with tp.login(regular_user):
|
|
# assertGoodView expects a resolved URL
|
|
# see https://github.com/revsys/django-test-plus/issues/202
|
|
url = tp.reverse(url_name)
|
|
tp.assertGoodView(url, test_query_count=4, verbose=True)
|
|
|
|
form = tp.get_context("form")
|
|
assert isinstance(form, form_class)
|
|
# for field in form:
|
|
# tp.assertResponseContains(str(field), response)
|
|
|
|
|
|
@pytest.mark.skip(reason="Fails in CI due to missing file")
|
|
@pytest.mark.parametrize(
|
|
"url_name, model_class, data_fields",
|
|
[
|
|
("news-news-create", News, NewsForm.Meta.fields),
|
|
("news-blogpost-create", BlogPost, BlogPostForm.Meta.fields),
|
|
("news-link-create", Link, LinkForm.Meta.fields),
|
|
("news-poll-create", Poll, PollForm.Meta.fields),
|
|
("news-video-create", Video, VideoForm.Meta.fields),
|
|
],
|
|
)
|
|
@pytest.mark.parametrize("with_image", [False, True])
|
|
def test_news_create_post(
|
|
tp,
|
|
url_name,
|
|
model_class,
|
|
data_fields,
|
|
with_image,
|
|
regular_user,
|
|
moderator_user,
|
|
settings,
|
|
):
|
|
data = {
|
|
k: f"random-value-{k}" if "url" not in k else "http://example.com"
|
|
for k in data_fields
|
|
if k not in ("image", "publish_at")
|
|
}
|
|
|
|
img = None
|
|
if with_image:
|
|
img = BytesIO(
|
|
b"GIF89a\x01\x00\x01\x00\x00\x00\x00!\xf9\x04\x01\x00\x00\x00"
|
|
b"\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x00\x02\x01\x00\x00"
|
|
)
|
|
img.name = f"random-value-{uuid.uuid4()}.png"
|
|
data["image"] = img
|
|
|
|
data["publish_at"] = right_now = now()
|
|
|
|
before = now()
|
|
with tp.login(regular_user):
|
|
response = tp.post(url_name, data=data, follow=True)
|
|
after = now()
|
|
|
|
entries = model_class.objects.filter(title=data["title"])
|
|
assert len(entries) == 1
|
|
entry = entries.get()
|
|
assert entry.slug == slugify(data["title"])
|
|
for field, value in data.items():
|
|
if field != "image":
|
|
assert getattr(entry, field) == value
|
|
elif with_image:
|
|
assert entry.image
|
|
expected = os.path.join(
|
|
settings.MEDIA_ROOT,
|
|
date.today().strftime(Entry.image.field.upload_to),
|
|
img.name,
|
|
)
|
|
assert entry.image.path == expected
|
|
else:
|
|
assert not entry.image
|
|
assert entry.author == regular_user
|
|
assert not entry.is_approved
|
|
assert not entry.is_published
|
|
# Avoid mocking `now()`, yet still ensure that the timestamps are
|
|
# between `before` and `after`
|
|
assert before <= entry.created_at <= after
|
|
assert before <= entry.modified_at <= after
|
|
assert entry.publish_at == right_now
|
|
# email to moderators was sent
|
|
assert len(mail.outbox) == 1
|
|
actual = mail.outbox[0]
|
|
# render the same email using the notifications' method to assert equality
|
|
assert send_email_news_needs_moderation(response.wsgi_request, entry) == 1
|
|
expected = mail.outbox[1]
|
|
assert actual.subject == expected.subject
|
|
assert actual.body == expected.body
|
|
assert actual.from_email == expected.from_email
|
|
assert actual.recipients() == expected.recipients()
|
|
assert actual.recipients() == [moderator_user.email]
|
|
# success message is shown
|
|
messages = [(m.level_tag, m.message) for m in tp.get_context("messages")]
|
|
assert messages == [("success", "The news entry was successfully created.")]
|
|
|
|
tp.assertRedirects(response, entry.get_absolute_url())
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_approve_get_method_not_allowed(
|
|
tp, make_entry, regular_user, moderator_user, model_class
|
|
):
|
|
entry = make_entry(model_class, approved=False)
|
|
|
|
# login is required
|
|
url_params = ("news-approve", entry.slug)
|
|
tp.assertLoginRequired(*url_params)
|
|
|
|
# regular users would get a 403
|
|
with tp.login(regular_user):
|
|
response = tp.get(*url_params)
|
|
tp.response_403(response)
|
|
|
|
# moderators users would get a 405 for GET
|
|
with tp.login(moderator_user):
|
|
response = tp.get(*url_params)
|
|
tp.response_405(response)
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_approve_post(
|
|
tp, make_entry, regular_user, moderator_user, make_user, model_class
|
|
):
|
|
entry = make_entry(model_class, approved=False)
|
|
url_params = ("news-approve", entry.slug)
|
|
|
|
recipients = {
|
|
# user does not allow notifications
|
|
"u1@example.com": [],
|
|
# allows nofitications for all news type
|
|
"u2@example.com": [m.news_type for m in NEWS_MODELS],
|
|
# allows only for the same type as entry
|
|
"u3@example.com": [entry.tag],
|
|
# allows for any other type except entry's
|
|
"u4@example.com": [
|
|
m.news_type for m in NEWS_MODELS if m.news_type != entry.tag
|
|
],
|
|
}
|
|
|
|
for email, notifications in recipients.items():
|
|
make_user(email=email, allow_notification_others_news_posted=notifications)
|
|
|
|
# regular users would still get a 403 on POST
|
|
with tp.login(regular_user):
|
|
response = tp.post(*url_params)
|
|
tp.response_403(response)
|
|
|
|
# moderators users can POST to the view to approve an entry
|
|
with tp.login(moderator_user):
|
|
before = now()
|
|
response = tp.post(*url_params, follow=True)
|
|
after = now()
|
|
|
|
tp.assertRedirects(response, entry.get_absolute_url())
|
|
|
|
entry.refresh_from_db()
|
|
assert entry.is_approved is True
|
|
assert entry.moderator == moderator_user
|
|
assert before <= entry.approved_at <= after
|
|
assert before <= entry.modified_at <= after
|
|
# email was sent, one to author, one to each of the two other matching users
|
|
assert len(mail.outbox) == 3
|
|
# approval email to author
|
|
actual = mail.outbox[0]
|
|
# render the same email using the notifications' method to assert equality
|
|
assert send_email_news_approved(response.wsgi_request, entry) == 1
|
|
expected = mail.outbox[3]
|
|
assert actual.subject == expected.subject
|
|
assert actual.body == expected.body
|
|
assert actual.from_email == expected.from_email
|
|
assert actual.recipients() == expected.recipients()
|
|
# news posted email to other users
|
|
actual = mail.outbox[1]
|
|
# render the same email using the notifications' method to assert equality
|
|
# here, only two emails are sent - one to each matching user
|
|
assert send_email_news_posted(response.wsgi_request, entry) == 2
|
|
expected = mail.outbox[4]
|
|
assert actual.subject == expected.subject
|
|
assert actual.body == expected.body
|
|
assert actual.from_email == expected.from_email
|
|
assert actual.recipients() == expected.recipients()
|
|
# success message is shown
|
|
messages = [(m.level_tag, m.message) for m in tp.get_context("messages")]
|
|
assert messages == [("success", "The entry was successfully approved.")]
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_approve_already_approved(tp, make_entry, moderator_user, model_class):
|
|
entry = make_entry(model_class, approved=True)
|
|
url_params = ("news-approve", entry.slug)
|
|
|
|
with tp.login(moderator_user):
|
|
before = now()
|
|
response = tp.post(*url_params, follow=True)
|
|
now()
|
|
|
|
tp.assertRedirects(response, entry.get_absolute_url())
|
|
|
|
entry.refresh_from_db()
|
|
# approval information was not changed
|
|
assert entry.is_approved is True
|
|
assert entry.moderator is not moderator_user
|
|
assert entry.approved_at <= before
|
|
assert entry.modified_at <= before
|
|
# email was not sent
|
|
assert mail.outbox == []
|
|
# error message is shown
|
|
messages = [(m.level_tag, m.message) for m in tp.get_context("messages")]
|
|
assert messages == [("error", "The entry was already approved.")]
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_approve_post_redirects_to_next_if_available(
|
|
tp, make_entry, moderator_user, model_class
|
|
):
|
|
entry = make_entry(model_class, approved=False)
|
|
url_params = ("news-approve", entry.slug)
|
|
next_url = "/foo"
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.post(*url_params, data={"next": next_url}, follow=False)
|
|
|
|
tp.assertRedirects(response, next_url, fetch_redirect_response=False)
|
|
|
|
# a non relative/non safe next URL is not used for redirection
|
|
with tp.login(moderator_user):
|
|
response = tp.post(
|
|
*url_params, data={"next": "http://google.com"}, follow=False
|
|
)
|
|
|
|
tp.assertRedirects(
|
|
response, entry.get_absolute_url(), fetch_redirect_response=False
|
|
)
|
|
|
|
|
|
def test_news_moderation_list(tp, regular_user, moderator_user):
|
|
url_name = "news-moderate"
|
|
|
|
# login required
|
|
tp.assertLoginRequired(url_name)
|
|
|
|
# regular users would get a 403 for news moderation list
|
|
with tp.login(regular_user):
|
|
response = tp.get(url_name)
|
|
tp.response_403(response)
|
|
|
|
# moderators users would get a 200
|
|
with tp.login(moderator_user):
|
|
response = tp.get(url_name)
|
|
tp.response_200(response)
|
|
|
|
|
|
@pytest.mark.skip(
|
|
reason="Fails; prefer to skip test functions and not the whole test suite"
|
|
)
|
|
def test_news_moderation_filter_unapproved_news(tp, make_entry, moderator_user):
|
|
unapproved_published = [
|
|
make_entry(model_class, approved=False, published=True)
|
|
for _ in range(3)
|
|
for model_class in NEWS_MODELS
|
|
]
|
|
# approved and published
|
|
ignore = [
|
|
make_entry(model_class, approved=True, published=True)
|
|
for _ in range(3)
|
|
for model_class in NEWS_MODELS
|
|
]
|
|
unapproved_unpublished = [
|
|
make_entry(model_class, approved=False, published=False)
|
|
for _ in range(3)
|
|
for model_class in NEWS_MODELS
|
|
]
|
|
# approved and unpublished
|
|
ignore.extend(
|
|
make_entry(model_class, approved=True, published=False)
|
|
for _ in range(3)
|
|
for model_class in NEWS_MODELS
|
|
)
|
|
|
|
url = tp.reverse("news-moderate")
|
|
with tp.login(moderator_user):
|
|
# 5 queries
|
|
# SELECT "django_session"...
|
|
# SELECT "users_user"...
|
|
# SELECT "django_content_type"... (perms)
|
|
# SELECT "django_content_type"... (perms and groups, may need debugging)
|
|
# SELECT "news_entry"...
|
|
response = tp.assertGoodView(url, test_query_count=7, verbose=True)
|
|
|
|
content = str(response.content)
|
|
for e in unapproved_published + unapproved_unpublished:
|
|
assert e.title in content
|
|
assert e.author.email in content
|
|
assert (e.get_absolute_url() + f"?next={url}") in content
|
|
for e in ignore:
|
|
assert e.title not in content
|
|
assert e.author.email not in content
|
|
assert e.get_absolute_url() not in content
|
|
|
|
|
|
@pytest.mark.parametrize("method", ["GET", "POST"])
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_update_acl(
|
|
tp, make_entry, regular_user, moderator_user, method, model_class
|
|
):
|
|
entry = make_entry(model_class, approved=False)
|
|
url_params = ("news-update", entry.slug)
|
|
|
|
# regular users would get a 404 for a news they don't own
|
|
with tp.login(regular_user):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_403(response)
|
|
|
|
# owner would get a 200 response, and so will moderators
|
|
with tp.login(entry.author):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_200(response)
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_200(response)
|
|
|
|
# but if the entry is approved, only moderator can access the update form
|
|
entry.approve(baker.make("users.User"))
|
|
|
|
with tp.login(entry.author):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_403(response)
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_200(response)
|
|
|
|
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_update(tp, make_entry, model_class):
|
|
entry = make_entry(model_class, approved=False, title="A news title")
|
|
url_params = ("news-update", entry.slug)
|
|
|
|
with tp.login(entry.author):
|
|
response = tp.get(*url_params)
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert entry.title in content
|
|
assert entry.content in content
|
|
|
|
new_title = "This is a different title"
|
|
data = {"title": new_title, "publish_at": entry.publish_at}
|
|
with tp.login(entry.author):
|
|
response = tp.post(*url_params, data=data, follow=True)
|
|
tp.response_200(response)
|
|
|
|
tp.assertRedirects(response, entry.get_absolute_url())
|
|
content = str(response.content)
|
|
assert new_title in content
|
|
assert "A news title" not in content
|
|
|
|
entry.refresh_from_db()
|
|
assert entry.title == new_title
|
|
|
|
|
|
@pytest.mark.parametrize("method", ["GET", "POST"])
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_delete_acl(
|
|
tp, make_entry, regular_user, moderator_user, method, model_class
|
|
):
|
|
entry = make_entry(model_class, approved=False)
|
|
url_params = ("news-delete", entry.slug)
|
|
|
|
# regular users would get a 404 for a news they don't own
|
|
with tp.login(regular_user):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_403(response)
|
|
|
|
# owner would get a 200 response, and so will moderators
|
|
with tp.login(entry.author):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_403(response)
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.request(method.lower(), *url_params, follow=True)
|
|
tp.response_200(response)
|
|
|
|
# but if the entry is approved, only moderator can access the delete form
|
|
entry = make_entry(model_class, approved=True)
|
|
url_params = ("news-delete", entry.slug)
|
|
|
|
with tp.login(entry.author):
|
|
response = tp.request(method.lower(), *url_params)
|
|
tp.response_403(response)
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.request(method.lower(), *url_params, follow=True)
|
|
tp.response_200(response)
|
|
|
|
|
|
@pytest.mark.skip(
|
|
reason="Fails; prefer to skip test functions and not the whole test suite"
|
|
)
|
|
@pytest.mark.parametrize("model_class", NEWS_MODELS)
|
|
def test_news_delete(tp, make_entry, moderator_user, model_class):
|
|
entry = make_entry(model_class, approved=False)
|
|
url_params = ("news-delete", entry.slug)
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.get(*url_params)
|
|
tp.response_200(response)
|
|
|
|
content = str(response.content)
|
|
assert "Please confirm your choice" in content
|
|
assert entry.title in content
|
|
# No entry removed just yet!
|
|
assert Entry.objects.filter(pk=entry.pk).count() == 1
|
|
|
|
with tp.login(moderator_user):
|
|
response = tp.post(*url_params, follow=True)
|
|
tp.response_200(response)
|
|
tp.assertRedirects(response, tp.reverse("news"))
|
|
assert Entry.objects.filter(pk=entry.pk).count() == 0
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@pytest.mark.parametrize(
|
|
"already_approved, expected_message_substring",
|
|
[
|
|
(False, "approved"),
|
|
(True, "already been approved"),
|
|
],
|
|
)
|
|
def test_magic_link_valid_token(
|
|
tp, make_entry, moderator_user, already_approved, expected_message_substring
|
|
):
|
|
"""Valid magic link approves unapproved entries or warns if already approved."""
|
|
|
|
entry = make_entry(approved=already_approved)
|
|
serializer = URLSafeTimedSerializer(settings.SECRET_KEY)
|
|
token = serializer.dumps(
|
|
{"entry_slug": entry.slug, "moderator_id": moderator_user.id},
|
|
salt=NEWS_APPROVAL_SALT,
|
|
)
|
|
|
|
url = f"/news/moderate/magic/{token}/"
|
|
response = tp.get(url)
|
|
entry.refresh_from_db()
|
|
|
|
if already_approved:
|
|
assert entry.is_approved
|
|
assert entry.moderator != moderator_user
|
|
else:
|
|
assert entry.is_approved
|
|
assert entry.moderator == moderator_user
|
|
|
|
tp.assertRedirects(response, entry.get_absolute_url())
|
|
|
|
msgs = [(m.level_tag, m.message) for m in get_messages(response.wsgi_request)]
|
|
assert any(expected_message_substring in message for _, message in msgs)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
@pytest.mark.parametrize(
|
|
"authenticated, expected_redirect",
|
|
[
|
|
(
|
|
False,
|
|
lambda tp: f"{reverse('account_login')}?next={reverse('news-moderate')}",
|
|
),
|
|
(
|
|
True,
|
|
lambda tp: reverse("news-moderate"),
|
|
),
|
|
],
|
|
)
|
|
def test_magic_link_expired_token(
|
|
tp, make_entry, moderator_user, monkeypatch, authenticated, expected_redirect
|
|
):
|
|
"""Expired magic link redirects appropriately for authenticated vs. anonymous users."""
|
|
|
|
entry = make_entry(approved=False)
|
|
serializer = URLSafeTimedSerializer(settings.SECRET_KEY)
|
|
token = serializer.dumps(
|
|
{"entry_slug": entry.slug, "moderator_id": moderator_user.id},
|
|
salt=NEWS_APPROVAL_SALT,
|
|
)
|
|
|
|
monkeypatch.setattr(
|
|
"news.views.URLSafeTimedSerializer.loads",
|
|
lambda *a, **k: (_ for _ in ()).throw(SignatureExpired("expired")),
|
|
)
|
|
|
|
url = f"/news/moderate/magic/{token}/"
|
|
if authenticated:
|
|
with tp.login(moderator_user):
|
|
response = tp.get(url, follow=True)
|
|
else:
|
|
response = tp.get(url, follow=True)
|
|
|
|
tp.assertRedirects(response, expected_redirect(tp))
|
|
msgs = [(m.level_tag, m.message) for m in get_messages(response.wsgi_request)]
|
|
assert any("expired" in message.lower() for _, message in msgs)
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_magic_link_invalid_token_returns_403(tp):
|
|
"""Invalid magic link returns HTTP 403 Forbidden with an error message."""
|
|
|
|
invalid_token = "not-a-valid-token"
|
|
url = reverse("news-magic-approve", args=[invalid_token])
|
|
response = tp.get(url, follow=False)
|
|
|
|
tp.response_403(response)
|
|
assert "Invalid magic link" in response.content.decode()
|