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("

Big win

", body) self.assertIn("Rivals FC", body) self.assertIn('href="https://example.com"', body) self.assertIn(">Full report", body) def test_body_markdown_a_single_newline_becomes_a_line_break(self): self.make_news(body="Line one\nLine two") body = self.api_get("/news/").json()["results"][0]["body_nl"] self.assertIn("Line one' logo = SimpleUploadedFile("logo.svg", svg, content_type="image/svg+xml") self.make_sponsor(logo=logo) sponsor = self.api_get("/sponsors/").json()[0] self.assertEqual(sponsor["logo_width"], 120) self.assertEqual(sponsor["logo_height"], 80) def test_logo_dimensions_fall_back_to_an_svg_viewbox(self): svg = b'' logo = SimpleUploadedFile("logo.svg", svg, content_type="image/svg+xml") self.make_sponsor(logo=logo) sponsor = self.api_get("/sponsors/").json()[0] self.assertEqual(sponsor["logo_width"], 64) self.assertEqual(sponsor["logo_height"], 32) def test_logo_dimensions_are_null_without_a_logo(self): self.make_sponsor() sponsor = self.api_get("/sponsors/").json()[0] self.assertIsNone(sponsor["logo_width"]) self.assertIsNone(sponsor["logo_height"]) def test_randomize_returns_the_same_set_of_sponsors(self): for i in range(5): self.make_sponsor(name=f"Sponsor {i}") stable = {s["id"] for s in self.api_get("/sponsors/").json()} randomized = {s["id"] for s in self.api_get("/sponsors/", randomize="true").json()} self.assertEqual(stable, randomized) self.assertEqual(len(stable), 5) def test_default_order_is_stable_and_alphabetical(self): self.make_sponsor(name="Zulu Corp") self.make_sponsor(name="Acme Corp") names = [s["name"] for s in self.api_get("/sponsors/").json()] self.assertEqual(names, ["Acme Corp", "Zulu Corp"])