import datetime import io from django.core.files.uploadedfile import SimpleUploadedFile from django.test import TestCase, override_settings from django.utils import timezone from PIL import Image from club.models import Club, ClubMembership, Season, Sponsor from events.models import Event, Location, Opponent from members.models import Member from news.models import News, NewsPhoto from teams.models import Position, StaffAssignment, Team, TeamMembership, TeamPhoto @override_settings( ROSTERCHIEF_BASE_DOMAIN="rosterchief.app", ALLOWED_HOSTS=["rosterchief.app", "ajax-united.rosterchief.app", "rival-fc.rosterchief.app", "testserver"], ) class ApiTestBase(TestCase): # The tenant, its current season and its one team back every API test and are read # only. The handful of tests that do change them (deleting the season, giving the # club a logo) get their own copy from setUpTestData and are rolled back with the # per-test transaction. @classmethod def setUpTestData(cls): cls.club = Club.objects.create(name="Ajax United", slug="ajax-united") today = timezone.localdate() cls.season = Season.objects.create(club=cls.club, start_date=today - datetime.timedelta(days=30), end_date=today + datetime.timedelta(days=300)) cls.team = Team.objects.create(club=cls.club, name="First Team", short_name="1st") def api_get(self, path, **params): return self.client.get(f"/api/v1{path}", params, HTTP_HOST="ajax-united.rosterchief.app") def api_get_base_domain(self, path, **params): return self.client.get(f"/api/v1{path}", params, HTTP_HOST="rosterchief.app") class NewsApiTests(ApiTestBase): def make_news(self, **overrides): defaults = {"club": self.club, "title": "News", "body": "body", "status": News.Status.PUBLISHED, "published_at": timezone.now() - datetime.timedelta(hours=1), "visibility": News.Visibility.EXTERNAL} defaults.update(overrides) return News.objects.create(**defaults) def test_a_draft_is_excluded(self): self.make_news(status=News.Status.DRAFT, published_at=None) self.assertEqual(self.api_get("/news/").json()["count"], 0) def test_a_scheduled_but_not_yet_released_item_is_excluded(self): self.make_news(published_at=timezone.now() + datetime.timedelta(days=1)) self.assertEqual(self.api_get("/news/").json()["count"], 0) def test_an_internal_only_item_is_excluded(self): self.make_news(visibility=News.Visibility.INTERNAL) self.assertEqual(self.api_get("/news/").json()["count"], 0) def test_an_external_item_is_included(self): item = self.make_news(visibility=News.Visibility.EXTERNAL) data = self.api_get("/news/").json() self.assertEqual(data["count"], 1) self.assertEqual(data["results"][0]["id"], str(item.pk)) def test_a_both_visibility_item_is_included(self): self.make_news(visibility=News.Visibility.BOTH) self.assertEqual(self.api_get("/news/").json()["count"], 1) def test_newest_first(self): older = self.make_news(title="Older", published_at=timezone.now() - datetime.timedelta(days=2)) newer = self.make_news(title="Newer", published_at=timezone.now() - datetime.timedelta(hours=1)) results = self.api_get("/news/").json()["results"] self.assertEqual([r["id"] for r in results], [str(newer.pk), str(older.pk)]) def test_photos_get_absolute_urls(self): item = self.make_news() NewsPhoto.objects.create(news_item=item, image="clubs/ajax-united/news/x/pic.jpg", is_main=True) photo = self.api_get("/news/").json()["results"][0]["photos"][0] self.assertTrue(photo["url"].startswith("http://ajax-united.rosterchief.app/media/")) self.assertTrue(photo["is_main"]) def test_pagination_limit_and_offset(self): for i in range(3): self.make_news(title=f"Item {i}", published_at=timezone.now() - datetime.timedelta(hours=1, minutes=i)) data = self.api_get("/news/", limit=1, offset=1).json() self.assertEqual(data["count"], 3) self.assertEqual(len(data["results"]), 1) def test_limit_is_capped(self): self.assertEqual(self.api_get("/news/", limit=1000).json()["limit"], 100) def test_no_news_is_an_empty_list_not_an_error(self): response = self.api_get("/news/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["results"], []) def test_excerpt_is_a_truncated_prefix_of_the_body(self): self.make_news(body=" ".join(f"word{i}" for i in range(80))) excerpt = self.api_get("/news/").json()["results"][0]["excerpt_nl"] self.assertTrue(excerpt.startswith("word0 word1")) self.assertTrue(excerpt.endswith("…")) self.assertLess(len(excerpt.split()), 80) def test_excerpt_is_unchanged_when_the_body_is_already_short(self): item = self.make_news(body="Short body.") excerpt = self.api_get("/news/").json()["results"][0]["excerpt_nl"] self.assertEqual(excerpt, item.body) def test_slug_is_auto_populated_from_the_title(self): item = self.make_news(title="Big Win This Weekend") self.assertEqual(item.slug, "big-win-this-weekend") def test_get_single_news_item_by_slug(self): item = self.make_news(title="Big Win This Weekend") response = self.api_get(f"/news/{item.slug}/") self.assertEqual(response.status_code, 200) self.assertEqual(response.json()["id"], str(item.pk)) def test_get_single_news_item_404s_for_an_unknown_slug(self): response = self.api_get("/news/no-such-item/") self.assertEqual(response.status_code, 404) def test_get_single_news_item_respects_visibility(self): item = self.make_news(visibility=News.Visibility.INTERNAL) response = self.api_get(f"/news/{item.slug}/") self.assertEqual(response.status_code, 404) def test_body_markdown_is_rendered_to_html(self): self.make_news(body="## Big win\n\nWe beat **Rivals FC** 4-2. [Full report](https://example.com).") body = self.api_get("/news/").json()["results"][0]["body_nl"] self.assertIn("