django-webhook-subscriber 0.4.0__py3-none-any.whl → 2.0.0__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.
- django_webhook_subscriber/__init__.py +7 -1
- django_webhook_subscriber/admin.py +831 -182
- django_webhook_subscriber/apps.py +3 -20
- django_webhook_subscriber/conf.py +11 -24
- django_webhook_subscriber/delivery.py +414 -159
- django_webhook_subscriber/http.py +51 -0
- django_webhook_subscriber/management/commands/webhook.py +169 -0
- django_webhook_subscriber/management/commands/webhook_cache.py +173 -0
- django_webhook_subscriber/management/commands/webhook_logs.py +226 -0
- django_webhook_subscriber/management/commands/webhook_performance_test.py +469 -0
- django_webhook_subscriber/management/commands/webhook_send.py +96 -0
- django_webhook_subscriber/management/commands/webhook_status.py +139 -0
- django_webhook_subscriber/managers.py +36 -14
- django_webhook_subscriber/migrations/0002_remove_webhookregistry_content_type_and_more.py +192 -0
- django_webhook_subscriber/models.py +291 -114
- django_webhook_subscriber/serializers.py +16 -50
- django_webhook_subscriber/tasks.py +434 -56
- django_webhook_subscriber/tests/factories.py +40 -0
- django_webhook_subscriber/tests/settings.py +27 -8
- django_webhook_subscriber/tests/test_delivery.py +453 -190
- django_webhook_subscriber/tests/test_http.py +32 -0
- django_webhook_subscriber/tests/test_managers.py +26 -37
- django_webhook_subscriber/tests/test_models.py +341 -251
- django_webhook_subscriber/tests/test_serializers.py +22 -56
- django_webhook_subscriber/tests/test_tasks.py +477 -189
- django_webhook_subscriber/tests/test_utils.py +98 -94
- django_webhook_subscriber/utils.py +87 -69
- django_webhook_subscriber/validators.py +53 -0
- django_webhook_subscriber-2.0.0.dist-info/METADATA +774 -0
- django_webhook_subscriber-2.0.0.dist-info/RECORD +38 -0
- django_webhook_subscriber/management/commands/check_webhook_tasks.py +0 -113
- django_webhook_subscriber/management/commands/clean_webhook_logs.py +0 -65
- django_webhook_subscriber/management/commands/test_webhook.py +0 -96
- django_webhook_subscriber/signals.py +0 -152
- django_webhook_subscriber/testing.py +0 -14
- django_webhook_subscriber/tests/test_signals.py +0 -268
- django_webhook_subscriber-0.4.0.dist-info/METADATA +0 -448
- django_webhook_subscriber-0.4.0.dist-info/RECORD +0 -33
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/WHEEL +0 -0
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/top_level.txt +0 -0
|
@@ -1,197 +1,460 @@
|
|
|
1
|
-
from unittest.mock import
|
|
1
|
+
from unittest.mock import Mock, patch
|
|
2
2
|
|
|
3
|
-
from django.test import TestCase, override_settings
|
|
4
3
|
from django.contrib.contenttypes.models import ContentType
|
|
5
|
-
from django.
|
|
4
|
+
from django.core.cache import cache
|
|
5
|
+
from django.test import TestCase, override_settings
|
|
6
6
|
|
|
7
|
-
from django_webhook_subscriber
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
process_and_deliver_webhook,
|
|
14
|
-
prepare_headers,
|
|
15
|
-
deliver_webhook,
|
|
7
|
+
from django_webhook_subscriber import delivery, models, utils
|
|
8
|
+
|
|
9
|
+
from .factories import (
|
|
10
|
+
WebhookDeliveryLogFactory,
|
|
11
|
+
WebhookSubscriberFactory,
|
|
12
|
+
WebhookSubscriptionFactory,
|
|
16
13
|
)
|
|
17
14
|
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
class WebhookDeliveryProcessorTests(TestCase):
|
|
17
|
+
def setUp(self):
|
|
18
|
+
self.processor = delivery.WebhookDeliveryProcessor()
|
|
19
|
+
# Clear cache before each test
|
|
20
|
+
delivery.clear_webhook_cache()
|
|
21
|
+
|
|
22
|
+
# Create test content type and model instance
|
|
23
|
+
self.content_type = ContentType.objects.get_for_model(
|
|
24
|
+
models.WebhookDeliveryLog
|
|
25
|
+
)
|
|
26
|
+
self.instance = WebhookDeliveryLogFactory()
|
|
27
|
+
self.subscriber = WebhookSubscriberFactory(
|
|
28
|
+
content_type=self.content_type
|
|
29
|
+
)
|
|
30
|
+
self.subscription = WebhookSubscriptionFactory(
|
|
31
|
+
subscriber=self.subscriber, event_name="created"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def tearDown(self):
|
|
35
|
+
cache.clear()
|
|
36
|
+
delivery.clear_webhook_cache()
|
|
37
|
+
|
|
38
|
+
def test_delivery_process_on_initialization(self):
|
|
39
|
+
self.assertEqual(self.processor.cache_ttl, 300)
|
|
40
|
+
with override_settings(WEBHOOK_SUBSCRIBER={"WEBHOOK_CACHE_TTL": 600}):
|
|
41
|
+
processor = delivery.WebhookDeliveryProcessor()
|
|
42
|
+
self.assertEqual(processor.cache_ttl, 600)
|
|
43
|
+
|
|
44
|
+
def test_send_webhook_on_webhooks_disabled(self):
|
|
45
|
+
with utils.disable_webhooks():
|
|
46
|
+
result = self.processor.send_webhook(self.instance, "created")
|
|
47
|
+
self.assertEqual(result, {"skipped": "Webhooks disabled"})
|
|
48
|
+
|
|
49
|
+
def test_send_webhook_on_no_subscriptions(self):
|
|
50
|
+
with patch.object(
|
|
51
|
+
self.processor, "_get_subscriptions_cached"
|
|
52
|
+
) as mock_get_subs:
|
|
53
|
+
mock_get_subs.return_value = []
|
|
54
|
+
result = self.processor.send_webhook(self.instance, "created")
|
|
55
|
+
self.assertEqual(result, {"skipped": "No subscriptions"})
|
|
56
|
+
|
|
57
|
+
def test_send_webhook_on_successful_generation(self):
|
|
58
|
+
# Generating subscription, and subscriber
|
|
59
|
+
subscription = WebhookSubscriptionFactory(
|
|
60
|
+
subscriber=self.subscriber,
|
|
61
|
+
event_name="created",
|
|
62
|
+
)
|
|
63
|
+
with (
|
|
64
|
+
patch.object(
|
|
65
|
+
self.processor, "_get_subscriptions_cached"
|
|
66
|
+
) as mock_get_subs,
|
|
67
|
+
patch.object(
|
|
68
|
+
self.processor, "_group_subscriptions_by_serializer"
|
|
69
|
+
) as mock_group,
|
|
70
|
+
patch.object(self.processor, "_generate_payload") as mock_generate,
|
|
71
|
+
patch.object(self.processor, "_deliver_webhooks") as mock_deliver,
|
|
72
|
+
):
|
|
73
|
+
mock_get_subs.return_value = [{"id": subscription.id}]
|
|
74
|
+
mock_group.return_value = {None: [{"id": subscription.id}]}
|
|
75
|
+
mock_generate.return_value = {"id": self.instance.id}
|
|
76
|
+
mock_deliver.return_value = {"delivered": 1}
|
|
77
|
+
|
|
78
|
+
self.processor.send_webhook(self.instance, "created")
|
|
79
|
+
mock_get_subs.assert_called_once_with(self.instance, "created")
|
|
80
|
+
mock_group.assert_called_once_with([{"id": subscription.id}])
|
|
81
|
+
mock_generate.assert_called_once_with(
|
|
82
|
+
self.instance, "created", None
|
|
83
|
+
)
|
|
84
|
+
mock_deliver.assert_called_once()
|
|
85
|
+
args = mock_deliver.call_args[0][0]
|
|
86
|
+
self.assertEqual(args[0]["id"], subscription.id)
|
|
87
|
+
|
|
88
|
+
def test_send_webhook_on_exception_raised(self):
|
|
89
|
+
# Generating subscription, and subscriber
|
|
90
|
+
with patch.object(
|
|
91
|
+
self.processor, "_get_subscriptions_cached"
|
|
92
|
+
) as mock_get_subs:
|
|
93
|
+
mock_get_subs.side_effect = Exception("Test exception")
|
|
94
|
+
results = self.processor.send_webhook(self.instance, "created")
|
|
95
|
+
self.assertIn("error", results)
|
|
96
|
+
self.assertIn("Error sending webhook", results["error"])
|
|
97
|
+
|
|
98
|
+
def test_group_subscriptions_by_serializer(self):
|
|
99
|
+
# Generating subscriptions with different serializers
|
|
100
|
+
results = self.processor._group_subscriptions_by_serializer([])
|
|
101
|
+
self.assertEqual(dict(results), {})
|
|
102
|
+
# Creating subscriptions
|
|
103
|
+
|
|
104
|
+
results = self.processor._group_subscriptions_by_serializer(
|
|
105
|
+
[
|
|
106
|
+
{"id": 1, "serializer_class": None},
|
|
107
|
+
{"id": 2, "serializer_class": "some.path.SerializerA"},
|
|
108
|
+
]
|
|
109
|
+
)
|
|
110
|
+
self.assertEqual(
|
|
111
|
+
dict(results),
|
|
112
|
+
{
|
|
113
|
+
None: [{"id": 1, "serializer_class": None}],
|
|
114
|
+
"some.path.SerializerA": [
|
|
115
|
+
{"id": 2, "serializer_class": "some.path.SerializerA"}
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
@patch("django_webhook_subscriber.delivery.get_content_type_id")
|
|
121
|
+
def test_get_subscriptions_cached_on_cache_hit(self, mock_get_ct_id):
|
|
122
|
+
mock_get_ct_id.return_value = 1
|
|
123
|
+
cached_data = [{"id": 1, "url": "http://example.com"}]
|
|
124
|
+
cache.set("webhook_subscriptions:1:created", cached_data)
|
|
125
|
+
|
|
126
|
+
result = self.processor._get_subscriptions_cached(
|
|
127
|
+
self.instance, "created"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.assertEqual(result, cached_data)
|
|
131
|
+
mock_get_ct_id.assert_called_once()
|
|
132
|
+
|
|
133
|
+
@patch("django_webhook_subscriber.delivery.get_content_type_id")
|
|
134
|
+
@patch.object(
|
|
135
|
+
delivery.WebhookDeliveryProcessor, "_fetch_subscriptions_from_db"
|
|
136
|
+
)
|
|
137
|
+
def test_get_subscriptions_cached_on_cache_miss(
|
|
138
|
+
self, mock_fetch, mock_get_ct_id
|
|
139
|
+
):
|
|
140
|
+
mock_get_ct_id.return_value = 1
|
|
141
|
+
db_data = [{"id": 1, "url": "http://example.com"}]
|
|
142
|
+
mock_fetch.return_value = db_data
|
|
143
|
+
|
|
144
|
+
result = self.processor._get_subscriptions_cached(
|
|
145
|
+
self.instance, "created"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.assertEqual(result, db_data)
|
|
149
|
+
mock_fetch.assert_called_once_with(1, "created")
|
|
150
|
+
# Verify it was cached
|
|
151
|
+
cached = cache.get("webhook_subscriptions:1:created")
|
|
152
|
+
self.assertEqual(cached, db_data)
|
|
153
|
+
|
|
154
|
+
@patch("django_webhook_subscriber.delivery.get_content_type_id")
|
|
155
|
+
@patch.object(
|
|
156
|
+
delivery.WebhookDeliveryProcessor, "_fetch_subscriptions_from_db"
|
|
157
|
+
)
|
|
158
|
+
def test_get_subscriptions_cached_on_empty_cache(
|
|
159
|
+
self, mock_fetch, mock_get_ct_id
|
|
160
|
+
):
|
|
161
|
+
mock_get_ct_id.return_value = 1
|
|
162
|
+
mock_fetch.return_value = []
|
|
163
|
+
|
|
164
|
+
result = self.processor._get_subscriptions_cached(
|
|
165
|
+
self.instance, "created"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
self.assertEqual(result, [])
|
|
169
|
+
mock_fetch.assert_called_once()
|
|
170
|
+
|
|
171
|
+
def test_fetch_subscriptions_from_db_on_no_subscriptions(self):
|
|
172
|
+
result = self.processor._fetch_subscriptions_from_db(
|
|
173
|
+
999, "nonexistent"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
self.assertEqual(result, [])
|
|
177
|
+
|
|
178
|
+
def test_fetch_subscriptions_from_db_on_existing_subscriptions(self):
|
|
179
|
+
# Create test data
|
|
180
|
+
result = self.processor._fetch_subscriptions_from_db(
|
|
181
|
+
self.content_type.id,
|
|
182
|
+
"created",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
self.assertEqual(len(result), 1)
|
|
186
|
+
self.assertEqual(result[0]["id"], self.subscription.id)
|
|
187
|
+
self.assertEqual(result[0]["subscriber_id"], self.subscriber.id)
|
|
188
|
+
self.assertEqual(result[0]["url"], self.subscriber.target_url)
|
|
189
|
+
|
|
190
|
+
@patch("django_webhook_subscriber.delivery.serialize_webhook_instance")
|
|
191
|
+
def test_generate_payload_on_successful_serialization(
|
|
192
|
+
self, mock_serialize
|
|
193
|
+
):
|
|
194
|
+
mock_serialize.return_value = {"field1": "value1", "field2": "value2"}
|
|
195
|
+
|
|
196
|
+
result = self.processor._generate_payload(
|
|
197
|
+
self.instance, "created", None
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
self.assertEqual(result["pk"], 1)
|
|
201
|
+
self.assertEqual(result["event_signal"], "created")
|
|
202
|
+
self.assertEqual(
|
|
203
|
+
result["source"], "django_webhook_subscriber.webhookdeliverylog"
|
|
204
|
+
)
|
|
205
|
+
self.assertEqual(
|
|
206
|
+
result["fields"], {"field1": "value1", "field2": "value2"}
|
|
207
|
+
)
|
|
208
|
+
self.assertIn("timestamp", result)
|
|
209
|
+
|
|
210
|
+
@patch("django_webhook_subscriber.delivery.serialize_webhook_instance")
|
|
211
|
+
def test_generate_payload_on_serialization_exception(self, mock_serialize):
|
|
212
|
+
mock_serialize.side_effect = Exception("Serialization failed")
|
|
213
|
+
|
|
214
|
+
result = self.processor._generate_payload(
|
|
215
|
+
self.instance, "created", None
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
self.assertEqual(result["pk"], 1)
|
|
219
|
+
self.assertEqual(result["event_signal"], "created")
|
|
220
|
+
self.assertIn("error", result)
|
|
221
|
+
self.assertEqual(result["fields"], {})
|
|
222
|
+
|
|
223
|
+
def test_deliver_webhooks_on_no_subscriptions(self):
|
|
224
|
+
result = self.processor._deliver_webhooks([])
|
|
225
|
+
|
|
226
|
+
self.assertEqual(result, {"processed": 0})
|
|
227
|
+
|
|
228
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
229
|
+
def test_deliver_webhooks_on_single_batch_processing(self, mock_task):
|
|
230
|
+
mock_result = Mock()
|
|
231
|
+
mock_result.id = "task-123"
|
|
232
|
+
mock_task.delay.return_value = mock_result
|
|
233
|
+
|
|
234
|
+
subscriptions = [{"id": i} for i in range(10)]
|
|
235
|
+
result = self.processor._deliver_webhooks(subscriptions)
|
|
236
|
+
|
|
237
|
+
self.assertEqual(result["processed"], 10)
|
|
238
|
+
self.assertEqual(result["batches"], 1)
|
|
239
|
+
self.assertEqual(result["task_id"], "task-123")
|
|
240
|
+
mock_task.delay.assert_called_once()
|
|
241
|
+
|
|
242
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
243
|
+
@override_settings(WEBHOOK_SUBSCRIBER={"MAX_BATCH_SIZE": 5})
|
|
244
|
+
def test_deliver_webhooks_on_multiple_batch_processing(self, mock_task):
|
|
245
|
+
mock_result1 = Mock()
|
|
246
|
+
mock_result1.id = "task-1"
|
|
247
|
+
mock_result2 = Mock()
|
|
248
|
+
mock_result2.id = "task-2"
|
|
249
|
+
mock_task.delay.side_effect = [mock_result1, mock_result2]
|
|
250
|
+
|
|
251
|
+
# Create processor with new settings
|
|
252
|
+
processor = delivery.WebhookDeliveryProcessor()
|
|
253
|
+
subscriptions = [{"id": i} for i in range(10)]
|
|
254
|
+
result = processor._deliver_webhooks(subscriptions)
|
|
255
|
+
|
|
256
|
+
self.assertEqual(result["processed"], 10)
|
|
257
|
+
self.assertEqual(result["batches"], 2)
|
|
258
|
+
self.assertEqual(len(result["task_ids"]), 2)
|
|
259
|
+
self.assertEqual(mock_task.delay.call_count, 2)
|
|
260
|
+
|
|
261
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
262
|
+
def test_process_single_batch_on_successful_delivery(self, mock_task):
|
|
263
|
+
mock_result = Mock()
|
|
264
|
+
mock_result.id = "task-123"
|
|
265
|
+
mock_task.delay.return_value = mock_result
|
|
266
|
+
|
|
267
|
+
subscriptions = [{"id": 1}]
|
|
268
|
+
result = self.processor._process_single_batch(subscriptions)
|
|
269
|
+
|
|
270
|
+
self.assertEqual(result["processed"], 1)
|
|
271
|
+
self.assertEqual(result["batches"], 1)
|
|
272
|
+
self.assertEqual(result["task_id"], "task-123")
|
|
273
|
+
|
|
274
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
275
|
+
def test_process_single_batch_on_delivery_exception(self, mock_task):
|
|
276
|
+
mock_task.delay.side_effect = Exception("Celery error")
|
|
277
|
+
|
|
278
|
+
subscriptions = [{"id": 1}]
|
|
279
|
+
result = self.processor._process_single_batch(subscriptions)
|
|
280
|
+
|
|
281
|
+
self.assertIn("error", result)
|
|
282
|
+
self.assertEqual(result["processed"], 0)
|
|
283
|
+
|
|
284
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
285
|
+
def test_process_multiple_batches_on_successful_delivery(self, mock_task):
|
|
286
|
+
mock_result1 = Mock()
|
|
287
|
+
mock_result1.id = "task-1"
|
|
288
|
+
mock_result2 = Mock()
|
|
289
|
+
mock_result2.id = "task-2"
|
|
290
|
+
mock_task.delay.side_effect = [mock_result1, mock_result2]
|
|
291
|
+
|
|
292
|
+
subscriptions = [{"id": i} for i in range(10)]
|
|
293
|
+
result = self.processor._process_multiple_batches(subscriptions, 5)
|
|
294
|
+
|
|
295
|
+
self.assertEqual(result["processed"], 10)
|
|
296
|
+
self.assertEqual(result["batches"], 2)
|
|
297
|
+
self.assertEqual(len(result["task_ids"]), 2)
|
|
298
|
+
|
|
299
|
+
@patch("django_webhook_subscriber.delivery.process_webhook_delivery_batch")
|
|
300
|
+
def test_process_multiple_batches_on_delivery_exception(self, mock_task):
|
|
301
|
+
mock_result1 = Mock()
|
|
302
|
+
mock_result1.id = "task-1"
|
|
303
|
+
mock_task.delay.side_effect = [mock_result1, Exception("Celery error")]
|
|
304
|
+
|
|
305
|
+
subscriptions = [{"id": i} for i in range(10)]
|
|
306
|
+
result = self.processor._process_multiple_batches(subscriptions, 5)
|
|
307
|
+
|
|
308
|
+
self.assertEqual(result["processed"], 5)
|
|
309
|
+
self.assertEqual(result["batches"], 2)
|
|
310
|
+
self.assertEqual(len(result["task_ids"]), 1) # Only successful one
|
|
311
|
+
# Check that one batch has error
|
|
312
|
+
error_batch = next(b for b in result["batch_details"] if "error" in b)
|
|
313
|
+
self.assertIn("error", error_batch)
|
|
314
|
+
|
|
315
|
+
def test_clear_webhook_cache_on_no_parameters(self):
|
|
316
|
+
# Set up some cache data
|
|
317
|
+
cache.set("webhook_subscriptions:1:created", [{"id": 1}])
|
|
318
|
+
cache.set("webhook_subscriptions:2:updated", [{"id": 2}])
|
|
319
|
+
|
|
320
|
+
self.processor.clear_webhook_cache()
|
|
321
|
+
|
|
322
|
+
# Verify cache is cleared
|
|
323
|
+
self.assertIsNone(cache.get("webhook_subscriptions:1:created"))
|
|
324
|
+
self.assertIsNone(cache.get("webhook_subscriptions:2:updated"))
|
|
325
|
+
|
|
326
|
+
def test_clear_webhook_cache_on_content_type_only(self):
|
|
327
|
+
# Create subscriptions for testing
|
|
328
|
+
WebhookSubscriptionFactory(
|
|
329
|
+
subscriber=self.subscriber, event_name="updated"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Set up cache
|
|
333
|
+
cache.set(
|
|
334
|
+
f"webhook_subscriptions:{self.content_type.id}:created",
|
|
335
|
+
[{"id": 1}],
|
|
336
|
+
)
|
|
337
|
+
cache.set(
|
|
338
|
+
f"webhook_subscriptions:{self.content_type.id}:updated",
|
|
339
|
+
[{"id": 2}],
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
self.processor.clear_webhook_cache(content_type=self.content_type)
|
|
343
|
+
|
|
344
|
+
# Verify both are cleared
|
|
345
|
+
self.assertIsNone(
|
|
346
|
+
cache.get(f"webhook_subscriptions:{self.content_type.id}:created")
|
|
347
|
+
)
|
|
348
|
+
self.assertIsNone(
|
|
349
|
+
cache.get(f"webhook_subscriptions:{self.content_type.id}:updated")
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
def test_clear_webhook_cache_on_content_type_and_event_name(self):
|
|
353
|
+
cache_key = f"webhook_subscriptions:{self.content_type.id}:created"
|
|
354
|
+
cache.set(cache_key, [{"id": 1}])
|
|
355
|
+
|
|
356
|
+
self.processor.clear_webhook_cache(
|
|
357
|
+
content_type=self.content_type, event_name="created"
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
self.assertIsNone(cache.get(cache_key))
|
|
361
|
+
|
|
362
|
+
def test_get_cache_stats_on_no_cache(self):
|
|
363
|
+
stats = self.processor.get_cache_stats()
|
|
364
|
+
|
|
365
|
+
self.assertEqual(stats["cached_keys"], 0)
|
|
366
|
+
self.assertEqual(stats["total_cached_subscriptions"], 0)
|
|
367
|
+
self.assertEqual(stats["cache_hit_ratio"], 0.0)
|
|
368
|
+
self.assertGreater(stats["total_possible_keys"], 0)
|
|
369
|
+
|
|
370
|
+
def test_get_cache_stats_on_existing_cache(self):
|
|
371
|
+
# Populate cache
|
|
372
|
+
cache_key = f"webhook_subscriptions:{self.content_type.id}:created"
|
|
373
|
+
cached_data = [{"id": 1}, {"id": 2}]
|
|
374
|
+
cache.set(cache_key, cached_data)
|
|
375
|
+
|
|
376
|
+
stats = self.processor.get_cache_stats()
|
|
377
|
+
|
|
378
|
+
self.assertEqual(stats["cached_keys"], 1)
|
|
379
|
+
self.assertEqual(stats["total_cached_subscriptions"], 2)
|
|
380
|
+
self.assertEqual(stats["cache_hit_ratio"], 50.0)
|
|
381
|
+
self.assertEqual(stats["total_possible_keys"], 2)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
class WebhookDeliveryProcessorFunctionsTests(TestCase):
|
|
21
385
|
def setUp(self):
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
self.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
#
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
)
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
mock_deliver_webhook.assert_called_once_with(
|
|
98
|
-
self.webhook,
|
|
99
|
-
payload,
|
|
100
|
-
event_signal,
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# Ensure non-matching and inactive webhooks were not processed
|
|
104
|
-
self.assertNotIn(
|
|
105
|
-
non_matching_webhook, mock_deliver_webhook.call_args_list
|
|
106
|
-
)
|
|
107
|
-
self.assertNotIn(inactive_webhook, mock_deliver_webhook.call_args_list)
|
|
108
|
-
|
|
109
|
-
@patch('requests.post')
|
|
110
|
-
def test_deliver_webhook_success(self, mock_post):
|
|
111
|
-
# Mock successful response
|
|
112
|
-
mock_response = MagicMock()
|
|
113
|
-
mock_response.status_code = 200
|
|
114
|
-
mock_response.ok = True # TODO check without this
|
|
115
|
-
mock_response.content = '{"status": "ok"}'
|
|
116
|
-
mock_post.return_value = mock_response
|
|
117
|
-
|
|
118
|
-
# Test delivery
|
|
119
|
-
payload = {'key': 'value'}
|
|
120
|
-
log = deliver_webhook(self.webhook, payload, 'created')
|
|
121
|
-
|
|
122
|
-
# Verify the request was made properly
|
|
123
|
-
mock_post.assert_called_once()
|
|
124
|
-
args, kwargs = mock_post.call_args
|
|
125
|
-
self.assertEqual(args[0], self.webhook.endpoint)
|
|
126
|
-
self.assertEqual(kwargs['json'], payload)
|
|
127
|
-
self.assertEqual(kwargs['headers']['X-Secret'], self.webhook.secret)
|
|
128
|
-
|
|
129
|
-
# Verify log was created
|
|
130
|
-
self.assertEqual(log.webhook, self.webhook)
|
|
131
|
-
self.assertEqual(log.event_signal, 'created')
|
|
132
|
-
self.assertEqual(log.response_status, 200)
|
|
133
|
-
self.assertEqual(log.response_body, '{"status": "ok"}')
|
|
134
|
-
|
|
135
|
-
# Verify the webhook was updated
|
|
136
|
-
self.webhook.refresh_from_db()
|
|
137
|
-
self.assertEqual(self.webhook.last_response, '{"status": "ok"}')
|
|
138
|
-
self.assertIsNotNone(self.webhook.last_success)
|
|
139
|
-
|
|
140
|
-
@patch('requests.post')
|
|
141
|
-
def test_deliver_webhook_response_error(self, mock_post):
|
|
142
|
-
# Mock successful response
|
|
143
|
-
mock_response = MagicMock()
|
|
144
|
-
mock_response.status_code = 400
|
|
145
|
-
mock_response.content = '{"status": "error"}'
|
|
146
|
-
mock_post.return_value = mock_response
|
|
147
|
-
|
|
148
|
-
# Test delivery
|
|
149
|
-
payload = {'key': 'value'}
|
|
150
|
-
log = deliver_webhook(self.webhook, payload, 'created')
|
|
151
|
-
|
|
152
|
-
# Verify log was created
|
|
153
|
-
self.assertEqual(log.webhook, self.webhook)
|
|
154
|
-
self.assertEqual(log.event_signal, 'created')
|
|
155
|
-
self.assertEqual(log.response_status, 400)
|
|
156
|
-
self.assertEqual(log.response_body, '{"status": "error"}')
|
|
157
|
-
|
|
158
|
-
# Verify the webhook was updated
|
|
159
|
-
self.webhook.refresh_from_db()
|
|
160
|
-
self.assertEqual(self.webhook.last_response, '{"status": "error"}')
|
|
161
|
-
self.assertIsNotNone(self.webhook.last_failure)
|
|
162
|
-
|
|
163
|
-
@patch('requests.post')
|
|
164
|
-
def test_deliver_webhook_keep_response_false(self, mock_post):
|
|
165
|
-
self.webhook.keep_last_response = False
|
|
166
|
-
# Mock successful response
|
|
167
|
-
mock_response = MagicMock()
|
|
168
|
-
mock_response.status_code = 200
|
|
169
|
-
mock_response.ok = True # TODO check without this
|
|
170
|
-
mock_response.content = '{"status": "ok"}'
|
|
171
|
-
mock_post.return_value = mock_response
|
|
172
|
-
|
|
173
|
-
# Test delivery
|
|
174
|
-
payload = {'key': 'value'}
|
|
175
|
-
deliver_webhook(self.webhook, payload, 'created')
|
|
176
|
-
|
|
177
|
-
# Verify the webhook was updated
|
|
178
|
-
self.webhook.refresh_from_db()
|
|
179
|
-
self.assertIsNone(self.webhook.last_response)
|
|
180
|
-
self.assertIsNotNone(self.webhook.last_success)
|
|
181
|
-
|
|
182
|
-
@patch('requests.post')
|
|
183
|
-
def test_deliver_webhook_failure(self, mock_post):
|
|
184
|
-
# Mock successful response
|
|
185
|
-
mock_post.side_effect = Exception("Connection error")
|
|
186
|
-
|
|
187
|
-
# Test delivery
|
|
188
|
-
payload = {'key': 'value'}
|
|
189
|
-
log = deliver_webhook(self.webhook, payload, 'created')
|
|
190
|
-
|
|
191
|
-
# Verify log was created with error
|
|
192
|
-
self.assertEqual(log.error_message, "Connection error")
|
|
193
|
-
self.assertIsNone(log.response_status)
|
|
194
|
-
|
|
195
|
-
# Verify webhook was updated
|
|
196
|
-
self.webhook.refresh_from_db()
|
|
197
|
-
self.assertIsNotNone(self.webhook.last_failure)
|
|
386
|
+
self.processor = delivery.webhook_delivery_processor
|
|
387
|
+
|
|
388
|
+
def test_send_webhook_function_on_calling_the_right_method(self):
|
|
389
|
+
with patch.object(self.processor, "send_webhook") as mock_send:
|
|
390
|
+
mock_send.return_value = Mock()
|
|
391
|
+
result = delivery.send_webhooks(
|
|
392
|
+
instance=self, event_name="created", extra_context={}
|
|
393
|
+
)
|
|
394
|
+
mock_send.assert_called_once_with(
|
|
395
|
+
self, "created", context=None, extra_context={}
|
|
396
|
+
)
|
|
397
|
+
self.assertEqual(result, mock_send.return_value)
|
|
398
|
+
|
|
399
|
+
def test_clear_webhook_cache_function_on_calling_the_right_functions(self):
|
|
400
|
+
with (
|
|
401
|
+
patch.object(self.processor, "clear_webhook_cache") as mock_clear,
|
|
402
|
+
patch(
|
|
403
|
+
"django_webhook_subscriber.delivery.clear_content_type_cache"
|
|
404
|
+
) as mock_clear_content_cache,
|
|
405
|
+
):
|
|
406
|
+
mock_clear.return_value = None
|
|
407
|
+
mock_clear_content_cache.return_value = None
|
|
408
|
+
result = delivery.clear_webhook_cache()
|
|
409
|
+
mock_clear.assert_called_once_with(
|
|
410
|
+
content_type=None, event_name=None
|
|
411
|
+
)
|
|
412
|
+
mock_clear_content_cache.assert_called_once_with()
|
|
413
|
+
self.assertIsNone(result)
|
|
414
|
+
|
|
415
|
+
def test_get_webhook_cache_stats_function_on_calling_the_right_method(
|
|
416
|
+
self,
|
|
417
|
+
):
|
|
418
|
+
with patch.object(self.processor, "get_cache_stats") as mock_stats:
|
|
419
|
+
mock_stats.return_value = {"key": "value"}
|
|
420
|
+
result = delivery.get_webhook_cache_stats()
|
|
421
|
+
mock_stats.assert_called_once_with()
|
|
422
|
+
self.assertEqual(result, {"key": "value"})
|
|
423
|
+
|
|
424
|
+
def test_warm_webhook_cache_on_warming_all_subscriptions(self):
|
|
425
|
+
# Generating subscription, and subscriber
|
|
426
|
+
content_type = ContentType.objects.get(
|
|
427
|
+
app_label="django_webhook_subscriber", model="webhooksubscriber"
|
|
428
|
+
)
|
|
429
|
+
subscriber = WebhookSubscriberFactory(content_type=content_type)
|
|
430
|
+
WebhookSubscriptionFactory(subscriber=subscriber, event_name="created")
|
|
431
|
+
|
|
432
|
+
with patch.object(
|
|
433
|
+
self.processor, "_get_subscriptions_cached"
|
|
434
|
+
) as mock_cache:
|
|
435
|
+
results = delivery.warm_webhook_cache()
|
|
436
|
+
mock_cache.assert_called_once()
|
|
437
|
+
args = mock_cache.call_args[0]
|
|
438
|
+
self.assertEqual(len(args), 2)
|
|
439
|
+
self.assertIsInstance(args[0], models.WebhookSubscriber)
|
|
440
|
+
self.assertEqual(args[1], "created")
|
|
441
|
+
self.assertEqual(results["warmed"], 1)
|
|
442
|
+
|
|
443
|
+
def test_warm_webhook_cache_on_exception_raised(self):
|
|
444
|
+
# Generating subscription, and subscriber
|
|
445
|
+
content_type = ContentType.objects.get(
|
|
446
|
+
app_label="django_webhook_subscriber", model="webhooksubscriber"
|
|
447
|
+
)
|
|
448
|
+
subscriber = WebhookSubscriberFactory(content_type=content_type)
|
|
449
|
+
WebhookSubscriptionFactory(subscriber=subscriber, event_name="created")
|
|
450
|
+
with patch.object(
|
|
451
|
+
self.processor, "_get_subscriptions_cached"
|
|
452
|
+
) as mock_cache:
|
|
453
|
+
mock_cache.side_effect = Exception("Test exception")
|
|
454
|
+
results = delivery.warm_webhook_cache()
|
|
455
|
+
mock_cache.assert_called_once()
|
|
456
|
+
args = mock_cache.call_args[0]
|
|
457
|
+
self.assertEqual(len(args), 2)
|
|
458
|
+
self.assertIsInstance(args[0], models.WebhookSubscriber)
|
|
459
|
+
self.assertEqual(args[1], "created")
|
|
460
|
+
self.assertEqual(results["warmed"], 0)
|