From 8aec4de38e7d5885d9069374dd818c90df91ddd2 Mon Sep 17 00:00:00 2001 From: Gagan Deep Date: Tue, 14 May 2024 22:23:32 +0530 Subject: [PATCH] [tests] Fixed tests that polluted the test environment Mock NOTIFICATION_TYPE and NOTIFICATION_CHOICES to avoid polluting the test environment from notification types registered in the test. --- openwisp_notifications/tasks.py | 10 ++--- openwisp_notifications/tests/test_api.py | 9 ++-- openwisp_notifications/tests/test_helpers.py | 40 ++++++++++++++++++ .../tests/test_ignore_object_notification.py | 26 ++++++------ .../tests/test_notification_setting.py | 41 ++++++++----------- .../tests/test_notifications.py | 35 ++++++++-------- openwisp_notifications/tests/test_utils.py | 14 ++++--- tests/openwisp2/sample_notifications/apps.py | 4 +- 8 files changed, 104 insertions(+), 75 deletions(-) diff --git a/openwisp_notifications/tasks.py b/openwisp_notifications/tasks.py index 3f7f0167..9869d46a 100644 --- a/openwisp_notifications/tasks.py +++ b/openwisp_notifications/tasks.py @@ -7,8 +7,8 @@ from django.db.utils import OperationalError from django.utils import timezone +from openwisp_notifications import types from openwisp_notifications.swapper import load_model, swapper_load_model -from openwisp_notifications.types import NOTIFICATION_TYPES from openwisp_utils.tasks import OpenwispCeleryTask User = get_user_model() @@ -105,7 +105,7 @@ def update_superuser_notification_settings(instance_id, is_superuser, is_created create_notification_settings( user=user, organizations=Organization.objects.all(), - notification_types=NOTIFICATION_TYPES.keys(), + notification_types=types.NOTIFICATION_TYPES.keys(), ) @@ -119,7 +119,7 @@ def ns_register_unregister_notification_type( """ notification_types = ( - [notification_type] if notification_type else NOTIFICATION_TYPES.keys() + [notification_type] if notification_type else types.NOTIFICATION_TYPES.keys() ) organizations = Organization.objects.all() @@ -166,7 +166,7 @@ def update_org_user_notificationsetting(org_user_id, user_id, org_id, is_org_adm create_notification_settings( user=user, organizations=[organization], - notification_types=NOTIFICATION_TYPES.keys(), + notification_types=types.NOTIFICATION_TYPES.keys(), ) @@ -192,7 +192,7 @@ def ns_organization_created(instance_id): create_notification_settings( user=user, organizations=[organization], - notification_types=NOTIFICATION_TYPES.keys(), + notification_types=types.NOTIFICATION_TYPES.keys(), ) diff --git a/openwisp_notifications/tests/test_api.py b/openwisp_notifications/tests/test_api.py index 3a68267f..52b2a87f 100644 --- a/openwisp_notifications/tests/test_api.py +++ b/openwisp_notifications/tests/test_api.py @@ -13,8 +13,8 @@ from openwisp_notifications.swapper import load_model, swapper_load_model from openwisp_notifications.tests.test_helpers import ( TEST_DATETIME, + mock_notification_types, register_notification_type, - unregister_notification_type, ) from openwisp_users.tests.test_api import AuthenticationMixin from openwisp_users.tests.utils import TestOrganizationMixin @@ -436,6 +436,7 @@ def test_list_view_notification_cache(self): self.assertEqual(response.data['count'], number_of_notifications) @capture_any_output() + @mock_notification_types def test_malformed_notifications(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -469,9 +470,8 @@ def test_malformed_notifications(self): self.assertEqual(response.status_code, 404) self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) - unregister_notification_type('test_type') - @capture_any_output() + @mock_notification_types @patch('openwisp_notifications.tasks.delete_obsolete_objects.delay') def test_obsolete_notifications_busy_worker(self, mocked_task): """ @@ -516,8 +516,6 @@ def test_obsolete_notifications_busy_worker(self, mocked_task): self.assertEqual(response.status_code, 404) self.assertDictEqual(response.data, {'detail': NOT_FOUND_ERROR}) - unregister_notification_type('test_type') - def test_notification_setting_list_api(self): self._create_org_user(is_admin=True) number_of_settings = NotificationSetting.objects.filter(user=self.admin).count() @@ -904,6 +902,7 @@ def test_list_ignore_obj_notification_api(self, mocked_task): self.assertIn('object_content_type', ignore_obj_notification) self.assertIsNone(ignore_obj_notification['valid_till']) + @capture_any_output() @patch('openwisp_notifications.tasks.delete_notification.delay') def test_deleted_notification_type(self, *args): notify.send(sender=self.admin, type='default', target=self.admin) diff --git a/openwisp_notifications/tests/test_helpers.py b/openwisp_notifications/tests/test_helpers.py index 34a9995f..a7037b1a 100644 --- a/openwisp_notifications/tests/test_helpers.py +++ b/openwisp_notifications/tests/test_helpers.py @@ -1,4 +1,6 @@ +from copy import deepcopy from datetime import datetime +from unittest.mock import patch from django.contrib.messages.storage.fallback import FallbackStorage from django.http import HttpRequest @@ -6,6 +8,7 @@ from openwisp_notifications.swapper import load_model from openwisp_notifications.tasks import ns_register_unregister_notification_type +from openwisp_notifications.types import NOTIFICATION_CHOICES, NOTIFICATION_TYPES from openwisp_notifications.types import ( register_notification_type as base_register_notification_type, ) @@ -13,6 +16,7 @@ unregister_notification_type as base_unregister_notification_type, ) +Notification = load_model('Notification') NotificationSetting = load_model('NotificationSetting') TEST_DATETIME = datetime(2020, 5, 4, 0, 0, 0, 0, timezone.get_default_timezone()) @@ -47,3 +51,39 @@ def unregister_notification_type(type_name): def notification_related_object_url(obj, field, *args, **kwargs): related_obj = getattr(obj, field) return f'https://{related_obj}.example.com' + + +def mock_notification_types(func): + """ + This decorator mocks the NOTIFICATION_CHOICES and NOTIFICATION_TYPES + to prevent polluting the test environment with any notification types + registered during the test. + """ + + def wrapper(*args, **kwargs): + with patch.multiple( + 'openwisp_notifications.types', + NOTIFICATION_CHOICES=deepcopy(NOTIFICATION_CHOICES), + NOTIFICATION_TYPES=deepcopy(NOTIFICATION_TYPES), + ): + from openwisp_notifications import types + + # Here we mock the choices for model fields directly. This is + # because the choices for model fields are defined during the + # model loading phase, and any changes to the mocked + # NOTIFICATION_CHOICES don't affect the model field choices. + # So, we mock the choices directly here to ensure that any + # changes to the mocked NOTIFICATION_CHOICES reflect in the + # model field choices. + with patch.object( + Notification._meta.get_field('type'), + 'choices', + types.NOTIFICATION_CHOICES, + ), patch.object( + NotificationSetting._meta.get_field('type'), + 'choices', + types.NOTIFICATION_CHOICES, + ): + return func(*args, **kwargs) + + return wrapper diff --git a/openwisp_notifications/tests/test_ignore_object_notification.py b/openwisp_notifications/tests/test_ignore_object_notification.py index 61a4a018..8c5e6a31 100644 --- a/openwisp_notifications/tests/test_ignore_object_notification.py +++ b/openwisp_notifications/tests/test_ignore_object_notification.py @@ -9,8 +9,8 @@ from openwisp_notifications.swapper import load_model from openwisp_users.tests.utils import TestOrganizationMixin -from ..types import NOTIFICATION_TYPES -from .test_helpers import register_notification_type +from ..types import get_notification_configuration +from .test_helpers import mock_notification_types, register_notification_type IgnoreObjectNotification = load_model('IgnoreObjectNotification') Notification = load_model('Notification') @@ -46,20 +46,18 @@ def test_notification_for_disabled_object(self, mocked_task): notify.send(sender=self.admin, type='default', target=self.obj) self.assertEqual(Notification.objects.count(), 0) + @mock_notification_types @patch('openwisp_notifications.tasks.delete_ignore_object_notification.apply_async') def test_related_object_deleted(self, *args): - type_config = NOTIFICATION_TYPES['default'] - with patch('openwisp_notifications.types.NOTIFICATION_TYPES', {}): - register_notification_type( - 'test', type_config, models=[self.obj._meta.model] - ) - IgnoreObjectNotification.objects.create( - object=self.obj, - user=self.admin, - valid_till=(timezone.now() + timezone.timedelta(days=1)), - ) - self.obj.delete() - self.assertEqual(on_queryset.count(), 0) + type_config = get_notification_configuration('default') + register_notification_type('test', type_config, models=[self.obj._meta.model]) + IgnoreObjectNotification.objects.create( + object=self.obj, + user=self.admin, + valid_till=(timezone.now() + timezone.timedelta(days=1)), + ) + self.obj.delete() + self.assertEqual(on_queryset.count(), 0) @patch.object( IgnoreObjectNotification.objects, 'filter', side_effect=OperationalError diff --git a/openwisp_notifications/tests/test_notification_setting.py b/openwisp_notifications/tests/test_notification_setting.py index 2bc8c8f4..254ddb9f 100644 --- a/openwisp_notifications/tests/test_notification_setting.py +++ b/openwisp_notifications/tests/test_notification_setting.py @@ -1,4 +1,3 @@ -from django.core.exceptions import ImproperlyConfigured from django.db.models.signals import post_save from django.test import TransactionTestCase @@ -9,10 +8,9 @@ from openwisp_notifications.tests.test_helpers import ( base_register_notification_type, base_unregister_notification_type, + mock_notification_types, register_notification_type, - unregister_notification_type, ) -from openwisp_notifications.types import get_notification_configuration from openwisp_users.tests.utils import TestOrganizationMixin test_notification_type = { @@ -32,15 +30,7 @@ class TestNotificationSetting(TestOrganizationMixin, TransactionTestCase): def setUp(self): - if not Organization.objects.first(): - self._create_org(name='default', slug='default') - - def tearDown(self): - super().tearDown() - try: - unregister_notification_type('test') - except ImproperlyConfigured: - pass + self.default_org = self._get_org('default') def _create_staff_org_admin(self): return self._create_org_user(user=self._create_operator(), is_admin=True) @@ -56,6 +46,7 @@ def test_user_created(self): self._get_user() self.assertEqual(ns_queryset.count(), 0) + @mock_notification_types def test_notification_type_registered(self): register_notification_type('test', test_notification_type) queryset = NotificationSetting.objects.filter(type='test') @@ -95,6 +86,7 @@ def test_organization_user(self): self.assertEqual(ns_queryset.filter(deleted=False).count(), 1) self.assertEqual(ns_queryset.filter(deleted=True).count(), 1) + @mock_notification_types def test_register_notification_org_user(self): self._create_staff_org_admin() @@ -103,13 +95,15 @@ def test_register_notification_org_user(self): register_notification_type('test', test_notification_type) self.assertEqual(queryset.count(), 1) + @mock_notification_types def test_post_migration_handler(self): + from openwisp_notifications.types import NOTIFICATION_CHOICES + # Simulates loading of app when Django server starts admin = self._get_admin() org_user = self._create_staff_org_admin() self.assertEqual(ns_queryset.count(), 3) - default_type_config = get_notification_configuration('default') base_unregister_notification_type('default') base_register_notification_type('test', test_notification_type) notification_type_registered_unregistered_handler(sender=self) @@ -119,16 +113,14 @@ def test_post_migration_handler(self): # Notification Settings for "test" type are created queryset = NotificationSetting.objects.filter(deleted=False) - if NotificationSetting._meta.app_label == 'sample_notifications': - self.assertEqual(queryset.count(), 6) - self.assertEqual(queryset.filter(user=admin).count(), 4) - self.assertEqual(queryset.filter(user=org_user.user).count(), 2) - else: - self.assertEqual(queryset.count(), 3) - self.assertEqual(queryset.filter(user=admin).count(), 2) - self.assertEqual(queryset.filter(user=org_user.user).count(), 1) - - base_register_notification_type('default', default_type_config) + notification_types_count = len(NOTIFICATION_CHOICES) + self.assertEqual(queryset.count(), 3 * notification_types_count) + self.assertEqual( + queryset.filter(user=admin).count(), 2 * notification_types_count + ) + self.assertEqual( + queryset.filter(user=org_user.user).count(), 1 * notification_types_count + ) def test_superuser_demoted_to_user(self): admin = self._get_admin() @@ -187,6 +179,7 @@ def test_multiple_org_membership(self): OrganizationUser.objects.create(user=user, organization=test_org, is_admin=True) self.assertEqual(ns_queryset.count(), 2) + @mock_notification_types def test_notification_setting_full_clean(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -207,8 +200,6 @@ def test_notification_setting_full_clean(self): self.assertIsNone(notification_setting.email) self.assertIsNone(notification_setting.web) - unregister_notification_type('test_type') - def test_organization_user_updated(self): default_org = Organization.objects.first() org_user = self._create_staff_org_admin() diff --git a/openwisp_notifications/tests/test_notifications.py b/openwisp_notifications/tests/test_notifications.py index da076552..87ef23a2 100644 --- a/openwisp_notifications/tests/test_notifications.py +++ b/openwisp_notifications/tests/test_notifications.py @@ -25,11 +25,11 @@ from openwisp_notifications.signals import notify from openwisp_notifications.swapper import load_model, swapper_load_model from openwisp_notifications.tests.test_helpers import ( + mock_notification_types, register_notification_type, unregister_notification_type, ) from openwisp_notifications.types import ( - NOTIFICATION_CHOICES, _unregister_notification_choice, get_notification_configuration, ) @@ -322,6 +322,7 @@ def test_default_notification_type(self): ) self.assertEqual(n.email_subject, '[example.com] Default Notification Subject') + @mock_notification_types def test_misc_notification_type_validation(self): with self.subTest('Registering with incomplete notification configuration.'): with self.assertRaises(AssertionError): @@ -339,6 +340,7 @@ def test_misc_notification_type_validation(self): with self.assertRaises(ImproperlyConfigured): unregister_notification_type(dict()) + @mock_notification_types def test_notification_type_message_template(self): message_template = { 'level': 'info', @@ -374,7 +376,10 @@ def test_notification_type_message_template(self): ) self.assertEqual(n.message, message) + @mock_notification_types def test_register_unregister_notification_type(self): + from openwisp_notifications.types import NOTIFICATION_CHOICES + test_type = { 'verbose_name': 'Test Notification Type', 'level': 'test', @@ -437,6 +442,7 @@ def test_notification_email(self): self._create_notification() self.assertEqual(len(mail.outbox), 1) + @mock_notification_types def test_missing_relation_object(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -487,8 +493,7 @@ def test_missing_relation_object(self): n_count = notification_queryset.count() self.assertEqual(n_count, 0) - unregister_notification_type('test_type') - + @mock_notification_types def test_notification_type_related_object_url(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -545,9 +550,8 @@ def test_notification_type_related_object_url(self): self.assertEqual(notification.actor_url, 'https://actor.example.com') self.assertEqual(notification.target_url, 'https://target.example.com') - unregister_notification_type('test_type') - @capture_any_output() + @mock_notification_types @patch('openwisp_notifications.tasks.delete_notification.delay') def test_notification_invalid_message_attribute(self, mocked_task): self.notification_options.update({'type': 'test_type'}) @@ -574,7 +578,6 @@ def test_notification_invalid_message_attribute(self, mocked_task): 'Error encountered in generating notification email', ) mocked_task.assert_called_with(notification_id=notification.id) - unregister_notification_type('test_type') def test_related_objects_database_query(self): operator = self._get_operator() @@ -635,16 +638,15 @@ def test_delete_old_notification(self): tasks.delete_old_notifications.delay(days_old) self.assertEqual(notification_queryset.count(), 1) + @mock_notification_types def test_unregistered_notification_type_related_notification(self): # Notifications related to notification type should # get deleted on unregistration of notification type - default_type_config = get_notification_configuration('default') self.notification_options.update({'type': 'default'}) unregister_notification_type('default') self.assertEqual(notification_queryset.count(), 0) - register_notification_type('default', default_type_config) - + @mock_notification_types def test_notification_type_email_notification_setting_true(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -672,8 +674,7 @@ def test_notification_type_email_notification_setting_true(self): self._create_notification() self.assertEqual(len(mail.outbox), 0) - unregister_notification_type('test_type') - + @mock_notification_types def test_notification_type_email_notification_setting_false(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -700,8 +701,7 @@ def test_notification_type_email_notification_setting_false(self): self._create_notification() self.assertEqual(len(mail.outbox), 1) - unregister_notification_type('test_type') - + @mock_notification_types def test_notification_type_web_notification_setting_true(self): self.notification_options.update({'target': self._get_org_user()}) test_type = { @@ -727,8 +727,7 @@ def test_notification_type_web_notification_setting_true(self): self._create_notification() self.assertEqual(notification_queryset.count(), 0) - unregister_notification_type('test_type') - + @mock_notification_types def test_notification_type_web_notification_setting_false(self): target_obj = self._get_org_user() self.notification_options.update({'target': target_obj}) @@ -764,8 +763,7 @@ def test_notification_type_web_notification_setting_false(self): self._create_notification() self.assertEqual(notification_queryset.count(), 1) - unregister_notification_type('test_type') - + @mock_notification_types def test_notification_type_email_web_notification_defaults(self): test_type = { 'verbose_name': 'Test Notification Type', @@ -780,8 +778,6 @@ def test_notification_type_email_web_notification_defaults(self): self.assertTrue(notification_type_config['web_notification']) self.assertTrue(notification_type_config['email_notification']) - unregister_notification_type('test_type') - def test_inactive_user_not_receive_notification(self): target = self._get_org_user() self.notification_options.update({'target': target}) @@ -846,6 +842,7 @@ def test_post_migrate_handler_celery_broker_unreachable(self, mocked_logger, *ar ) mocked_logger.assert_called_once() + @mock_notification_types @patch.object(post_migrate, 'receivers', []) @patch( 'openwisp_notifications.tasks.ns_register_unregister_notification_type.delay', diff --git a/openwisp_notifications/tests/test_utils.py b/openwisp_notifications/tests/test_utils.py index c21034bc..807d7d7b 100644 --- a/openwisp_notifications/tests/test_utils.py +++ b/openwisp_notifications/tests/test_utils.py @@ -54,17 +54,21 @@ class TestChecks(TestCase, TestOrganizationMixin): ) def test_cors_not_configured(self): # If INSTALLED_APPS not configured - with patch('openwisp_notifications.types.NOTIFICATION_TYPES', dict(),), patch( - 'openwisp_utils.admin_theme.menu.MENU', {} - ), self.modify_settings( + with patch.multiple( + 'openwisp_notifications.types', + NOTIFICATION_TYPES={}, + NOTIFICATION_CHOICES=[], + ), patch('openwisp_utils.admin_theme.menu.MENU', {}), self.modify_settings( INSTALLED_APPS={'remove': 'corsheaders'} ), StringIO() as stderr: management.call_command('check', stderr=stderr) self.assertIn('django-cors-headers', stderr.getvalue()) # If MIDDLEWARE not configured - with patch( - 'openwisp_notifications.types.NOTIFICATION_TYPES', dict() + with patch.multiple( + 'openwisp_notifications.types', + NOTIFICATION_TYPES={}, + NOTIFICATION_CHOICES=[], ), self.modify_settings( MIDDLEWARE={'remove': 'corsheaders.middleware.CorsMiddleware'} ), StringIO() as stderr: diff --git a/tests/openwisp2/sample_notifications/apps.py b/tests/openwisp2/sample_notifications/apps.py index 129a3f57..d8d31930 100644 --- a/tests/openwisp2/sample_notifications/apps.py +++ b/tests/openwisp2/sample_notifications/apps.py @@ -15,7 +15,7 @@ class SampleNotificationsConfig(OpenwispNotificationsConfig): def ready(self): super().ready() self.register_notification_types() - self.connect_recievers() + self.connect_receivers() def register_notification_types(self): register_notification_type( @@ -29,7 +29,7 @@ def register_notification_types(self): }, ) - def connect_recievers(self): + def connect_receivers(self): from openwisp_notifications.handlers import register_notification_cache_update Organization = load_model('openwisp_users', 'Organization')