From 3ee603b963aff91739c8bb40ef7dc64fe5d3715c Mon Sep 17 00:00:00 2001 From: Sergei Maertens Date: Wed, 9 Oct 2024 20:30:27 +0200 Subject: [PATCH] :white_check_mark: [#16] Add tests for the model/admin base setup Ensure that the base assumptions and functionality work as expected. --- src/woo_publications/logging/models.py | 4 +- .../logging/tests/__init__.py | 0 .../logging/tests/test_proxy_model.py | 237 ++++++++++++++++++ 3 files changed, 239 insertions(+), 2 deletions(-) create mode 100644 src/woo_publications/logging/tests/__init__.py create mode 100644 src/woo_publications/logging/tests/test_proxy_model.py diff --git a/src/woo_publications/logging/models.py b/src/woo_publications/logging/models.py index c00930a2..5773d2a0 100644 --- a/src/woo_publications/logging/models.py +++ b/src/woo_publications/logging/models.py @@ -10,7 +10,7 @@ class ActingUser(TypedDict): - identifier: str + identifier: int | str display_name: str @@ -49,7 +49,7 @@ def save(self, *args, **kwargs): if self.extra_data is None: raise ValueError("'extra_data' may not be empty.") if not isinstance(self.extra_data, dict): - raise TypeError("'extra_data' must be a JSON object (python dict)") + raise TypeError("'extra_data' must be a JSON object (python dict).") # ensure that we always track the event self._validate_event() diff --git a/src/woo_publications/logging/tests/__init__.py b/src/woo_publications/logging/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/woo_publications/logging/tests/test_proxy_model.py b/src/woo_publications/logging/tests/test_proxy_model.py new file mode 100644 index 00000000..e737b6f3 --- /dev/null +++ b/src/woo_publications/logging/tests/test_proxy_model.py @@ -0,0 +1,237 @@ +from django.test import TestCase + +from timeline_logger.models import TimelineLog + +from woo_publications.accounts.tests.factories import UserFactory + +from ..constants import Events +from ..models import TimelineLogProxy + + +class ProxyModelTests(TestCase): + + def test_extra_metadata_required(self): + with ( + self.subTest("extra_data=null not allowed"), + self.assertRaisesMessage(ValueError, "'extra_data' may not be empty."), + ): + TimelineLogProxy.objects.create(extra_data=None) + + with ( + self.subTest("extra_data top level must be dict"), + self.assertRaisesMessage(TypeError, "'extra_data' must be a JSON object"), + ): + TimelineLogProxy.objects.create(extra_data=["foo"]) + + def test_event_must_be_specified_and_valid(self): + extra_data_base = { + "acting_user": {"identifier": 123, "display_name": "Herbert"}, + } + + for invalid in ( + {}, + {"event": "afterPartyOnceTheProjectIsDone"}, + ): + with ( + self.subTest("unknown event is blocked"), + self.assertRaisesMessage( + ValueError, "The extra data must contain an 'event' key from" + ), + ): + TimelineLogProxy.objects.create( + extra_data={**extra_data_base, **invalid}, + ) + + for value, _ in Events.choices: + with self.subTest("known events are valid", event=value): + extra_data = { + **extra_data_base, + "event": value, + } + + result = TimelineLogProxy.objects.create(extra_data=extra_data) + + self.assertIsNotNone(result.pk) + + def test_event_extraction(self): + with self.subTest("log record complying with the validation rules"): + record = TimelineLogProxy.objects.create( + extra_data={ + "event": Events.read, + "acting_user": {"identifier": 123, "display_name": "Herbert"}, + } + ) + + self.assertIs(record.event, Events.read) + + with self.subTest("log records with absent key"): + # we can't guarantee that corrupt data will never enter the database due + # to external factors or mistakes made in the future by developers. our + # code needs to be robust for those situations + _broken_record_1 = TimelineLog.objects.create(extra_data=None) + _broken_record_2 = TimelineLog.objects.create(extra_data={}) + _broken_record_3 = TimelineLog.objects.create(extra_data=[]) + + broken_record_1 = TimelineLogProxy.objects.get(pk=_broken_record_1.pk) + self.assertEqual(broken_record_1.event, "unknown") + + broken_record_2 = TimelineLogProxy.objects.get(pk=_broken_record_2.pk) + self.assertEqual(broken_record_2.event, "unknown") + + broken_record_3 = TimelineLogProxy.objects.get(pk=_broken_record_3.pk) + self.assertEqual(broken_record_3.event, "unknown") + + with self.subTest("log record with unexpected event value"): + _broken_record_4 = TimelineLog.objects.create( + extra_data={ + "event": "wildAfterPartyFollowingTheRelease", + } + ) + + broken_record_4 = TimelineLogProxy.objects.get(pk=_broken_record_4.pk) + self.assertEqual(broken_record_4.event, "unknown") + + def test_acting_user_must_be_specified_and_valid(self): + extra_data_base = {"event": Events.read} + + with ( + self.subTest("missing acting_user key"), + self.assertRaisesMessage( + ValueError, + "Audit logs must contain the 'acting_user' key in the metadata.", + ), + ): + TimelineLogProxy.objects.create(extra_data={"event": Events.read}) + + invalid_user_samples = ( + {}, + [], + None, + 123, + {"foo": "bar"}, + {"identifier": "123"}, + {"identifier": 123}, + {"display_name": "Annie"}, + ) + for invalid in invalid_user_samples: + with ( + self.subTest("invalid user details are blocked", user_data=invalid), + self.assertRaisesMessage( + ValueError, + "The user details in audit logs must contain the 'identifier' " + "and 'display_name' keys.", + ), + ): + TimelineLogProxy.objects.create( + extra_data={**extra_data_base, "acting_user": invalid}, + ) + + valid_user_samples = ( + {"identifier": "123", "display_name": "Annie"}, + {"identifier": 123, "display_name": "Annie"}, + ) + for valid in valid_user_samples: + with self.subTest("valid data is accepted", user_data=valid): + extra_data = {**extra_data_base, "acting_user": valid} + + result = TimelineLogProxy.objects.create(extra_data=extra_data) + + self.assertIsNotNone(result.pk) + + def test_acting_user_extraction(self): + # we can't guarantee that corrupt data will never enter the database due + # to external factors or mistakes made in the future by developers. our + # code needs to be robust for those situations + + with self.subTest( + "log record complying with the validation rules, no django user" + ): + record = TimelineLogProxy.objects.create( + extra_data={ + "event": Events.read, + "acting_user": {"identifier": 123, "display_name": "Herbert"}, + } + ) + + acting_user, django_user = record.acting_user + + self.assertIsNone(django_user) + self.assertEqual( + acting_user, + { + "identifier": 123, + "display_name": "Herbert", + }, + ) + + with self.subTest( + "log record complying with the validation rules, with django user" + ): + _django_user = UserFactory.create() + record = TimelineLogProxy.objects.create( + user=_django_user, + extra_data={ + "event": Events.read, + "acting_user": {"identifier": 123, "display_name": "Herbert"}, + }, + ) + + acting_user, django_user = record.acting_user + + self.assertIsNotNone(django_user) + self.assertEqual(django_user, _django_user) + self.assertEqual( + acting_user, + { + "identifier": 123, + "display_name": "Herbert", + }, + ) + + with self.subTest("log records with absent key"): + _broken_record_1 = TimelineLog.objects.create(extra_data=None) + _broken_record_2 = TimelineLog.objects.create(extra_data={}) + _broken_record_3 = TimelineLog.objects.create(extra_data=[]) + + broken_record_1 = TimelineLogProxy.objects.get(pk=_broken_record_1.pk) + _acting_user_1 = broken_record_1.acting_user[0] + self.assertEqual( + _acting_user_1, + {"identifier": "unknown", "display_name": "unknown"}, + ) + + broken_record_2 = TimelineLogProxy.objects.get(pk=_broken_record_2.pk) + _acting_user_2 = broken_record_2.acting_user[0] + self.assertEqual( + _acting_user_2, + {"identifier": "unknown", "display_name": "unknown"}, + ) + + broken_record_3 = TimelineLogProxy.objects.get(pk=_broken_record_3.pk) + _acting_user_3 = broken_record_3.acting_user[0] + self.assertEqual( + _acting_user_3, + {"identifier": "unknown", "display_name": "unknown"}, + ) + + with self.subTest("log records with incomplete data"): + _broken_record_4 = TimelineLog.objects.create( + extra_data={"acting_user": {"identifier": 123}} + ) + _broken_record_5 = TimelineLog.objects.create( + extra_data={"acting_user": {"display_name": "Margareth"}} + ) + + broken_record_4 = TimelineLogProxy.objects.get(pk=_broken_record_4.pk) + _acting_user_4 = broken_record_4.acting_user[0] + self.assertEqual( + _acting_user_4, + {"identifier": 123, "display_name": "unknown"}, + ) + + broken_record_5 = TimelineLogProxy.objects.get(pk=_broken_record_5.pk) + _acting_user_5 = broken_record_5.acting_user[0] + self.assertEqual( + _acting_user_5, + {"identifier": "unknown", "display_name": "Margareth"}, + )