from dataclasses import dataclass from datetime import timedelta from pathlib import Path import pytest from django.conf import settings from django.contrib.auth.models import Permission from django.http import HttpResponse from django.test import Client, TestCase from django.urls import reverse from django.utils.timezone import now from model_bakery import baker, seq from pytest_django.asserts import assertNumQueries from com.ics_calendar import IcsCalendar from com.models import News, NewsDate from core.markdown import markdown from core.models import User @dataclass class MockResponse: ok: bool value: str @property def content(self): return self.value.encode("utf8") def accel_redirect_to_file(response: HttpResponse) -> Path | None: redirect = Path(response.headers.get("X-Accel-Redirect", "")) if not redirect.is_relative_to(Path("/") / settings.MEDIA_ROOT.stem): return None return settings.MEDIA_ROOT / redirect.relative_to( Path("/") / settings.MEDIA_ROOT.stem ) @pytest.mark.django_db class TestInternalCalendar: @pytest.fixture(autouse=True) def clear_cache(self): IcsCalendar._INTERNAL_CALENDAR.unlink(missing_ok=True) def test_fetch_success(self, client: Client): response = client.get(reverse("api:calendar_internal")) assert response.status_code == 200 out_file = accel_redirect_to_file(response) assert out_file is not None assert out_file.exists() @pytest.mark.django_db class TestModerateNews: @pytest.mark.parametrize("news_is_published", [True, False]) def test_moderation_ok(self, client: Client, news_is_published: bool): # noqa FBT user = baker.make( User, user_permissions=[Permission.objects.get(codename="moderate_news")] ) # The API call should work even if the news is initially moderated. # In the latter case, the result should be a noop, rather than an error. news = baker.make(News, is_published=news_is_published) initial_moderator = news.moderator client.force_login(user) response = client.patch( reverse("api:moderate_news", kwargs={"news_id": news.id}) ) # if it wasn't moderated, it should now be moderated and the moderator should # be the user that made the request. # If it was already moderated, it should be a no-op, but not an error assert response.status_code == 200 news.refresh_from_db() assert news.is_published if not news_is_published: assert news.moderator == user else: assert news.moderator == initial_moderator def test_moderation_forbidden(self, client: Client): user = baker.make(User) news = baker.make(News, is_published=False) client.force_login(user) response = client.patch( reverse("api:moderate_news", kwargs={"news_id": news.id}) ) assert response.status_code == 403 news.refresh_from_db() assert not news.is_published @pytest.mark.django_db class TestDeleteNews: def test_delete_news_ok(self, client: Client): user = baker.make( User, user_permissions=[Permission.objects.get(codename="delete_news")] ) news = baker.make(News) client.force_login(user) response = client.delete( reverse("api:delete_news", kwargs={"news_id": news.id}) ) assert response.status_code == 200 assert not News.objects.filter(id=news.id).exists() def test_delete_news_forbidden(self, client: Client): user = baker.make(User) news = baker.make(News) client.force_login(user) response = client.delete( reverse("api:delete_news", kwargs={"news_id": news.id}) ) assert response.status_code == 403 assert News.objects.filter(id=news.id).exists() class TestFetchNewsDates(TestCase): @classmethod def setUpTestData(cls): News.objects.all().delete() cls.dates = baker.make( NewsDate, _quantity=5, _bulk_create=True, start_date=seq(value=now(), increment_by=timedelta(days=1)), end_date=seq( value=now() + timedelta(hours=2), increment_by=timedelta(days=1) ), news=iter( baker.make(News, is_published=True, _quantity=5, _bulk_create=True) ), ) cls.dates.append( baker.make( NewsDate, start_date=now() + timedelta(days=2, hours=1), end_date=now() + timedelta(days=2, hours=5), news=baker.make(News, is_published=True), ) ) cls.dates.sort(key=lambda d: d.start_date) def test_num_queries(self): with assertNumQueries(2): self.client.get(reverse("api:fetch_news_dates")) def test_html_format(self): """Test that when the summary is asked in html, the summary is in html.""" summary_1 = "# First event\nThere is something happening.\n" self.dates[0].news.summary = summary_1 self.dates[0].news.save() summary_2 = ( "# Second event\n" "There is something happening **for real**.\n" "Everything is [here](https://youtu.be/dQw4w9WgXcQ)\n" ) self.dates[1].news.summary = summary_2 self.dates[1].news.save() response = self.client.get( reverse("api:fetch_news_dates") + "?page_size=2&text_format=html" ) assert response.status_code == 200 dates = response.json()["results"] assert dates[0]["news"]["summary"] == markdown(summary_1) assert dates[1]["news"]["summary"] == markdown(summary_2) def test_fetch(self): after = (now() + timedelta(days=1)).isoformat() response = self.client.get( reverse("api:fetch_news_dates", query={"page_size": 3, "after": after}) ) assert response.status_code == 200 dates = response.json()["results"] assert [d["id"] for d in dates] == [d.id for d in self.dates[1:4]]