From 889a88034ed5c33f1a2e2f2a4fdc1a6da6dc13c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Fri, 2 Dec 2022 21:52:53 +0100 Subject: [PATCH 01/11] Add view tests Introduce framework to simply test correct rendering of views Create tests for dashboard views Create tests for plan views Create tests for default admin views (add and change) of AKModel models --- AKDashboard/fixtures/dashboard.json | 13 + AKDashboard/tests.py | 12 + AKModel/fixtures/model.json | 670 ++++++++++++++++++++++++++++ AKModel/tests.py | 96 +++- AKPlan/tests.py | 29 +- 5 files changed, 818 insertions(+), 2 deletions(-) create mode 100644 AKDashboard/fixtures/dashboard.json create mode 100644 AKModel/fixtures/model.json diff --git a/AKDashboard/fixtures/dashboard.json b/AKDashboard/fixtures/dashboard.json new file mode 100644 index 00000000..dde1a64d --- /dev/null +++ b/AKDashboard/fixtures/dashboard.json @@ -0,0 +1,13 @@ +[ +{ + "model": "AKDashboard.dashboardbutton", + "pk": 1, + "fields": { + "text": "Wiki", + "url": "http://wiki.kif.rocks", + "icon": "fab,wikipedia-w", + "color": 2, + "event": 2 + } +} +] diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py index f4fbff87..dc77d528 100644 --- a/AKDashboard/tests.py +++ b/AKDashboard/tests.py @@ -6,6 +6,7 @@ from django.utils.timezone import now from AKDashboard.models import DashboardButton from AKModel.models import Event, AK, AKCategory +from AKModel.tests import BasicViewTests class DashboardTests(TestCase): @@ -126,3 +127,14 @@ class DashboardTests(TestCase): response = self.client.get(url_event_dashboard) self.assertContains(response, "Dashboard Button Test") + + +class DashboardViewTests(BasicViewTests, TestCase): + fixtures = ['model.json', 'dashboard.json'] + + APP_NAME = 'dashboard' + + VIEWS = [ + ('dashboard', {}), + ('dashboard_event', {'slug': 'kif42'}), + ] diff --git a/AKModel/fixtures/model.json b/AKModel/fixtures/model.json new file mode 100644 index 00000000..b75d5093 --- /dev/null +++ b/AKModel/fixtures/model.json @@ -0,0 +1,670 @@ +[ +{ + "model": "AKModel.event", + "pk": 1, + "fields": { + "name": "KIF 23", + "slug": "kif23", + "place": "Neuland", + "timezone": "Europe/Berlin", + "start": "2020-10-01T17:41:22Z", + "end": "2020-10-04T17:41:30Z", + "reso_deadline": "2020-10-03T10:00:00Z", + "interest_start": null, + "interest_end": null, + "public": true, + "active": false, + "plan_hidden": true, + "plan_published_at": null, + "base_url": "", + "wiki_export_template_name": "", + "default_slot": "2.00", + "contact_email": "dummy@eyample.org" + } +}, +{ + "model": "AKModel.event", + "pk": 2, + "fields": { + "name": "KIF 42", + "slug": "kif42", + "place": "Neuland noch neuer", + "timezone": "Europe/Berlin", + "start": "2020-11-06T17:51:26Z", + "end": "2020-11-10T17:51:27Z", + "reso_deadline": "2020-11-08T17:51:42Z", + "interest_start": null, + "interest_end": null, + "public": true, + "active": true, + "plan_hidden": false, + "plan_published_at": "2022-12-01T21:50:01Z", + "base_url": "", + "wiki_export_template_name": "", + "default_slot": "2.00", + "contact_email": "dummy@example.org" + } +}, +{ + "model": "AKModel.akowner", + "pk": 1, + "fields": { + "name": "a", + "slug": "a", + "institution": "Uni X", + "link": "", + "event": 2 + } +}, +{ + "model": "AKModel.akowner", + "pk": 2, + "fields": { + "name": "b", + "slug": "b", + "institution": "Hochschule Y", + "link": "", + "event": 2 + } +}, +{ + "model": "AKModel.akowner", + "pk": 3, + "fields": { + "name": "c", + "slug": "c", + "institution": "", + "link": "", + "event": 1 + } +}, +{ + "model": "AKModel.akowner", + "pk": 4, + "fields": { + "name": "d", + "slug": "d", + "institution": "", + "link": "", + "event": 1 + } +}, +{ + "model": "AKModel.akcategory", + "pk": 1, + "fields": { + "name": "Spaâ–€", + "color": "275246", + "description": "", + "present_by_default": true, + "event": 1 + } +}, +{ + "model": "AKModel.akcategory", + "pk": 2, + "fields": { + "name": "Ernst", + "color": "234567", + "description": "", + "present_by_default": true, + "event": 1 + } +}, +{ + "model": "AKModel.akcategory", + "pk": 3, + "fields": { + "name": "Spaâ–€/Kultur", + "color": "333333", + "description": "", + "present_by_default": true, + "event": 2 + } +}, +{ + "model": "AKModel.akcategory", + "pk": 4, + "fields": { + "name": "Inhalt", + "color": "456789", + "description": "", + "present_by_default": true, + "event": 2 + } +}, +{ + "model": "AKModel.akcategory", + "pk": 5, + "fields": { + "name": "Meta", + "color": "111111", + "description": "", + "present_by_default": true, + "event": 2 + } +}, +{ + "model": "AKModel.aktrack", + "pk": 1, + "fields": { + "name": "Track 1", + "color": "", + "event": 2 + } +}, +{ + "model": "AKModel.aktag", + "pk": 1, + "fields": { + "name": "metametameta" + } +}, +{ + "model": "AKModel.akrequirement", + "pk": 1, + "fields": { + "name": "Beamer", + "event": 1 + } +}, +{ + "model": "AKModel.akrequirement", + "pk": 2, + "fields": { + "name": "Internet", + "event": 1 + } +}, +{ + "model": "AKModel.akrequirement", + "pk": 3, + "fields": { + "name": "BBB", + "event": 2 + } +}, +{ + "model": "AKModel.akrequirement", + "pk": 4, + "fields": { + "name": "Mumble", + "event": 2 + } +}, +{ + "model": "AKModel.akrequirement", + "pk": 5, + "fields": { + "name": "Matrix", + "event": 2 + } +}, +{ + "model": "AKModel.historicalak", + "pk": 1, + "fields": { + "id": 1, + "name": "Test AK Inhalt", + "short_name": "test1", + "description": "", + "link": "", + "protocol_link": "", + "reso": false, + "present": true, + "notes": "", + "interest": -1, + "category": 4, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:28:59.265Z", + "history_change_reason": null, + "history_type": "+", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 2, + "fields": { + "id": 1, + "name": "Test AK Inhalt", + "short_name": "test1", + "description": "", + "link": "", + "protocol_link": "", + "reso": false, + "present": true, + "notes": "", + "interest": -1, + "category": 4, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:28:59.305Z", + "history_change_reason": null, + "history_type": "~", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 3, + "fields": { + "id": 2, + "name": "Test AK Meta", + "short_name": "test2", + "description": "", + "link": "", + "protocol_link": "", + "reso": false, + "present": null, + "notes": "", + "interest": -1, + "category": 5, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:29:40.296Z", + "history_change_reason": null, + "history_type": "+", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 4, + "fields": { + "id": 2, + "name": "Test AK Meta", + "short_name": "test2", + "description": "", + "link": "", + "protocol_link": "", + "reso": false, + "present": null, + "notes": "", + "interest": -1, + "category": 5, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:29:40.355Z", + "history_change_reason": null, + "history_type": "~", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 5, + "fields": { + "id": 3, + "name": "AK Wish", + "short_name": "wish1", + "description": "Description of my Wish", + "link": "", + "protocol_link": "", + "reso": false, + "present": null, + "notes": "We need to find a volunteer first...", + "interest": -1, + "category": 3, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:30:59.469Z", + "history_change_reason": null, + "history_type": "+", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 6, + "fields": { + "id": 3, + "name": "AK Wish", + "short_name": "wish1", + "description": "Description of my Wish", + "link": "", + "protocol_link": "", + "reso": false, + "present": null, + "notes": "We need to find a volunteer first...", + "interest": -1, + "category": 3, + "track": null, + "event": 2, + "history_date": "2021-05-04T10:30:59.509Z", + "history_change_reason": null, + "history_type": "~", + "history_user": null + } +}, +{ + "model": "AKModel.historicalak", + "pk": 7, + "fields": { + "id": 2, + "name": "Test AK Meta", + "short_name": "test2", + "description": "", + "link": "", + "protocol_link": "", + "reso": false, + "present": null, + "notes": "", + "interest": -1, + "category": 5, + "track": 1, + "event": 2, + "history_date": "2022-12-02T12:27:14.277Z", + "history_change_reason": null, + "history_type": "~", + "history_user": null + } +}, +{ + "model": "AKModel.ak", + "pk": 1, + "fields": { + "name": "Test AK Inhalt", + "short_name": "test1", + "description": "", + "link": "", + "protocol_link": "", + "category": 4, + "track": null, + "reso": false, + "present": true, + "notes": "", + "interest": -1, + "interest_counter": 0, + "event": 2, + "owners": [ + 1 + ], + "tags": [], + "requirements": [ + 3 + ], + "conflicts": [], + "prerequisites": [] + } +}, +{ + "model": "AKModel.ak", + "pk": 2, + "fields": { + "name": "Test AK Meta", + "short_name": "test2", + "description": "", + "link": "", + "protocol_link": "", + "category": 5, + "track": 1, + "reso": false, + "present": null, + "notes": "", + "interest": -1, + "interest_counter": 0, + "event": 2, + "owners": [ + 2 + ], + "tags": [ + 1 + ], + "requirements": [], + "conflicts": [], + "prerequisites": [] + } +}, +{ + "model": "AKModel.ak", + "pk": 3, + "fields": { + "name": "AK Wish", + "short_name": "wish1", + "description": "Description of my Wish", + "link": "", + "protocol_link": "", + "category": 3, + "track": null, + "reso": false, + "present": null, + "notes": "We need to find a volunteer first...", + "interest": -1, + "interest_counter": 0, + "event": 2, + "owners": [], + "tags": [], + "requirements": [ + 4 + ], + "conflicts": [ + 1 + ], + "prerequisites": [ + 2 + ] + } +}, +{ + "model": "AKModel.room", + "pk": 1, + "fields": { + "name": "Testraum", + "location": "BBB", + "capacity": -1, + "event": 2, + "properties": [ + 3 + ] + } +}, +{ + "model": "AKModel.room", + "pk": 2, + "fields": { + "name": "Testraum 2", + "location": "", + "capacity": 30, + "event": 2, + "properties": [] + } +}, +{ + "model": "AKModel.akslot", + "pk": 1, + "fields": { + "ak": 1, + "room": 2, + "start": "2020-11-06T18:30:00Z", + "duration": "2.00", + "fixed": false, + "event": 2, + "updated": "2022-12-02T12:23:11.856Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 2, + "fields": { + "ak": 2, + "room": 1, + "start": "2020-11-08T12:30:00Z", + "duration": "2.00", + "fixed": false, + "event": 2, + "updated": "2022-12-02T12:23:18.279Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 3, + "fields": { + "ak": 3, + "room": null, + "start": null, + "duration": "1.50", + "fixed": false, + "event": 2, + "updated": "2021-05-04T10:30:59.523Z" + } +}, +{ + "model": "AKModel.akslot", + "pk": 4, + "fields": { + "ak": 3, + "room": null, + "start": null, + "duration": "3.00", + "fixed": false, + "event": 2, + "updated": "2021-05-04T10:30:59.528Z" + } +}, +{ + "model": "AKModel.constraintviolation", + "pk": 1, + "fields": { + "type": "soa", + "level": 10, + "event": 2, + "ak_owner": null, + "room": null, + "requirement": null, + "category": null, + "comment": "", + "timestamp": "2022-12-02T12:23:11.875Z", + "manually_resolved": false, + "aks": [ + 1 + ], + "ak_slots": [ + 1 + ] + } +}, +{ + "model": "AKModel.constraintviolation", + "pk": 2, + "fields": { + "type": "rng", + "level": 10, + "event": 2, + "ak_owner": null, + "room": 2, + "requirement": 3, + "category": null, + "comment": "", + "timestamp": "2022-12-02T12:23:11.890Z", + "manually_resolved": false, + "aks": [ + 1 + ], + "ak_slots": [ + 1 + ] + } +}, +{ + "model": "AKModel.constraintviolation", + "pk": 3, + "fields": { + "type": "soa", + "level": 10, + "event": 2, + "ak_owner": null, + "room": null, + "requirement": null, + "category": null, + "comment": "", + "timestamp": "2022-12-02T12:23:18.301Z", + "manually_resolved": false, + "aks": [ + 2 + ], + "ak_slots": [ + 2 + ] + } +}, +{ + "model": "AKModel.availability", + "pk": 1, + "fields": { + "event": 2, + "person": null, + "room": null, + "ak": 1, + "ak_category": null, + "start": "2020-11-07T08:00:00Z", + "end": "2020-11-07T18:00:00Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 2, + "fields": { + "event": 2, + "person": null, + "room": null, + "ak": 1, + "ak_category": null, + "start": "2020-11-09T10:00:00Z", + "end": "2020-11-09T16:30:00Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 3, + "fields": { + "event": 2, + "person": null, + "room": 1, + "ak": null, + "ak_category": null, + "start": "2020-11-06T17:51:26Z", + "end": "2020-11-10T23:00:00Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 4, + "fields": { + "event": 2, + "person": null, + "room": 2, + "ak": null, + "ak_category": null, + "start": "2020-11-06T17:51:26Z", + "end": "2020-11-06T21:00:00Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 5, + "fields": { + "event": 2, + "person": null, + "room": 2, + "ak": null, + "ak_category": null, + "start": "2020-11-08T15:30:00Z", + "end": "2020-11-08T19:30:00Z" + } +}, +{ + "model": "AKModel.availability", + "pk": 6, + "fields": { + "event": 2, + "person": null, + "room": 2, + "ak": null, + "ak_category": null, + "start": "2020-11-07T18:30:00Z", + "end": "2020-11-07T21:30:00Z" + } +} +] diff --git a/AKModel/tests.py b/AKModel/tests.py index a39b155a..4ba5d32f 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -1 +1,95 @@ -# Create your tests here. +from django.contrib.auth.models import User +from django.test import TestCase +from django.urls import reverse_lazy + +from AKModel.models import Event, AKOwner, AKCategory, AKTrack, AKRequirement, AK, Room, AKSlot, AKOrgaMessage, \ + ConstraintViolation, DefaultSlot + + +class BasicViewTests: + VIEWS = [] + APP_NAME = '' + VIEWS_STAFF_ONLY = [] + + def setUp(self): + self.staff_user = User.objects.create( + username='Test Staff User', email='teststaff@example.com', password='staffpw', + is_staff=True, is_active=True + ) + self.admin_user = User.objects.create( + username='Test Admin User', email='testadmin@example.com', password='adminpw', + is_staff=True, is_superuser=True, is_active=True + ) + self.deactivated_user = User.objects.create( + username='Test Deactivated User', email='testdeactivated@example.com', password='deactivatedpw', + is_staff=True, is_active=False + ) + + def _name_and_url(self, view_name): + """ + Get full view name (with prefix if there is one) and url from raw view definition + + :param view_name: raw definition of a view + :type view_name: (str, dict) + :return: full view name with prefix if applicable, url of the view + :rtype: str, str + """ + view_name_with_prefix = f"{self.APP_NAME}:{view_name[0]}" if self.APP_NAME != "" else view_name[0] + url = reverse_lazy(view_name_with_prefix, kwargs=view_name[1]) + return view_name_with_prefix, url + + def testViewsFor200(self): + for view_name in self.VIEWS: + view_name_with_prefix, url = self._name_and_url(view_name) + response = self.client.get(url) + self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") + + def testAccessControlStaffOnly(self): + self.client.logout() + for view_name in self.VIEWS_STAFF_ONLY: + view_name_with_prefix, url = self._name_and_url(view_name) + response = self.client.get(url) + self.assertEqual(response.status_code, 302, msg=f"{view_name_with_prefix} ({url}) accessible by non-staff") + + self.client.force_login(self.staff_user) + for view_name in self.VIEWS_STAFF_ONLY: + view_name_with_prefix, url = self._name_and_url(view_name) + response = self.client.get(url) + self.assertEqual(response.status_code, 200, + msg=f"{view_name_with_prefix} ({url}) should be accessible for staff (but isn't)") + + self.client.force_login(self.deactivated_user) + for view_name in self.VIEWS_STAFF_ONLY: + view_name_with_prefix, url = self._name_and_url(view_name) + response = self.client.get(url) + self.assertEqual(response.status_code, 302, + msg=f"{view_name_with_prefix} ({url}) still accessible for deactivated user") + + +class ModelViewTests(BasicViewTests, TestCase): + fixtures = ['model.json'] + + ADMIN_MODELS = [ + (Event, 'event'), (AKOwner, 'akowner'), (AKCategory, 'akcategory'), (AKTrack, 'aktrack'), + (AKRequirement, 'akrequirement'), (AK, 'ak'), (Room, 'room'), (AKSlot, 'akslot'), + (AKOrgaMessage, 'akorgamessage'), (ConstraintViolation, 'constraintviolation'), + (DefaultSlot, 'defaultslot') + ] + + def testAdmin(self): + self.client.force_login(self.admin_user) + + for model in self.ADMIN_MODELS: + if model[1] == "event": + continue + view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_add', {})) + response = self.client.get(url) + self.assertEqual(response.status_code, 200, msg=f"Add form for model {model[1]} ({url}) broken") + + for model in self.ADMIN_MODELS: + m = model[0].objects.first() + if m is not None: + view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})) + print(view_name_with_prefix) + response = self.client.get(url) + self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") diff --git a/AKPlan/tests.py b/AKPlan/tests.py index a39b155a..8a7bd4e1 100644 --- a/AKPlan/tests.py +++ b/AKPlan/tests.py @@ -1 +1,28 @@ -# Create your tests here. +from django.test import TestCase + +from AKModel.tests import BasicViewTests + + +class PlanViewTests(BasicViewTests, TestCase): + fixtures = ['model.json'] + APP_NAME = 'plan' + + VIEWS = [ + ('plan_overview', {'event_slug': 'kif42'}), + ('plan_wall', {'event_slug': 'kif42'}), + ('plan_room', {'event_slug': 'kif42', 'pk': 2}), + ('plan_track', {'event_slug': 'kif42', 'pk': 1}), + ] + + def testPlanHidden(self): + view_name_with_prefix, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'})) + + self.client.logout() + response = self.client.get(url) + self.assertContains(response, "Plan is not visible (yet).", + msg_prefix="Plan is visible even though it shouldn't be") + + self.client.force_login(self.staff_user) + response = self.client.get(url) + self.assertNotContains(response, "Plan is not visible (yet).", + msg_prefix="Plan is not visible for staff user") -- GitLab From 3261eea0d82bd7d07c0671e3c75209cc5ff442e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Tue, 6 Dec 2022 23:18:39 +0100 Subject: [PATCH 02/11] Adapt CI settings for fixture loading --- .gitlab-ci.yml | 6 ++++-- AKPlanning/settings_ci.py | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 28c0603b..73637ffc 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -17,11 +17,13 @@ cache: before_script: - python -V # Print out python version for debugging - apt-get -qq update - - apt-get -qq install -y python3-virtualenv python3 python3-dev python3-pip gettext default-libmysqlclient-dev + - apt-get -qq install -y python3-virtualenv python3 python3-dev python3-pip gettext default-mysql-client default-libmysqlclient-dev - export DJANGO_SETTINGS_MODULE=AKPlanning.settings_ci - ./Utils/setup.sh --prod + - mysql --version test: script: - source venv/bin/activate - - python manage.py test --settings AKPlanning.settings_ci --keepdb + - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql + - python manage.py test --settings AKPlanning.settings_ci diff --git a/AKPlanning/settings_ci.py b/AKPlanning/settings_ci.py index 99aa3a7a..77f75f4e 100644 --- a/AKPlanning/settings_ci.py +++ b/AKPlanning/settings_ci.py @@ -16,10 +16,11 @@ DATABASES = { 'PASSWORD': 'mysql', 'OPTIONS': { 'init_command': "SET sql_mode='STRICT_TRANS_TABLES'", - 'charset': "utf8mb4", }, 'TEST': { - 'NAME': 'test', + 'NAME': 'tests', + 'CHARSET': "utf8mb4", + 'COLLATION': 'utf8mb4_unicode_ci', }, } } -- GitLab From 7498df5b98774ad745d44d2faaf353d787e854b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 7 Dec 2022 00:25:35 +0100 Subject: [PATCH 03/11] Add static checks and migration dry-runs to CI --- .gitlab-ci.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 73637ffc..653742b3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -22,6 +22,15 @@ before_script: - ./Utils/setup.sh --prod - mysql --version +check: + script: + - ./Utils/check.sh --all + +check-migrations: + script: + - source venv/bin/activate + - ./manage.py makemigrations --dry-run --check + test: script: - source venv/bin/activate -- GitLab From f300d7e003456fd204160ceec01292990e22a6d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Wed, 7 Dec 2022 00:49:32 +0100 Subject: [PATCH 04/11] Remove debug print statements from tests --- AKDashboard/tests.py | 1 - AKModel/tests.py | 1 - 2 files changed, 2 deletions(-) diff --git a/AKDashboard/tests.py b/AKDashboard/tests.py index dc77d528..9610f5a2 100644 --- a/AKDashboard/tests.py +++ b/AKDashboard/tests.py @@ -65,7 +65,6 @@ class DashboardTests(TestCase): self.event.public = False self.event.save() response = self.client.get(url_dashboard_index) - print(response) self.assertEqual(response.status_code, 200) self.assertFalse(self.event in response.context["events"]) response = self.client.get(url_event_dashboard) diff --git a/AKModel/tests.py b/AKModel/tests.py index 4ba5d32f..c9ae61fb 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -90,6 +90,5 @@ class ModelViewTests(BasicViewTests, TestCase): m = model[0].objects.first() if m is not None: view_name_with_prefix, url = self._name_and_url((f'admin:AKModel_{model[1]}_change', {'object_id': m.pk})) - print(view_name_with_prefix) response = self.client.get(url) self.assertEqual(response.status_code, 200, msg=f"Edit form for model {model[1]} ({url}) broken") -- GitLab From 84d4ccebd2d96aee93f9f9d06d26d388ed73261f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 25 Dec 2022 13:38:39 +0100 Subject: [PATCH 05/11] Add view tests for AKScheduling --- AKScheduling/tests.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/AKScheduling/tests.py b/AKScheduling/tests.py index a39b155a..8b7bcf91 100644 --- a/AKScheduling/tests.py +++ b/AKScheduling/tests.py @@ -1 +1,17 @@ -# Create your tests here. +from django.test import TestCase +from AKModel.tests import BasicViewTests + + +class ModelViewTests(BasicViewTests, TestCase): + fixtures = ['model.json'] + + VIEWS_STAFF_ONLY = [ + ('admin:schedule', {'event_slug': 'kif42'}), + ('admin:slots_unscheduled', {'event_slug': 'kif42'}), + ('admin:constraint-violations', {'slug': 'kif42'}), + ('admin:special-attention', {'slug': 'kif42'}), + ('admin:cleanup-wish-slots', {'event_slug': 'kif42'}), + ('admin:autocreate-availabilities', {'event_slug': 'kif42'}), + ('admin:tracks_manage', {'event_slug': 'kif42'}), + ('admin:enter-interest', {'event_slug': 'kif42', 'pk': 1}), + ] -- GitLab From b2715dcb9fd2a8e3acbea24367768216a755ec1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Sun, 25 Dec 2022 16:14:03 +0100 Subject: [PATCH 06/11] Fix test method titles (naming convention) --- AKModel/tests.py | 6 +++--- AKPlan/tests.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/AKModel/tests.py b/AKModel/tests.py index c9ae61fb..2b76348e 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -38,13 +38,13 @@ class BasicViewTests: url = reverse_lazy(view_name_with_prefix, kwargs=view_name[1]) return view_name_with_prefix, url - def testViewsFor200(self): + def test_views_for_200(self): for view_name in self.VIEWS: view_name_with_prefix, url = self._name_and_url(view_name) response = self.client.get(url) self.assertEqual(response.status_code, 200, msg=f"{view_name_with_prefix} ({url}) broken") - def testAccessControlStaffOnly(self): + def test_access_control_staff_only(self): self.client.logout() for view_name in self.VIEWS_STAFF_ONLY: view_name_with_prefix, url = self._name_and_url(view_name) @@ -76,7 +76,7 @@ class ModelViewTests(BasicViewTests, TestCase): (DefaultSlot, 'defaultslot') ] - def testAdmin(self): + def test_admin(self): self.client.force_login(self.admin_user) for model in self.ADMIN_MODELS: diff --git a/AKPlan/tests.py b/AKPlan/tests.py index 8a7bd4e1..13f671c6 100644 --- a/AKPlan/tests.py +++ b/AKPlan/tests.py @@ -14,7 +14,7 @@ class PlanViewTests(BasicViewTests, TestCase): ('plan_track', {'event_slug': 'kif42', 'pk': 1}), ] - def testPlanHidden(self): + def test_plan_hidden(self): view_name_with_prefix, url = self._name_and_url(('plan_overview', {'event_slug': 'kif23'})) self.client.logout() -- GitLab From 3d1185bb7c337958127bca9383053dc31b64d438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 26 Dec 2022 01:33:54 +0100 Subject: [PATCH 07/11] Add view tests for AKSubmission This includes a new helper method to assert the existence of a particular message --- AKModel/fixtures/model.json | 13 ++++ AKModel/tests.py | 18 +++++ AKSubmission/tests.py | 127 +++++++++++++++++++++++++++++++++++- 3 files changed, 157 insertions(+), 1 deletion(-) diff --git a/AKModel/fixtures/model.json b/AKModel/fixtures/model.json index b75d5093..b5bdcf01 100644 --- a/AKModel/fixtures/model.json +++ b/AKModel/fixtures/model.json @@ -523,6 +523,19 @@ "updated": "2021-05-04T10:30:59.528Z" } }, +{ + "model": "AKModel.akslot", + "pk": 5, + "fields": { + "ak": 1, + "room": null, + "start": null, + "duration": "2.00", + "fixed": false, + "event": 2, + "updated": "2022-12-02T12:23:11.856Z" + } +}, { "model": "AKModel.constraintviolation", "pk": 1, diff --git a/AKModel/tests.py b/AKModel/tests.py index 2b76348e..106de097 100644 --- a/AKModel/tests.py +++ b/AKModel/tests.py @@ -1,4 +1,8 @@ +from typing import List + from django.contrib.auth.models import User +from django.contrib.messages import get_messages +from django.contrib.messages.storage.base import Message from django.test import TestCase from django.urls import reverse_lazy @@ -38,6 +42,20 @@ class BasicViewTests: url = reverse_lazy(view_name_with_prefix, kwargs=view_name[1]) return view_name_with_prefix, url + def _assert_message(self, response, expected_message, msg_prefix=""): + messages:List[Message] = list(get_messages(response.wsgi_request)) + + msg_count = "No message shown to user" + msg_content = "Wrong message, expected '{expected_message}'" + if msg_prefix != "": + msg_count = f"{msg_prefix}: {msg_count}" + msg_content = f"{msg_prefix}: {msg_content}" + + # Check that the last message correctly reports the issue + # (there might be more messages from previous calls that were not already rendered) + self.assertGreater(len(messages), 0, msg=msg_count) + self.assertEqual(messages[-1].message, expected_message, msg=msg_content) + def test_views_for_200(self): for view_name in self.VIEWS: view_name_with_prefix, url = self._name_and_url(view_name) diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py index a39b155a..de0ede7e 100644 --- a/AKSubmission/tests.py +++ b/AKSubmission/tests.py @@ -1 +1,126 @@ -# Create your tests here. +from django.test import TestCase +from django.urls import reverse_lazy + +from AKModel.models import AK, AKSlot +from AKModel.tests import BasicViewTests + + +class ModelViewTests(BasicViewTests, TestCase): + fixtures = ['model.json'] + + VIEWS = [ + ('submission_overview', {'event_slug': 'kif42'}), + ('ak_detail', {'event_slug': 'kif42', 'pk': 1}), + ('ak_history', {'event_slug': 'kif42', 'pk': 1}), + ('ak_edit', {'event_slug': 'kif42', 'pk': 1}), + ('akslot_add', {'event_slug': 'kif42', 'pk': 1}), + ('akmessage_add', {'event_slug': 'kif42', 'pk': 1}), + ('akslot_edit', {'event_slug': 'kif42', 'pk': 5}), + ('akslot_delete', {'event_slug': 'kif42', 'pk': 5}), + ('ak_list', {'event_slug': 'kif42'}), + ('ak_list_by_category', {'event_slug': 'kif42', 'category_pk': 4}), + ('ak_list_by_track', {'event_slug': 'kif42', 'track_pk': 1}), + ('akowner_create', {'event_slug': 'kif42'}), + ('akowner_edit', {'event_slug': 'kif42', 'slug': 'a'}), + ('submit_ak', {'event_slug': 'kif42', 'owner_slug': 'a'}), + ('submit_ak_wish', {'event_slug': 'kif42'}), + ('error_not_configured', {'event_slug': 'kif42'}), + ] + + APP_NAME = 'submit' + + def test_akslot_edit_delete_prevention(self): + """ + Slots planned already may not be modified or deleted in front end + """ + self.client.logout() + + view_name_with_prefix, url = self._name_and_url(('akslot_edit', {'event_slug': 'kif42', 'pk': 1})) + response = self.client.get(url) + self.assertEqual(response.status_code, 302, + msg=f"AK Slot editing ({url}) possible even though slot was already scheduled") + self._assert_message(response, "You cannot edit a slot that has already been scheduled") + + view_name_with_prefix, url = self._name_and_url(('akslot_delete', {'event_slug': 'kif42', 'pk': 1})) + response = self.client.get(url) + self.assertEqual(response.status_code, 302, + msg=f"AK Slot deletion ({url}) possible even though slot was already scheduled") + self._assert_message(response, "You cannot delete a slot that has already been scheduled") + + def test_slot_creation_deletion(self): + ak_args = {'event_slug': 'kif42', 'pk': 1} + redirect_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs=ak_args) + + count_slots = AK.objects.get(pk=1).akslot_set.count() + + create_url = reverse_lazy(f"{self.APP_NAME}:akslot_add", kwargs=ak_args) + response = self.client.post(create_url, {'ak': 1, 'event': 2, 'duration': 1.5}) + self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200, + msg_prefix="Did not correctly trigger redirect") + self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots + 1, + msg="New slot was not correctly saved") + + # Get primary key of newly created Slot + slot_pk = AK.objects.get(pk=1).akslot_set.order_by('pk').last().pk + + edit_url = reverse_lazy(f"{self.APP_NAME}:akslot_edit", kwargs={'event_slug': 'kif42', 'pk': slot_pk}) + response = self.client.get(edit_url) + self.assertEqual(response.status_code, 200, msg=f"Cant open edit view for newly created slot ({edit_url})") + response = self.client.post(edit_url, {'ak': 1, 'event': 2, 'duration': 2}) + self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200, + msg_prefix="Did not correctly trigger redirect") + self.assertEqual(AKSlot.objects.get(pk=slot_pk).duration, 2, + msg="Slot was not correctly changed") + + deletion_url = reverse_lazy(f"{self.APP_NAME}:akslot_delete", kwargs={'event_slug': 'kif42', 'pk': slot_pk}) + response = self.client.get(deletion_url) + self.assertEqual(response.status_code, 200, + msg="Cant open deletion view for newly created slot ({deletion_url})") + response = self.client.post(deletion_url, {}) + self.assertRedirects(response, redirect_url, status_code=302, target_status_code=200, + msg_prefix="Did not correctly trigger redirect") + self.assertFalse(AKSlot.objects.filter(pk=slot_pk).exists(), msg="Slot was not correctly deleted") + self.assertEqual(AK.objects.get(pk=1).akslot_set.count(), count_slots, msg="AK still has to many slots") + + def test_ak_owner_editing(self): + # Test editing of new user + edit_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit_dispatch", kwargs={'event_slug': 'kif42'}) + + base_url = reverse_lazy(f"{self.APP_NAME}:submission_overview", kwargs={'event_slug': 'kif42'}) + response = self.client.post(edit_url, {'owner_id': -1}) + self.assertRedirects(response, base_url, status_code=302, target_status_code=200, + msg_prefix="Did not redirect to start page even though no user was selected") + self._assert_message(response, "No user selected") + + edit_redirect_url = reverse_lazy(f"{self.APP_NAME}:akowner_edit", kwargs={'event_slug': 'kif42', 'slug': 'a'}) + response = self.client.post(edit_url, {'owner_id': 1}) + self.assertRedirects(response, edit_redirect_url, status_code=302, target_status_code=200, + msg_prefix=f"Dispatch redirect failed (should go to {edit_redirect_url})") + + def test_ak_owner_selection(self): + select_url = reverse_lazy(f"{self.APP_NAME}:akowner_select", kwargs={'event_slug': 'kif42'}) + + create_url = reverse_lazy(f"{self.APP_NAME}:akowner_create", kwargs={'event_slug': 'kif42'}) + response = self.client.post(select_url, {'owner_id': -1}) + self.assertRedirects(response, create_url, status_code=302, target_status_code=200, + msg_prefix="Did not redirect to user create view even though no user was specified") + + add_redirect_url = reverse_lazy(f"{self.APP_NAME}:submit_ak", kwargs={'event_slug': 'kif42', 'owner_slug': 'a'}) + response = self.client.post(select_url, {'owner_id': 1}) + self.assertRedirects(response, add_redirect_url, status_code=302, target_status_code=200, + msg_prefix=f"Dispatch redirect to ak submission page failed (should go to {add_redirect_url})") + + def test_orga_message_submission(self): + form_url = reverse_lazy(f"{self.APP_NAME}:akmessage_add", kwargs={'event_slug': 'kif42', 'pk': 1}) + detail_url = reverse_lazy(f"{self.APP_NAME}:ak_detail", kwargs={'event_slug': 'kif42', 'pk': 1}) + + count_messages = AK.objects.get(pk=1).akorgamessage_set.count() + + response = self.client.get(form_url) + self.assertEqual(response.status_code, 200, msg="Could not load message form view") + response = self.client.post(form_url, {'ak': 1, 'event': 2, 'text': 'Test message text'}) + self.assertRedirects(response, detail_url, status_code=302, target_status_code=200, + msg_prefix=f"Did not trigger redirect to ak detail page ({detail_url})") + self._assert_message(response, "Message to organizers successfully saved") + self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1, + msg="Message was not correctly saved") -- GitLab From 9142cc2b15b83711749478bcfee5b9d9856edcbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 26 Dec 2022 01:39:25 +0100 Subject: [PATCH 08/11] Add coverage check to GitLab CI --- .gitlab-ci.yml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 653742b3..f62a299b 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,4 +35,13 @@ test: script: - source venv/bin/activate - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - - python manage.py test --settings AKPlanning.settings_ci + - pip install pytest-cov + - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci + - coverage report + - coverage xml + coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: coverage.xml -- GitLab From 8442afd8f601798ad66fb1b88ed2cf9f581cbfe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 26 Dec 2022 11:48:14 +0100 Subject: [PATCH 09/11] Visualize unit test results in GitLab --- .gitlab-ci.yml | 3 ++- AKPlanning/settings_ci.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index f62a299b..51954c1e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,7 @@ test: script: - source venv/bin/activate - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - - pip install pytest-cov + - pip install pytest-cov unittest-xml-reporting - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci - coverage report - coverage xml @@ -45,3 +45,4 @@ test: coverage_report: coverage_format: cobertura path: coverage.xml + junit: unit.xml diff --git a/AKPlanning/settings_ci.py b/AKPlanning/settings_ci.py index 77f75f4e..c8e6a62e 100644 --- a/AKPlanning/settings_ci.py +++ b/AKPlanning/settings_ci.py @@ -24,3 +24,6 @@ DATABASES = { }, } } + +TEST_RUNNER = 'xmlrunner.extra.djangotestrunner.XMLTestRunner' +TEST_OUTPUT_FILE_NAME = 'unit.xml' -- GitLab From dfd48ae0073a9a695ebd5c90c3b8dbb4b3ec3667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 26 Dec 2022 12:11:27 +0100 Subject: [PATCH 10/11] Make sure coverage is computed after failed tests, too --- .gitlab-ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 51954c1e..7c730cff 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,6 +37,8 @@ test: - echo "GRANT ALL on *.* to '${MYSQL_USER}';"| mysql -u root --password="${MYSQL_ROOT_PASSWORD}" -h mysql - pip install pytest-cov unittest-xml-reporting - coverage run --source='.' manage.py test --settings AKPlanning.settings_ci + after_script: + - source venv/bin/activate - coverage report - coverage xml coverage: '/(?i)total.*? (100(?:\.0+)?\%|[1-9]?\d(?:\.\d+)?\%)$/' -- GitLab From 7c148274fffb0ea615b1a70f1133d8c34fa9064c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20H=C3=A4ttasch?= <benjamin.haettasch@fachschaft.informatik.tu-darmstadt.de> Date: Mon, 26 Dec 2022 16:12:07 +0100 Subject: [PATCH 11/11] Add test for interest counter API endpoint Fix issue detected while creating test --- AKSubmission/api.py | 7 ++++--- AKSubmission/tests.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/AKSubmission/api.py b/AKSubmission/api.py index 29a6f42a..db46d40c 100644 --- a/AKSubmission/api.py +++ b/AKSubmission/api.py @@ -26,8 +26,8 @@ def increment_interest_counter(request, event_slug, pk, **kwargs): """ Increment interest counter for AK """ - ak = AK.objects.get(pk=pk) - if ak: + try: + ak = AK.objects.get(pk=pk) # Check whether interest indication is currently allowed current_timestamp = datetime.now().astimezone(ak.event.timezone) if ak_interest_indication_active(ak.event, current_timestamp): @@ -35,4 +35,5 @@ def increment_interest_counter(request, event_slug, pk, **kwargs): ak.save() return Response({'interest_counter': ak.interest_counter}, status=status.HTTP_200_OK) return Response(status=status.HTTP_403_FORBIDDEN) - return Response(status=status.HTTP_404_NOT_FOUND) + except AK.DoesNotExist: + return Response(status=status.HTTP_404_NOT_FOUND) diff --git a/AKSubmission/tests.py b/AKSubmission/tests.py index de0ede7e..4cff0830 100644 --- a/AKSubmission/tests.py +++ b/AKSubmission/tests.py @@ -1,7 +1,10 @@ +from datetime import timedelta + from django.test import TestCase from django.urls import reverse_lazy +from django.utils.datetime_safe import datetime -from AKModel.models import AK, AKSlot +from AKModel.models import AK, AKSlot, Event from AKModel.tests import BasicViewTests @@ -124,3 +127,36 @@ class ModelViewTests(BasicViewTests, TestCase): self._assert_message(response, "Message to organizers successfully saved") self.assertEqual(AK.objects.get(pk=1).akorgamessage_set.count(), count_messages + 1, msg="Message was not correctly saved") + + def test_interest_api(self): + interest_api_url = "/kif42/api/ak/1/indicate-interest/" + + ak = AK.objects.get(pk=1) + event = Event.objects.get(slug='kif42') + ak_interest_counter = ak.interest_counter + + response = self.client.get(interest_api_url) + self.assertEqual(response.status_code, 405, "Should not be accessible via GET") + + event.interest_start = datetime.now().astimezone(event.timezone) + timedelta(minutes=-10) + event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=+10) + event.save() + + response = self.client.post(interest_api_url) + self.assertEqual(response.status_code, 200, f"API end point not working ({interest_api_url})") + self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, "Counter was not increased") + + event.interest_end = datetime.now().astimezone(event.timezone) + timedelta(minutes=-2) + event.save() + + response = self.client.post(interest_api_url) + self.assertEqual(response.status_code, 403, + "API end point still reachable even though interest indication window ended ({interest_api_url})") + self.assertEqual(AK.objects.get(pk=1).interest_counter, ak_interest_counter + 1, + "Counter was increased even though interest indication window ended") + + invalid_interest_api_url = "/kif42/api/ak/-1/indicate-interest/" + response = self.client.post(invalid_interest_api_url) + self.assertEqual(response.status_code, 404, f"Invalid URL reachable ({interest_api_url})") + + -- GitLab