Sith/com/tests/test_api.py

251 lines
8.8 KiB
Python
Raw Normal View History

2025-01-05 01:04:11 +01:00
from dataclasses import dataclass
2025-01-05 01:32:54 +01:00
from datetime import datetime, timedelta
2025-01-05 01:04:11 +01:00
from pathlib import Path
2025-01-05 01:32:54 +01:00
from typing import Callable
2025-01-05 01:04:11 +01:00
from unittest.mock import MagicMock, patch
2025-02-17 13:29:12 +01:00
from urllib.parse import quote
2025-01-05 01:04:11 +01:00
import pytest
from django.conf import settings
2025-01-20 17:03:25 +01:00
from django.contrib.auth.models import Permission
2025-01-05 01:32:54 +01:00
from django.http import HttpResponse
2025-02-17 13:29:12 +01:00
from django.test import Client, TestCase
2025-01-05 01:04:11 +01:00
from django.urls import reverse
2025-01-05 01:32:54 +01:00
from django.utils import timezone
2025-02-17 13:29:12 +01:00
from django.utils.timezone import now
from model_bakery import baker, seq
from pytest_django.asserts import assertNumQueries
2025-01-05 01:04:11 +01:00
from com.calendar import IcsCalendar
2025-02-17 13:29:12 +01:00
from com.models import News, NewsDate
from core.markdown import markdown
2025-01-20 17:03:25 +01:00
from core.models import User
2025-01-05 01:04:11 +01:00
@dataclass
class MockResponse:
ok: bool
2025-01-05 01:04:11 +01:00
value: str
@property
def content(self):
2025-01-05 01:04:11 +01:00
return self.value.encode("utf8")
2025-01-05 01:32:54 +01:00
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
)
2025-01-05 01:04:11 +01:00
@pytest.mark.django_db
class TestExternalCalendar:
@pytest.fixture
def mock_request(self):
2025-01-05 01:32:54 +01:00
mock = MagicMock()
with patch("requests.get", mock):
2025-01-05 01:32:54 +01:00
yield mock
@pytest.fixture
def mock_current_time(self):
mock = MagicMock()
original = timezone.now
with patch("django.utils.timezone.now", mock):
yield mock, original
2025-01-05 01:04:11 +01:00
@pytest.fixture(autouse=True)
def clear_cache(self):
IcsCalendar._EXTERNAL_CALENDAR.unlink(missing_ok=True)
def test_fetch_error(self, client: Client, mock_request: MagicMock):
mock_request.return_value = MockResponse(ok=False, value="not allowed")
2025-01-05 01:04:11 +01:00
assert client.get(reverse("api:calendar_external")).status_code == 404
def test_fetch_success(self, client: Client, mock_request: MagicMock):
external_response = MockResponse(ok=True, value="Definitely an ICS")
2025-01-05 01:04:11 +01:00
mock_request.return_value = external_response
response = client.get(reverse("api:calendar_external"))
assert response.status_code == 200
2025-01-05 01:32:54 +01:00
out_file = accel_redirect_to_file(response)
assert out_file is not None
2025-01-05 01:04:11 +01:00
assert out_file.exists()
with open(out_file, "r") as f:
assert f.read() == external_response.value
2025-01-05 01:32:54 +01:00
def test_fetch_caching(
self,
client: Client,
mock_request: MagicMock,
mock_current_time: tuple[MagicMock, Callable[[], datetime]],
):
fake_current_time, original_timezone = mock_current_time
start_time = original_timezone()
fake_current_time.return_value = start_time
external_response = MockResponse(200, "Definitely an ICS")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.return_value = MockResponse(200, "This should be ignored")
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
mock_request.assert_called_once()
fake_current_time.return_value = start_time + timedelta(hours=1, seconds=1)
external_response = MockResponse(200, "This won't be ignored")
mock_request.return_value = external_response
with open(
accel_redirect_to_file(client.get(reverse("api:calendar_external"))), "r"
) as f:
assert f.read() == external_response.value
assert mock_request.call_count == 2
2025-01-05 01:36:41 +01:00
@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()
2025-01-20 17:03:25 +01:00
@pytest.mark.django_db
class TestModerateNews:
2025-02-25 18:08:16 +01:00
@pytest.mark.parametrize("news_is_published", [True, False])
def test_moderation_ok(self, client: Client, news_is_published: bool): # noqa FBT
2025-01-20 17:03:25 +01:00
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.
2025-02-25 18:08:16 +01:00
news = baker.make(News, is_published=news_is_published)
2025-01-23 14:32:10 +01:00
initial_moderator = news.moderator
2025-01-20 17:03:25 +01:00
client.force_login(user)
response = client.patch(
reverse("api:moderate_news", kwargs={"news_id": news.id})
)
2025-01-23 14:32:10 +01:00
# 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
2025-01-20 17:03:25 +01:00
assert response.status_code == 200
news.refresh_from_db()
2025-02-25 18:08:16 +01:00
assert news.is_published
if not news_is_published:
2025-01-23 14:32:10 +01:00
assert news.moderator == user
else:
assert news.moderator == initial_moderator
2025-01-20 17:03:25 +01:00
def test_moderation_forbidden(self, client: Client):
user = baker.make(User)
2025-02-25 18:08:16 +01:00
news = baker.make(News, is_published=False)
2025-01-20 17:03:25 +01:00
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()
2025-02-25 18:08:16 +01:00
assert not news.is_published
2025-01-20 17:03:25 +01:00
@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()
2025-02-17 13:29:12 +01:00
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(
2025-02-25 18:08:16 +01:00
baker.make(News, is_published=True, _quantity=5, _bulk_create=True)
2025-02-17 13:29:12 +01:00
),
)
cls.dates.append(
baker.make(
NewsDate,
start_date=now() + timedelta(days=2, hours=1),
end_date=now() + timedelta(days=2, hours=5),
2025-02-25 18:08:16 +01:00
news=baker.make(News, is_published=True),
2025-02-17 13:29:12 +01:00
)
)
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 = quote((now() + timedelta(days=1)).isoformat())
response = self.client.get(
reverse("api:fetch_news_dates") + f"?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]]