wbcrm 2.2.1__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of wbcrm might be problematic. Click here for more details.

Files changed (155) hide show
  1. wbcrm/__init__.py +1 -0
  2. wbcrm/admin/__init__.py +4 -0
  3. wbcrm/admin/accounts.py +59 -0
  4. wbcrm/admin/activities.py +101 -0
  5. wbcrm/admin/groups.py +7 -0
  6. wbcrm/admin/products.py +8 -0
  7. wbcrm/apps.py +5 -0
  8. wbcrm/configurations/__init__.py +1 -0
  9. wbcrm/configurations/base.py +16 -0
  10. wbcrm/dynamic_preferences_registry.py +38 -0
  11. wbcrm/factories/__init__.py +14 -0
  12. wbcrm/factories/accounts.py +56 -0
  13. wbcrm/factories/activities.py +125 -0
  14. wbcrm/factories/groups.py +23 -0
  15. wbcrm/factories/products.py +10 -0
  16. wbcrm/filters/__init__.py +10 -0
  17. wbcrm/filters/accounts.py +67 -0
  18. wbcrm/filters/activities.py +181 -0
  19. wbcrm/filters/groups.py +20 -0
  20. wbcrm/filters/products.py +37 -0
  21. wbcrm/filters/signals.py +94 -0
  22. wbcrm/migrations/0001_initial_squashed_squashed_0032_productcompanyrelationship_alter_product_prospects_and_more.py +3948 -0
  23. wbcrm/migrations/0002_alter_activity_repeat_choice.py +32 -0
  24. wbcrm/migrations/0003_remove_activity_external_id_and_more.py +63 -0
  25. wbcrm/migrations/0004_alter_activity_status.py +28 -0
  26. wbcrm/migrations/0005_account_accountrole_accountroletype_and_more.py +182 -0
  27. wbcrm/migrations/0006_alter_activity_location.py +17 -0
  28. wbcrm/migrations/0007_alter_account_status.py +23 -0
  29. wbcrm/migrations/0008_alter_activity_options.py +16 -0
  30. wbcrm/migrations/0009_alter_account_is_public.py +19 -0
  31. wbcrm/migrations/0010_alter_account_reference_id.py +17 -0
  32. wbcrm/migrations/0011_activity_summary.py +22 -0
  33. wbcrm/migrations/0012_alter_activity_summary.py +17 -0
  34. wbcrm/migrations/0013_account_action_plan_account_relationship_status_and_more.py +34 -0
  35. wbcrm/migrations/0014_alter_account_relationship_status.py +24 -0
  36. wbcrm/migrations/0015_alter_activity_type.py +23 -0
  37. wbcrm/migrations/0016_auto_20241205_1015.py +106 -0
  38. wbcrm/migrations/__init__.py +0 -0
  39. wbcrm/models/__init__.py +4 -0
  40. wbcrm/models/accounts.py +637 -0
  41. wbcrm/models/activities.py +1335 -0
  42. wbcrm/models/groups.py +118 -0
  43. wbcrm/models/products.py +83 -0
  44. wbcrm/models/recurrence.py +279 -0
  45. wbcrm/preferences.py +14 -0
  46. wbcrm/serializers/__init__.py +23 -0
  47. wbcrm/serializers/accounts.py +126 -0
  48. wbcrm/serializers/activities.py +526 -0
  49. wbcrm/serializers/groups.py +30 -0
  50. wbcrm/serializers/products.py +57 -0
  51. wbcrm/serializers/recurrence.py +90 -0
  52. wbcrm/serializers/signals.py +70 -0
  53. wbcrm/synchronization/__init__.py +0 -0
  54. wbcrm/synchronization/activity/__init__.py +0 -0
  55. wbcrm/synchronization/activity/admin.py +72 -0
  56. wbcrm/synchronization/activity/backend.py +207 -0
  57. wbcrm/synchronization/activity/backends/__init__.py +0 -0
  58. wbcrm/synchronization/activity/backends/google/__init__.py +2 -0
  59. wbcrm/synchronization/activity/backends/google/google_calendar_backend.py +399 -0
  60. wbcrm/synchronization/activity/backends/google/request_utils/__init__.py +16 -0
  61. wbcrm/synchronization/activity/backends/google/tasks.py +21 -0
  62. wbcrm/synchronization/activity/backends/google/tests/__init__.py +0 -0
  63. wbcrm/synchronization/activity/backends/google/tests/conftest.py +1 -0
  64. wbcrm/synchronization/activity/backends/google/tests/test_data.py +81 -0
  65. wbcrm/synchronization/activity/backends/google/tests/test_google_backend.py +319 -0
  66. wbcrm/synchronization/activity/backends/google/tests/test_utils.py +274 -0
  67. wbcrm/synchronization/activity/backends/google/typing_informations.py +139 -0
  68. wbcrm/synchronization/activity/backends/google/utils.py +216 -0
  69. wbcrm/synchronization/activity/backends/outlook/__init__.py +0 -0
  70. wbcrm/synchronization/activity/backends/outlook/backend.py +576 -0
  71. wbcrm/synchronization/activity/backends/outlook/msgraph.py +438 -0
  72. wbcrm/synchronization/activity/backends/outlook/parser.py +423 -0
  73. wbcrm/synchronization/activity/backends/outlook/tests/__init__.py +0 -0
  74. wbcrm/synchronization/activity/backends/outlook/tests/conftest.py +1 -0
  75. wbcrm/synchronization/activity/backends/outlook/tests/fixtures.py +606 -0
  76. wbcrm/synchronization/activity/backends/outlook/tests/test_admin.py +117 -0
  77. wbcrm/synchronization/activity/backends/outlook/tests/test_backend.py +269 -0
  78. wbcrm/synchronization/activity/backends/outlook/tests/test_controller.py +237 -0
  79. wbcrm/synchronization/activity/backends/outlook/tests/test_parser.py +173 -0
  80. wbcrm/synchronization/activity/controller.py +545 -0
  81. wbcrm/synchronization/activity/dynamic_preferences_registry.py +107 -0
  82. wbcrm/synchronization/activity/preferences.py +21 -0
  83. wbcrm/synchronization/activity/shortcuts.py +9 -0
  84. wbcrm/synchronization/activity/signals.py +28 -0
  85. wbcrm/synchronization/activity/tasks.py +21 -0
  86. wbcrm/synchronization/activity/urls.py +6 -0
  87. wbcrm/synchronization/activity/utils.py +46 -0
  88. wbcrm/synchronization/activity/views.py +37 -0
  89. wbcrm/synchronization/admin.py +1 -0
  90. wbcrm/synchronization/apps.py +15 -0
  91. wbcrm/synchronization/dynamic_preferences_registry.py +1 -0
  92. wbcrm/synchronization/management.py +36 -0
  93. wbcrm/synchronization/tasks.py +1 -0
  94. wbcrm/synchronization/urls.py +5 -0
  95. wbcrm/tasks.py +312 -0
  96. wbcrm/tests/__init__.py +0 -0
  97. wbcrm/tests/accounts/__init__.py +0 -0
  98. wbcrm/tests/accounts/test_models.py +380 -0
  99. wbcrm/tests/accounts/test_viewsets.py +87 -0
  100. wbcrm/tests/conftest.py +76 -0
  101. wbcrm/tests/disable_signals.py +52 -0
  102. wbcrm/tests/e2e/__init__.py +1 -0
  103. wbcrm/tests/e2e/e2e_wbcrm_utility.py +82 -0
  104. wbcrm/tests/e2e/test_e2e.py +369 -0
  105. wbcrm/tests/test_assignee_methods.py +39 -0
  106. wbcrm/tests/test_chartviewsets.py +111 -0
  107. wbcrm/tests/test_dto.py +63 -0
  108. wbcrm/tests/test_filters.py +51 -0
  109. wbcrm/tests/test_models.py +216 -0
  110. wbcrm/tests/test_recurrence.py +291 -0
  111. wbcrm/tests/test_report.py +20 -0
  112. wbcrm/tests/test_serializers.py +170 -0
  113. wbcrm/tests/test_tasks.py +94 -0
  114. wbcrm/tests/test_viewsets.py +967 -0
  115. wbcrm/tests/tests.py +120 -0
  116. wbcrm/typings.py +107 -0
  117. wbcrm/urls.py +67 -0
  118. wbcrm/viewsets/__init__.py +22 -0
  119. wbcrm/viewsets/accounts.py +121 -0
  120. wbcrm/viewsets/activities.py +315 -0
  121. wbcrm/viewsets/buttons/__init__.py +7 -0
  122. wbcrm/viewsets/buttons/accounts.py +27 -0
  123. wbcrm/viewsets/buttons/activities.py +68 -0
  124. wbcrm/viewsets/buttons/signals.py +17 -0
  125. wbcrm/viewsets/display/__init__.py +12 -0
  126. wbcrm/viewsets/display/accounts.py +110 -0
  127. wbcrm/viewsets/display/activities.py +443 -0
  128. wbcrm/viewsets/display/groups.py +22 -0
  129. wbcrm/viewsets/display/products.py +105 -0
  130. wbcrm/viewsets/endpoints/__init__.py +8 -0
  131. wbcrm/viewsets/endpoints/accounts.py +32 -0
  132. wbcrm/viewsets/endpoints/activities.py +30 -0
  133. wbcrm/viewsets/endpoints/groups.py +7 -0
  134. wbcrm/viewsets/endpoints/products.py +9 -0
  135. wbcrm/viewsets/groups.py +37 -0
  136. wbcrm/viewsets/menu/__init__.py +8 -0
  137. wbcrm/viewsets/menu/accounts.py +18 -0
  138. wbcrm/viewsets/menu/activities.py +61 -0
  139. wbcrm/viewsets/menu/groups.py +16 -0
  140. wbcrm/viewsets/menu/products.py +20 -0
  141. wbcrm/viewsets/mixins.py +34 -0
  142. wbcrm/viewsets/previews/__init__.py +1 -0
  143. wbcrm/viewsets/previews/activities.py +10 -0
  144. wbcrm/viewsets/products.py +56 -0
  145. wbcrm/viewsets/recurrence.py +26 -0
  146. wbcrm/viewsets/titles/__init__.py +13 -0
  147. wbcrm/viewsets/titles/accounts.py +22 -0
  148. wbcrm/viewsets/titles/activities.py +61 -0
  149. wbcrm/viewsets/titles/products.py +13 -0
  150. wbcrm/viewsets/titles/utils.py +46 -0
  151. wbcrm/workflows/__init__.py +1 -0
  152. wbcrm/workflows/assignee_methods.py +25 -0
  153. wbcrm-2.2.1.dist-info/METADATA +11 -0
  154. wbcrm-2.2.1.dist-info/RECORD +155 -0
  155. wbcrm-2.2.1.dist-info/WHEEL +5 -0
@@ -0,0 +1,117 @@
1
+ from unittest.mock import patch
2
+
3
+ import pytest
4
+ from django.contrib.admin import AdminSite
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.messages import get_messages
7
+ from dynamic_preferences.registries import global_preferences_registry
8
+ from rest_framework import status
9
+ from wbcore.contrib.authentication.factories import UserFactory
10
+ from wbcrm.synchronization.activity.admin import UserSyncAdmin
11
+
12
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ "backend, credentials, subscription, tenant_id",
17
+ [
18
+ (False, True, {"id": "fake_id"}, "fake_tenant_id"),
19
+ (True, False, None, None),
20
+ (True, True, None, None),
21
+ (True, True, {"id": "fake_id"}, None),
22
+ (True, True, {"id": "fake_id"}, "fake_tenant_id"),
23
+ ],
24
+ )
25
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
26
+ @pytest.mark.django_db
27
+ class TestAdminUserWebhook(TestOutlookSyncFixture):
28
+ def _initialiation(self, mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture):
29
+ preferences = global_preferences_registry.manager()
30
+ preferences["wbactivity_sync__sync_backend_calendar"] = preferences[
31
+ "wbactivity_sync__outlook_sync_credentials"
32
+ ] = ""
33
+ if backend:
34
+ preferences[
35
+ "wbactivity_sync__sync_backend_calendar"
36
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
37
+ mock_msgraph.return_value.status_code = status.HTTP_200_OK
38
+ mock_msgraph.return_value = msgraph_fixture()
39
+ if credentials:
40
+ preferences["wbactivity_sync__outlook_sync_credentials"] = credentials_fixture
41
+ msgraph_fixture._subscription = subscription
42
+ msgraph_fixture.tenant_id = tenant_id
43
+ else:
44
+ mock_msgraph.side_effect = AssertionError("Invalid URL")
45
+ user = UserFactory(is_superuser=True)
46
+ if subscription:
47
+ user.metadata = {"outlook": {"subscription": subscription}}
48
+ user.save()
49
+ return user
50
+
51
+ def test_set_web_hook(
52
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
53
+ ):
54
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
55
+ fixture_request.user = user
56
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
57
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
58
+ user_admin.set_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
59
+ messages = [m.message for m in get_messages(fixture_request)]
60
+ assert len(messages) == 1
61
+ if backend:
62
+ if credentials:
63
+ if subscription or tenant_id:
64
+ assert "Operation completed" in messages[0]
65
+ else:
66
+ assert messages[0] == f"Operation Failed, Outlook TenantId not found for: {user}"
67
+ else:
68
+ assert "Operation Failed, Invalid URL" in messages[0]
69
+ else:
70
+ assert messages[0] == "Operation Failed, No backend set in preferences"
71
+
72
+ def test_stop_web_hook(
73
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
74
+ ):
75
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
76
+ fixture_request.user = user
77
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
78
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
79
+ user_admin.stop_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
80
+ messages = [m.message for m in get_messages(fixture_request)]
81
+ assert len(messages) == 1
82
+ if backend:
83
+ if subscription:
84
+ if credentials:
85
+ assert "Operation completed" in messages[0]
86
+ else:
87
+ assert "Operation Failed, Invalid URL" in messages[0]
88
+ else:
89
+ assert f"Operation Failed, {user} has no active webhook"
90
+ else:
91
+ assert messages[0] == "Operation Failed, No backend set in preferences"
92
+
93
+ def test_check_web_hook(
94
+ self, mock_msgraph, backend, credentials, subscription, tenant_id, fixture_request, credentials_fixture
95
+ ):
96
+ user = self._initialiation(mock_msgraph, backend, credentials, subscription, tenant_id, credentials_fixture)
97
+ fixture_request.user = user
98
+ assert len([m.message for m in get_messages(fixture_request)]) == 0
99
+ user_admin = UserSyncAdmin(UserFactory, AdminSite())
100
+ user_admin.check_web_hook(fixture_request, get_user_model().objects.filter(id=user.id))
101
+ messages = [m.message for m in get_messages(fixture_request)]
102
+ assert len(messages) == 1
103
+ if backend:
104
+ if credentials:
105
+ if subscription:
106
+ assert "Operation completed" in messages[0]
107
+ elif tenant_id:
108
+ assert (
109
+ "Operation Failed, Webhook not found. Number of subscriptions found in outlook for"
110
+ in messages[0]
111
+ )
112
+ else:
113
+ assert messages[0] == f"Operation Failed, Webhook not found. TenantId not found for {user}"
114
+ else:
115
+ assert "Operation Failed, Invalid URL" in messages[0]
116
+ else:
117
+ assert messages[0] == "Operation Failed, No backend set in preferences"
@@ -0,0 +1,269 @@
1
+ import json
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+ from dynamic_preferences.registries import global_preferences_registry
6
+ from rest_framework.test import APIRequestFactory
7
+ from wbcore.contrib.authentication.factories import UserFactory
8
+ from wbcrm.models.activities import Activity
9
+ from wbcrm.synchronization.activity.backends.outlook.backend import OutlookSyncBackend
10
+
11
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
12
+
13
+
14
+ @pytest.mark.django_db
15
+ class TestOutlookSyncBackend(TestOutlookSyncFixture):
16
+ backend = OutlookSyncBackend()
17
+
18
+ def test_attribute(self):
19
+ assert OutlookSyncBackend.METADATA_KEY == "outlook"
20
+
21
+ def test_open(self):
22
+ preferences = global_preferences_registry.manager()
23
+ preferences["wbactivity_sync__outlook_sync_credentials"] = ""
24
+ assert hasattr(self.backend, "msgraph") is False
25
+ self.backend.open()
26
+ assert self.backend.msgraph
27
+
28
+ @pytest.mark.parametrize("type_request", [None, "validationToken", "admin_consent"])
29
+ def test_validation_response(self, type_request):
30
+ request1 = APIRequestFactory().get("")
31
+ if type_request:
32
+ request1.GET = request1.GET.copy()
33
+ request1.GET[type_request] = "fake_info"
34
+ assert self.backend._validation_response(request1).content.decode("UTF-8") == "fake_info"
35
+ else:
36
+ assert self.backend._validation_response(request1) is None
37
+
38
+ @pytest.mark.parametrize("client_state", [False, True])
39
+ def test_is_inbound_request_valid(
40
+ self, client_state, notification_created_fixture, notification_call_record_fixture
41
+ ):
42
+ api_factory = APIRequestFactory()
43
+ request1 = api_factory.post("", data={})
44
+ request2 = api_factory.post(
45
+ "", data=json.dumps({"value": [notification_call_record_fixture]}), content_type="application/json"
46
+ )
47
+ request3 = api_factory.post(
48
+ "", data=json.dumps({"value": [notification_created_fixture]}), content_type="application/json"
49
+ )
50
+ preferences = global_preferences_registry.manager()
51
+ preferences["wbactivity_sync__outlook_sync_client_state"] = ""
52
+ if client_state:
53
+ preferences["wbactivity_sync__outlook_sync_client_state"] = notification_created_fixture["client_state"]
54
+
55
+ assert self.backend._is_inbound_request_valid(request1) is False
56
+ assert self.backend._is_inbound_request_valid(request2) is False
57
+ assert self.backend._is_inbound_request_valid(request3) == client_state
58
+
59
+ @pytest.mark.parametrize("event_found, is_organizer", [(False, True), (True, True), (True, False)])
60
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
61
+ def test_get_events_from_request(
62
+ self,
63
+ mock_msgraph,
64
+ event_found,
65
+ is_organizer,
66
+ notification_created_fixture,
67
+ organizer_event_fixture_parsed,
68
+ invitation_event_fixture_parsed,
69
+ ):
70
+ api_factory = APIRequestFactory()
71
+ request1 = api_factory.post(
72
+ "", data=json.dumps({"value": [notification_created_fixture]}), content_type="application/json"
73
+ )
74
+ self.backend.open()
75
+ self.backend.msgraph = msgraph_fixture()
76
+ if event_found:
77
+ if is_organizer:
78
+ msgraph_fixture.event = organizer_event_fixture_parsed
79
+ msgraph_fixture.tenant_id = "fake_tenant_id"
80
+ else:
81
+ msgraph_fixture.event = invitation_event_fixture_parsed
82
+ msgraph_fixture.event_by_uid = organizer_event_fixture_parsed
83
+ events = self.backend._get_events_from_request(request1)
84
+ assert len(events) == 1
85
+ if event_found:
86
+ assert {"change_type", "resource", "subscription_id"}.issubset(set(events[0].keys()))
87
+ assert events[0].get("id")
88
+ else:
89
+ assert {"change_type", "resource", "subscription_id"} == set(events[0].keys())
90
+
91
+ def test_deserialize(self, organizer_event_fixture_parsed, organizer_master_event_fixture_parsed):
92
+ activity_dto0, is_deleted0, user_dto0 = self.backend._deserialize(
93
+ organizer_event_fixture_parsed, include_metadata=False
94
+ )
95
+ activity_dto, is_deleted, user_dto = self.backend._deserialize(organizer_event_fixture_parsed)
96
+ activity_dto2, is_deleted2, user_dto2 = self.backend._deserialize(organizer_master_event_fixture_parsed)
97
+
98
+ assert is_deleted0 == is_deleted == is_deleted2 is False
99
+ assert user_dto0 == user_dto == user_dto2 is None
100
+ assert activity_dto0.metadata == {}
101
+ assert activity_dto.metadata == {
102
+ self.backend.METADATA_KEY: {
103
+ "event_uid": organizer_event_fixture_parsed["uid"],
104
+ "event_id": organizer_event_fixture_parsed["id"],
105
+ }
106
+ }
107
+ assert activity_dto2.metadata == {
108
+ self.backend.METADATA_KEY: {
109
+ "event_uid": organizer_master_event_fixture_parsed["uid"],
110
+ "event_id": organizer_master_event_fixture_parsed["id"],
111
+ }
112
+ }
113
+ assert activity_dto.period
114
+ assert activity_dto.repeat_choice == "NEVER"
115
+ assert activity_dto2.repeat_choice != "NEVER"
116
+
117
+ def test_serialize(self, activity_factory):
118
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
119
+ act_dto = activity_factory(
120
+ repeat_choice=Activity.ReoccuranceChoice.DAILY, recurrence_count=3, preceded_by=None
121
+ )._build_dto()
122
+ activity_dict = self.backend._serialize(activity_dto)
123
+ activity_dict1 = self.backend._serialize(activity_dto, created=True)
124
+ act_dict = self.backend._serialize(act_dto)
125
+ act_dict1 = self.backend._serialize(act_dto, created=True)
126
+ keys = {
127
+ "subject",
128
+ "start",
129
+ "end",
130
+ "body",
131
+ "attendees",
132
+ "sensitivity",
133
+ "isReminderOn",
134
+ "reminderMinutesBeforeStart",
135
+ "isAllDay",
136
+ "responseRequested",
137
+ "location",
138
+ "locations",
139
+ }
140
+ assert set(activity_dict.keys()) == set(act_dict.keys()) == set(activity_dict1.keys()) == keys
141
+ assert set(act_dict1.keys()) == keys.union({"recurrence"})
142
+
143
+ @pytest.mark.parametrize(
144
+ "metadata, master_event",
145
+ [
146
+ ({}, False),
147
+ ({}, True),
148
+ ({"organizer_resource": "fake_resource"}, False),
149
+ ({"organizer_resource": "fake_resource"}, True),
150
+ ({"event_id": "fake_event_id"}, False),
151
+ ({"event_id": "fake_event_id"}, True),
152
+ ({"event_uid": "fake_event_uid"}, False),
153
+ ({"event_uid": "fake_event_uid"}, True),
154
+ ({"occurrence_resource": "fake_resource"}, False),
155
+ ({"occurrence_resource": "fake_resource"}, True),
156
+ ({"occurrence_id": "fake_occurrence_id"}, False),
157
+ ({"occurrence_id": "fake_occurrence_id"}, True),
158
+ ],
159
+ )
160
+ def test_get_external_event(self, metadata, master_event, activity_factory):
161
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
162
+ activity_dto2 = activity_factory(preceded_by=None, metadata={self.backend.METADATA_KEY: metadata})._build_dto()
163
+ self.backend.open()
164
+ self.backend.msgraph = msgraph_fixture()
165
+ msgraph_fixture.tenant_id = "fake_tenant_id"
166
+ msgraph_fixture.event_by_uid = msgraph_fixture.event = {"id": "event_id"}
167
+ assert (
168
+ self.backend.get_external_event(activity_dto, master_event)
169
+ == self.backend.get_external_event(activity_dto, master_event)
170
+ is None
171
+ )
172
+ if metadata:
173
+ event_result = self.backend.get_external_event(activity_dto2, master_event)
174
+ if master_event or not activity_dto2.is_recurrent:
175
+ if metadata.get("event_uid") or metadata.get("event_id") or metadata.get("organizer_resource"):
176
+ assert event_result == {"id": "event_id"}
177
+ else:
178
+ assert event_result is None
179
+ else:
180
+ if metadata.get("occurrence_resource") or metadata.get("occurrence_id"):
181
+ assert event_result == {"id": "event_id"}
182
+ else:
183
+ assert event_result is None
184
+ else:
185
+ assert self.backend.get_external_event(activity_dto2, master_event) is None
186
+
187
+ def test_get_external_participants(self, activity_factory, person_factory):
188
+ person = person_factory()
189
+ activity_dto = activity_factory(preceded_by=None, participants=(person,))._build_dto()
190
+ activity_dto2 = activity_factory(
191
+ preceded_by=None,
192
+ participants=(person,),
193
+ metadata={self.backend.METADATA_KEY: {"organizer_resource": "fake_resource"}},
194
+ )._build_dto()
195
+ self.backend.open()
196
+ self.backend.msgraph = msgraph_fixture()
197
+ msgraph_fixture.tenant_id = "fake_tenant_id"
198
+ msgraph_fixture.event_by_uid = msgraph_fixture.event = {
199
+ "id": "event_id",
200
+ "attendees": [{"emailAddress": {"address": person._build_dto().email}}],
201
+ }
202
+ assert len(self.backend.get_external_participants(activity_dto, [])) == 0
203
+ assert len(self.backend.get_external_participants(activity_dto2, [])) == 1
204
+ assert len(self.backend.get_external_participants(activity_dto2, activity_dto2.participants)) == 0
205
+
206
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
207
+ def test_is_participant_valid(self, mock_msgraph):
208
+ user = UserFactory(is_superuser=True)
209
+ user2 = UserFactory(is_superuser=True)
210
+ user2.metadata = {self.backend.METADATA_KEY: {"subscription": {"id": "fake_subscription_id"}}}
211
+ user2.save()
212
+ msgraph_fixture._subscription = {"id": "fake_subscription_id"}
213
+ mock_msgraph.return_value = msgraph_fixture()
214
+ assert self.backend._is_participant_valid(user2) is True
215
+ assert self.backend._is_participant_valid(user) is False
216
+
217
+ def test_generate_event_metadata(self):
218
+ event = {"id": "fake_id", "uid": "fake_uid"}
219
+ occurrence_event = {"id": "fake_id", "uid": "fake_uid"}
220
+ resource = f"Users/fake_tenant_id/Events/{event['id']}"
221
+ result = {
222
+ "resources": [resource],
223
+ "organizer_resource": resource,
224
+ "event_uid": event["uid"],
225
+ "event_id": event["id"],
226
+ }
227
+ metadata = self.backend._generate_event_metadata("fake_tenant_id", event)
228
+ metadata2 = self.backend._generate_event_metadata("fake_tenant_id", event, occurrence_event)
229
+ assert metadata == {self.backend.METADATA_KEY: result}
230
+ assert metadata2 == {
231
+ self.backend.METADATA_KEY: {**result, **{"occurrence_id": event["id"], "occurrence_resource": resource}}
232
+ }
233
+
234
+ def test_get_metadata_from_event(
235
+ self, activity_factory, organizer_event_fixture_parsed, organizer_master_event_fixture_parsed
236
+ ):
237
+ self.backend.open()
238
+ self.backend.msgraph = msgraph_fixture()
239
+ msgraph_fixture.tenant_id = "fake_tenant_id"
240
+ activity_dto = activity_factory(preceded_by=None)._build_dto()
241
+ metadata_list = self.backend._get_metadata_from_event(activity_dto, organizer_event_fixture_parsed)
242
+ # self.backend._get_metadata_from_event()
243
+ resource = f"Users/fake_tenant_id/Events/{organizer_event_fixture_parsed['id']}"
244
+ assert len(metadata_list) == 1
245
+ assert metadata_list[0][0] == activity_dto
246
+ assert metadata_list[0][1] == {
247
+ self.backend.METADATA_KEY: {
248
+ "resources": [resource],
249
+ "organizer_resource": resource,
250
+ "event_uid": organizer_event_fixture_parsed["uid"],
251
+ "event_id": organizer_event_fixture_parsed["id"],
252
+ }
253
+ }
254
+
255
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
256
+ def test_renew_web_hooks(self, mock_msgraph):
257
+ msgraph_fixture._subscription = {"id": "fake_id_new"}
258
+ mock_msgraph.return_value = msgraph_fixture()
259
+
260
+ user1 = UserFactory(is_superuser=True)
261
+ user2 = UserFactory(is_superuser=True)
262
+ user2.metadata = {self.backend.METADATA_KEY: {"subscription": {"id": "fake_id"}}}
263
+ user2.save()
264
+
265
+ self.backend.renew_web_hooks()
266
+ user1.refresh_from_db()
267
+ user2.refresh_from_db()
268
+ assert user1.metadata == {}
269
+ assert user2.metadata == {self.backend.METADATA_KEY: {"subscription": {"id": "fake_id_new"}}}
@@ -0,0 +1,237 @@
1
+ import itertools
2
+ import json
3
+ from unittest.mock import patch
4
+
5
+ import pytest
6
+ from django.contrib.auth import get_user_model
7
+ from django.contrib.auth.models import Permission
8
+ from django.contrib.contenttypes.models import ContentType
9
+ from dynamic_preferences.registries import global_preferences_registry
10
+ from rest_framework.test import APIRequestFactory
11
+ from wbcore.permissions.registry import user_registry
12
+ from wbcrm.factories import ActivityFactory
13
+ from wbcrm.models import Activity, ActivityParticipant
14
+ from wbcrm.synchronization.activity.controller import ActivityController
15
+ from wbcrm.typings import User as UserDTO
16
+
17
+ from .fixtures import TestOutlookSyncFixture, msgraph_fixture
18
+
19
+ User = get_user_model()
20
+
21
+
22
+ @pytest.mark.django_db
23
+ class TestInitController:
24
+ def test_init(self):
25
+ global_preferences_registry.manager()["wbactivity_sync__sync_backend_calendar"] = ""
26
+ controller = ActivityController()
27
+ assert controller.backend is None
28
+
29
+ global_preferences_registry.manager()[
30
+ "wbactivity_sync__sync_backend_calendar"
31
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
32
+ controller2 = ActivityController()
33
+ assert controller2.backend
34
+
35
+
36
+ @pytest.mark.django_db
37
+ class TestController(TestOutlookSyncFixture):
38
+ def setup_method(self):
39
+ global_preferences_registry.manager()[
40
+ "wbactivity_sync__sync_backend_calendar"
41
+ ] = "wbcrm.synchronization.activity.backends.outlook.backend.OutlookSyncBackend"
42
+
43
+ @pytest.fixture()
44
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
45
+ def controller(self, mock_msgraph):
46
+ controller = ActivityController()
47
+ controller.backend.open()
48
+ controller.backend.msgraph = msgraph_fixture()
49
+ return controller
50
+
51
+ def test_init(self):
52
+ controller = ActivityController()
53
+ assert controller.backend
54
+
55
+ @pytest.mark.parametrize("type_request", [None, "validationToken", "admin_consent"])
56
+ def test_handle_inbound_validation_response(self, controller, type_request):
57
+ request1 = APIRequestFactory().get("")
58
+ if type_request:
59
+ request1.GET = request1.GET.copy()
60
+ request1.GET[type_request] = "fake_info"
61
+ assert controller.handle_inbound_validation_response(request1).content.decode("UTF-8") == "fake_info"
62
+ else:
63
+ assert controller.handle_inbound_validation_response(request1) is None
64
+
65
+ @pytest.mark.parametrize("client_state", ["secret1", "secret2"])
66
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
67
+ def test_get_events_from_inbound_request(
68
+ self, mock_msgraph, controller, client_state, notification_fixture, teams_event_fixture
69
+ ):
70
+ notification_fixture["client_state"] = client_state
71
+ global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"] = "secret2"
72
+ api_factory = APIRequestFactory()
73
+ request1 = api_factory.post("", data={})
74
+ request2 = api_factory.post(
75
+ "", data=json.dumps({"value": [notification_fixture]}), content_type="application/json"
76
+ )
77
+
78
+ mock_msgraph.return_value = controller.backend.msgraph
79
+ controller.backend.msgraph.event = teams_event_fixture
80
+ controller.backend.msgraph.event_by_uid = teams_event_fixture
81
+ events1 = controller.get_events_from_inbound_request(request1)
82
+ events2 = controller.get_events_from_inbound_request(request2)
83
+ assert events1 == []
84
+ if client_state != global_preferences_registry.manager()["wbactivity_sync__outlook_sync_client_state"]:
85
+ assert events1 == events2 == []
86
+ else:
87
+ assert events1 == []
88
+ assert len(events2) == 1
89
+ expected_result = {
90
+ "change_type": notification_fixture["change_type"],
91
+ "resource": notification_fixture["resource"],
92
+ "subscription_id": notification_fixture["subscription_id"],
93
+ "organizer_resource": notification_fixture["resource"],
94
+ **teams_event_fixture,
95
+ }
96
+ assert set(events2[0].keys()) == set(expected_result.keys())
97
+
98
+ @patch("wbcrm.synchronization.activity.backends.outlook.backend.MicrosoftGraphAPI")
99
+ def test_user_for_handle_inbound(
100
+ self, mock_msgraph, controller, notification_fixture, teams_event_fixture, user_factory
101
+ ):
102
+ event = {
103
+ "change_type": notification_fixture["change_type"],
104
+ "resource": notification_fixture["resource"],
105
+ "subscription_id": notification_fixture["subscription_id"],
106
+ **teams_event_fixture,
107
+ }
108
+ metadata = {"outlook": {"subscription": {"id": notification_fixture["subscription_id"]}}}
109
+ _, _, user_dto = controller.backend._deserialize(event)
110
+ user_result = UserDTO(metadata=metadata, id=None)
111
+ assert user_dto.id is None
112
+ assert user_dto.metadata == user_result.metadata
113
+
114
+ assert controller.get_activity_participant(user_dto) is None
115
+ user = user_factory(is_active=True, is_superuser=True, metadata=metadata)
116
+ assert controller.get_activity_participant(user_dto) == user.profile
117
+
118
+ def _get_user_and_activities(self, notification_fixture, user_factory, teams_event_fixture):
119
+ user1 = user_factory(is_active=True)
120
+ user2 = user_factory(
121
+ is_active=True, metadata={"outlook": {"subscription": {"id": notification_fixture["subscription_id"]}}}
122
+ )
123
+
124
+ other1_activity = ActivityFactory(creator=user1.profile, participants=(user2.profile,))
125
+ other2_activity = ActivityFactory(
126
+ creator=user1.profile,
127
+ participants=(user2.profile,),
128
+ metadata={"outlook": {"event_id": 1, "event_uid": 2, "resources": ["Users/1/events/1"]}},
129
+ )
130
+ metadata = {
131
+ "outlook": {
132
+ "resources": [notification_fixture["resource"]],
133
+ "organizer_resource": notification_fixture["resource"],
134
+ "event_uid": teams_event_fixture["uid"],
135
+ "event_id": teams_event_fixture["id"],
136
+ }
137
+ }
138
+ activity = ActivityFactory(creator=user1.profile, participants=(user2.profile,))
139
+ Activity.objects.filter(id=activity.id).update(metadata=metadata)
140
+ return {"activities": (other1_activity, other2_activity, activity), "users": (user1, user2)}
141
+
142
+ @pytest.mark.parametrize("is_internal_creator, cancel_activity", list(itertools.product([True, False], repeat=2)))
143
+ def test_delete_activity(
144
+ self, cancel_activity, is_internal_creator, controller, notification_fixture, user_factory, teams_event_fixture
145
+ ):
146
+ global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"] = cancel_activity
147
+
148
+ data = self._get_user_and_activities(notification_fixture, user_factory, teams_event_fixture)
149
+ other1_activity, other2_activity, activity = data["activities"]
150
+ user1, user2 = data["users"]
151
+ if is_internal_creator:
152
+ permission = Permission.objects.get_or_create(
153
+ content_type=ContentType.objects.get_for_model(User), codename="is_internal_user"
154
+ )[0]
155
+ user1.user_permissions.add(permission)
156
+ user_registry.reset_cache()
157
+ activity.refresh_from_db()
158
+ activity_dto = activity._build_dto()
159
+
160
+ user1_dto = UserDTO(metadata=user1.metadata, id=None)
161
+ user2_dto = UserDTO(metadata=user2.metadata, id=None)
162
+
163
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
164
+
165
+ controller.delete_activity(activity_dto, user1_dto)
166
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
167
+
168
+ controller.delete_activity(
169
+ activity_dto, user2_dto
170
+ ) # user 2 is not the creator of the activity. it's a participant
171
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
172
+ if is_internal_creator:
173
+ assert (
174
+ activity.activity_participants.get(participant=user2.profile).participation_status
175
+ == ActivityParticipant.ParticipationStatus.CANCELLED
176
+ )
177
+
178
+ controller.delete_activity(
179
+ activity_dto, user1_dto
180
+ ) # user 1 is the creator of the activity but doesn't have an active subscription
181
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
182
+ user1.metadata = user2.metadata
183
+ user1.save()
184
+ user2.metadata = {}
185
+ user2.metadata = {"outlook": {"subscription": {"id": "new_subscription1"}}}
186
+ user2.save()
187
+ user1.refresh_from_db()
188
+ user1_dto = UserDTO(metadata=user1.metadata, id=None)
189
+
190
+ controller.delete_activity(
191
+ activity_dto, user1_dto
192
+ ) # user 1 is the creator of the activity with an active subscription
193
+ if is_internal_creator and not cancel_activity:
194
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity}
195
+ else:
196
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
197
+ assert set(Activity.all_objects.all()) == {other1_activity, other2_activity, activity}
198
+
199
+ @pytest.mark.parametrize(
200
+ "cancel_external_creator_activity, cancel_activity", list(itertools.product([True, False], repeat=2))
201
+ )
202
+ def test_cancel_no_participant_external_creator_activity(
203
+ self,
204
+ cancel_external_creator_activity,
205
+ cancel_activity,
206
+ controller,
207
+ notification_fixture,
208
+ user_factory,
209
+ teams_event_fixture,
210
+ ):
211
+ global_preferences_registry.manager()["wbactivity_sync__sync_cancelled_activity"] = cancel_activity
212
+ global_preferences_registry.manager()[
213
+ "wbactivity_sync__sync_cancelled_external_activity"
214
+ ] = cancel_external_creator_activity
215
+
216
+ data = self._get_user_and_activities(notification_fixture, user_factory, teams_event_fixture)
217
+ other1_activity, other2_activity, activity = data["activities"]
218
+ user1, user2 = data["users"]
219
+ permission = Permission.objects.get_or_create(
220
+ content_type=ContentType.objects.get_for_model(User), codename="is_internal_user"
221
+ )[0]
222
+ user2.user_permissions.add(permission)
223
+ user_registry.reset_cache()
224
+ user2_dto = UserDTO(metadata=user2.metadata, id=None)
225
+ Activity.objects.filter(id=activity.id).update(creator=user1.profile)
226
+ activity.refresh_from_db()
227
+ activity_dto = activity._build_dto()
228
+
229
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}
230
+
231
+ # user 1 is an external user and it's the creator of the activity without an active subscription
232
+ # user 2 is a internal participant
233
+ controller.delete_activity(activity_dto, user2_dto)
234
+ if cancel_external_creator_activity and not cancel_activity:
235
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity}
236
+ else:
237
+ assert set(Activity.objects.all()) == {other1_activity, other2_activity, activity}