|
1 | 1 | from unittest.mock import MagicMock, call, patch |
2 | 2 |
|
3 | 3 | from django.conf import settings |
| 4 | +from django.db import connection |
4 | 5 | from django.db.models import Q |
5 | 6 | from django.test import override_settings |
| 7 | +from django.test.utils import CaptureQueriesContext |
6 | 8 |
|
7 | 9 | import pytest |
8 | 10 |
|
|
14 | 16 | UserNotificationsGrouper, |
15 | 17 | ) |
16 | 18 | from baserow.core.notifications.models import Notification, NotificationRecipient |
| 19 | +from baserow.core.notifications.registries import ( |
| 20 | + EmailNotificationTypeMixin, |
| 21 | + NotificationType, |
| 22 | + notification_type_registry, |
| 23 | +) |
17 | 24 | from baserow.core.user.handler import UserHandler |
18 | 25 |
|
19 | 26 | from .utils import custom_notification_types_registered |
@@ -813,3 +820,87 @@ def test_email_notifications_are_not_sent_if_already_cleared_by_user( |
813 | 820 | assert res.remaining_users_to_notify_count == 0 |
814 | 821 |
|
815 | 822 | mock_get_mail_connection.assert_not_called() |
| 823 | + |
| 824 | + |
| 825 | +@pytest.mark.django_db(transaction=True) |
| 826 | +@patch("baserow.core.notifications.handler.get_mail_connection") |
| 827 | +def test_email_notifications_queries_are_limited( |
| 828 | + mock_get_mail_connection, |
| 829 | + data_fixture, |
| 830 | + mutable_notification_type_registry, |
| 831 | +): |
| 832 | + """ |
| 833 | + The query count for sending notification emails must not scale with the |
| 834 | + number of notifications or the number of users. In particular the |
| 835 | + prefetch must include select_related("sender") so that accessing |
| 836 | + notification.sender during email rendering doesn't cause one query per |
| 837 | + notification. |
| 838 | + """ |
| 839 | + |
| 840 | + mock_connection = MagicMock() |
| 841 | + mock_get_mail_connection.return_value = mock_connection |
| 842 | + |
| 843 | + class SenderAccessingNotification(EmailNotificationTypeMixin, NotificationType): |
| 844 | + type = "test_sender_accessing_notification" |
| 845 | + |
| 846 | + @classmethod |
| 847 | + def get_notification_title_for_email(cls, notification, context): |
| 848 | + return f"Notification from {notification.sender.first_name}" |
| 849 | + |
| 850 | + @classmethod |
| 851 | + def get_notification_description_for_email(cls, notification, context): |
| 852 | + return None |
| 853 | + |
| 854 | + notification_type_registry.register(SenderAccessingNotification()) |
| 855 | + try: |
| 856 | + sender = data_fixture.create_user(first_name="Sender") |
| 857 | + |
| 858 | + # --- Warm-up run to prime internal caches (license cache, etc.) --- |
| 859 | + warmup_user = data_fixture.create_user() |
| 860 | + data_fixture.create_notification_for_users( |
| 861 | + recipients=[warmup_user], |
| 862 | + sender=sender, |
| 863 | + notification_type=SenderAccessingNotification.type, |
| 864 | + ) |
| 865 | + NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters( |
| 866 | + Q(pk=warmup_user.pk) |
| 867 | + ) |
| 868 | + mock_get_mail_connection.reset_mock() |
| 869 | + mock_connection.reset_mock() |
| 870 | + |
| 871 | + # --- Run 1: 1 user, 1 notification --- |
| 872 | + user_a = data_fixture.create_user() |
| 873 | + data_fixture.create_notification_for_users( |
| 874 | + recipients=[user_a], |
| 875 | + sender=sender, |
| 876 | + notification_type=SenderAccessingNotification.type, |
| 877 | + ) |
| 878 | + |
| 879 | + with CaptureQueriesContext(connection) as ctx_small: |
| 880 | + NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters( |
| 881 | + Q(pk=user_a.pk) |
| 882 | + ) |
| 883 | + assert len(ctx_small) # sanity |
| 884 | + |
| 885 | + # --- Run 2: 2 users, 5 notifications each --- |
| 886 | + mock_get_mail_connection.reset_mock() |
| 887 | + mock_connection.reset_mock() |
| 888 | + |
| 889 | + user_b = data_fixture.create_user() |
| 890 | + user_c = data_fixture.create_user() |
| 891 | + for _ in range(5): |
| 892 | + data_fixture.create_notification_for_users( |
| 893 | + recipients=[user_b, user_c], |
| 894 | + sender=sender, |
| 895 | + notification_type=SenderAccessingNotification.type, |
| 896 | + ) |
| 897 | + |
| 898 | + with CaptureQueriesContext(connection) as ctx_large: |
| 899 | + NotificationHandler.send_unread_notifications_by_email_to_users_matching_filters( |
| 900 | + Q(pk__in=[user_b.pk, user_c.pk]) |
| 901 | + ) |
| 902 | + |
| 903 | + # Query count must be identical: no N+1 on sender, users, or notifications. |
| 904 | + assert len(ctx_large) == len(ctx_small) |
| 905 | + finally: |
| 906 | + notification_type_registry.unregister(SenderAccessingNotification.type) |
0 commit comments