From 2a6728ad8f3f1232170160e569de9022d0f6063d Mon Sep 17 00:00:00 2001
From: Konrad Mohrfeldt <konrad.mohrfeldt@farbdev.org>
Date: Fri, 7 Apr 2023 02:13:58 +0200
Subject: [PATCH] test: implement tests for note REST API endpoint

---
 program/tests/__init__.py   | 100 +++++++++++++++++++++++++++++
 program/tests/test_notes.py | 124 ++++++++++++++++++++++++++++++++++++
 2 files changed, 224 insertions(+)
 create mode 100644 program/tests/__init__.py
 create mode 100644 program/tests/test_notes.py

diff --git a/program/tests/__init__.py b/program/tests/__init__.py
new file mode 100644
index 00000000..eeef18a2
--- /dev/null
+++ b/program/tests/__init__.py
@@ -0,0 +1,100 @@
+import datetime
+
+from django.contrib.auth.models import User
+from django.utils.text import slugify
+from django.utils.timezone import now
+from program.models import Note, RRule, Schedule, Show, TimeSlot
+
+
+class SteeringTestCaseMixin:
+    base_url = "/api/v1"
+
+    def _url(self, *paths, **kwargs):
+        url = "/".join(str(p) for p in paths) + "/"
+        return f"{self.base_url}/{url.format(**kwargs)}"
+
+    def _get_client(self, user=None):
+        client = self.client_class()
+        if user:
+            client.force_authenticate(user=user)
+        return client
+
+
+class UserMixin:
+    user_admin: User
+    user_common: User
+
+    def setUp(self):
+        self.user_admin = User.objects.create_superuser(
+            "admin", "admin@aura.radio", password="admin"
+        )
+        self.user_common = User.objects.create_user(
+            "herbert", "herbert@aura.radio", password="herbert"
+        )
+
+
+class ShowMixin:
+    def _create_show(self, name: str, **kwargs):
+        kwargs["name"] = name
+        kwargs.setdefault("slug", slugify(name))
+        kwargs.setdefault("short_description", f"The {name} show")
+        owners = kwargs.pop("owners", [])
+        show = Show.objects.create(**kwargs)
+        if owners:
+            show.owners.set(owners)
+        return show
+
+
+class ScheduleMixin:
+    def _get_rrule(self):
+        rrule = RRule.objects.first()
+        if rrule is None:
+            rrule = RRule.objects.create(name="once", freq=0)
+        return rrule
+
+    def _create_schedule(self, show: Show, **kwargs):
+        _first_date = kwargs.get("first_date", now().date())
+        kwargs["show"] = show
+        kwargs.setdefault("first_date", _first_date)
+        kwargs.setdefault("start_time", "08:00")
+        kwargs.setdefault("last_date", _first_date + datetime.timedelta(days=365))
+        kwargs.setdefault("end_time", "09:00")
+        kwargs.setdefault("rrule", self._get_rrule())
+        return Schedule.objects.create(**kwargs)
+
+
+class TimeSlotMixin:
+    def _create_timeslot(self, schedule: Schedule, **kwargs):
+        _start = kwargs.get("start", now())
+        kwargs.setdefault("schedule", schedule)
+        kwargs.setdefault("show", schedule.show)
+        kwargs.setdefault("start", _start)
+        kwargs.setdefault("end", _start + datetime.timedelta(hours=1))
+        return TimeSlot.objects.create(**kwargs)
+
+
+class NoteMixin:
+    def _create_note(self, timeslot: TimeSlot, **kwargs):
+        note_count = Note.objects.all().count()
+        _title = kwargs.get("title", f"a random note #{note_count}")
+        kwargs["timeslot"] = timeslot
+        kwargs["title"] = _title
+        kwargs.setdefault("slug", slugify(_title))
+        return Note.objects.create(**kwargs)
+
+    def _create_random_note_content(self, **kwargs):
+        note_count = Note.objects.all().count()
+        _title = kwargs.get("title", f"a random note #{note_count}")
+        kwargs["title"] = _title
+        kwargs.setdefault("slug", slugify(_title))
+        kwargs.setdefault("content", "some random content")
+        kwargs.setdefault("contributors", [])
+        return kwargs
+
+
+class ProgramModelMixin(ShowMixin, ScheduleMixin, TimeSlotMixin, NoteMixin):
+    pass
+
+
+class BaseMixin(UserMixin, ProgramModelMixin, SteeringTestCaseMixin):
+    pass
diff --git a/program/tests/test_notes.py b/program/tests/test_notes.py
new file mode 100644
index 00000000..d21fdadc
--- /dev/null
+++ b/program/tests/test_notes.py
@@ -0,0 +1,124 @@
+from rest_framework.test import APITransactionTestCase
+
+from program import tests
+from program.models import Schedule, Show
+
+
+class NoteViewTestCase(tests.BaseMixin, APITransactionTestCase):
+    reset_sequences = True
+
+    show_beatbetrieb: Show
+    schedule_beatbetrieb: Schedule
+    show_musikrotation: Show
+    schedule_musikrotation: Schedule
+
+    def setUp(self) -> None:
+        super().setUp()
+        self.show_beatbetrieb = self._create_show("Beatbetrieb")
+        self.schedule_beatbetrieb = self._create_schedule(self.show_beatbetrieb)
+        self.show_musikrotation = self._create_show("Musikrotation", owners=[self.user_common])
+        self.schedule_musikrotation = self._create_schedule(
+            self.show_musikrotation, start_time="10:00", end_time="12:00"
+        )
+
+    def test_everyone_can_read_notes(self):
+        self._create_note(self._create_timeslot(schedule=self.schedule_beatbetrieb))
+        self._create_note(self._create_timeslot(schedule=self.schedule_musikrotation))
+        res = self._get_client().get(self._url("notes"))
+        self.assertEqual(len(res.data), 2)
+
+    def test_common_users_can_create_notes_for_owned_shows(self):
+        ts = self._create_timeslot(schedule=self.schedule_musikrotation)
+        client = self._get_client(self.user_common)
+        endpoint = self._url("notes")
+        res = client.post(
+            endpoint, self._create_random_note_content(timeslot=ts.id), format="json"
+        )
+        self.assertEqual(res.status_code, 201)
+
+    def test_common_users_cannot_create_notes_for_foreign_shows(self):
+        ts = self._create_timeslot(schedule=self.schedule_beatbetrieb)
+        client = self._get_client(self.user_common)
+        endpoint = self._url("notes")
+        res = client.post(
+            endpoint, self._create_random_note_content(timeslot=ts.id), format="json"
+        )
+        self.assertEqual(res.status_code, 404)
+
+    def test_common_user_can_update_owned_shows(self):
+        ts = self._create_timeslot(schedule=self.schedule_musikrotation)
+        note = self._create_note(ts)
+        client = self._get_client(self.user_common)
+        new_note_content = self._create_random_note_content(title="meh")
+        res = client.put(self._url("notes", note.id), new_note_content, format="json")
+        self.assertEqual(res.status_code, 200)
+
+    def test_common_user_cannot_update_notes_of_foreign_shows(self):
+        ts = self._create_timeslot(schedule=self.schedule_beatbetrieb)
+        note = self._create_note(ts)
+        client = self._get_client(self.user_common)
+        new_note_content = self._create_random_note_content(title="meh")
+        res = client.put(self._url("notes", note.id), new_note_content, format="json")
+        self.assertEqual(res.status_code, 404)
+
+    def test_admin_can_create_notes_for_all_timeslots(self):
+        timeslot = self._create_timeslot(schedule=self.schedule_musikrotation)
+        client = self._get_client(self.user_admin)
+        res = client.post(
+            self._url("notes"),
+            self._create_random_note_content(timeslot=timeslot.id),
+            format="json",
+        )
+        self.assertEqual(res.status_code, 201)
+
+    def test_notes_can_be_created_through_nested_routes(self):
+        client = self._get_client(self.user_admin)
+
+        # /shows/{pk}/notes/
+        ts1 = self._create_timeslot(schedule=self.schedule_musikrotation)
+        url = self._url("shows", self.show_musikrotation.id, "notes")
+        note = self._create_random_note_content(title="meh", timeslot=ts1.id)
+        res = client.post(url, note, format="json")
+        self.assertEqual(res.status_code, 201)
+
+        # /shows/{pk}/timeslots/{pk}/note/
+        ts2 = self._create_timeslot(schedule=self.schedule_musikrotation)
+        url = self._url("shows", self.show_musikrotation, "timeslots", ts2.id, "note")
+        note = self._create_random_note_content(title="cool")
+        res = client.post(url, note, format="json")
+        self.assertEqual(res.status_code, 201)
+
+    def test_notes_can_be_filtered_through_nested_routes_and_query_params(self):
+        client = self._get_client()
+
+        ts1 = self._create_timeslot(schedule=self.schedule_musikrotation)
+        ts2 = self._create_timeslot(schedule=self.schedule_beatbetrieb)
+        ts3 = self._create_timeslot(schedule=self.schedule_beatbetrieb)
+        n1 = self._create_note(timeslot=ts1)
+        n2 = self._create_note(timeslot=ts2)
+        n3 = self._create_note(timeslot=ts3)
+
+        def _get_ids(res):
+            return set(ts["id"] for ts in res.data)
+
+        # /shows/{pk}/notes/
+        query_res = client.get(self._url("notes") + f"?show={self.show_beatbetrieb.id}")
+        route_res = client.get(self._url("shows", self.show_beatbetrieb.id, "notes"))
+        ids = {n2.id, n3.id}
+        self.assertEqual(_get_ids(query_res), ids)
+        self.assertEqual(_get_ids(route_res), ids)
+
+        query_res = client.get(self._url("notes") + f"?show={self.show_musikrotation.id}")
+        route_res = client.get(self._url("shows", self.show_musikrotation.id, "notes"))
+        ids = {n1.id}
+        self.assertEqual(_get_ids(query_res), ids)
+        self.assertEqual(_get_ids(route_res), ids)
+
+        # /shows/{pk}/timeslots/{pk}/note/
+        query_res = client.get(self._url("notes") + f"?timeslot={ts2.id}")
+        route_res = client.get(
+            self._url("shows", self.show_beatbetrieb.id, "timeslots", ts2.id, "note")
+        )
+        ids = {n2.id}
+        self.assertEqual(_get_ids(query_res), ids)
+        self.assertEqual(_get_ids(route_res), ids)
-- 
GitLab