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.
Files changed (41) hide show
  1. django_webhook_subscriber/__init__.py +7 -1
  2. django_webhook_subscriber/admin.py +831 -182
  3. django_webhook_subscriber/apps.py +3 -20
  4. django_webhook_subscriber/conf.py +11 -24
  5. django_webhook_subscriber/delivery.py +414 -159
  6. django_webhook_subscriber/http.py +51 -0
  7. django_webhook_subscriber/management/commands/webhook.py +169 -0
  8. django_webhook_subscriber/management/commands/webhook_cache.py +173 -0
  9. django_webhook_subscriber/management/commands/webhook_logs.py +226 -0
  10. django_webhook_subscriber/management/commands/webhook_performance_test.py +469 -0
  11. django_webhook_subscriber/management/commands/webhook_send.py +96 -0
  12. django_webhook_subscriber/management/commands/webhook_status.py +139 -0
  13. django_webhook_subscriber/managers.py +36 -14
  14. django_webhook_subscriber/migrations/0002_remove_webhookregistry_content_type_and_more.py +192 -0
  15. django_webhook_subscriber/models.py +291 -114
  16. django_webhook_subscriber/serializers.py +16 -50
  17. django_webhook_subscriber/tasks.py +434 -56
  18. django_webhook_subscriber/tests/factories.py +40 -0
  19. django_webhook_subscriber/tests/settings.py +27 -8
  20. django_webhook_subscriber/tests/test_delivery.py +453 -190
  21. django_webhook_subscriber/tests/test_http.py +32 -0
  22. django_webhook_subscriber/tests/test_managers.py +26 -37
  23. django_webhook_subscriber/tests/test_models.py +341 -251
  24. django_webhook_subscriber/tests/test_serializers.py +22 -56
  25. django_webhook_subscriber/tests/test_tasks.py +477 -189
  26. django_webhook_subscriber/tests/test_utils.py +98 -94
  27. django_webhook_subscriber/utils.py +87 -69
  28. django_webhook_subscriber/validators.py +53 -0
  29. django_webhook_subscriber-2.0.0.dist-info/METADATA +774 -0
  30. django_webhook_subscriber-2.0.0.dist-info/RECORD +38 -0
  31. django_webhook_subscriber/management/commands/check_webhook_tasks.py +0 -113
  32. django_webhook_subscriber/management/commands/clean_webhook_logs.py +0 -65
  33. django_webhook_subscriber/management/commands/test_webhook.py +0 -96
  34. django_webhook_subscriber/signals.py +0 -152
  35. django_webhook_subscriber/testing.py +0 -14
  36. django_webhook_subscriber/tests/test_signals.py +0 -268
  37. django_webhook_subscriber-0.4.0.dist-info/METADATA +0 -448
  38. django_webhook_subscriber-0.4.0.dist-info/RECORD +0 -33
  39. {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/WHEEL +0 -0
  40. {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/licenses/LICENSE +0 -0
  41. {django_webhook_subscriber-0.4.0.dist-info → django_webhook_subscriber-2.0.0.dist-info}/top_level.txt +0 -0
@@ -1,219 +1,507 @@
1
- from unittest.mock import patch, MagicMock
1
+ from unittest.mock import Mock, patch
2
2
 
3
+ import requests
3
4
  from django.test import TestCase
4
- from django.contrib.auth.models import User
5
- from django.contrib.contenttypes.models import ContentType
5
+ from django.utils import timezone
6
6
 
7
- from django_webhook_subscriber.models import WebhookRegistry
8
- from django_webhook_subscriber.tasks import (
9
- async_deliver_webhook,
10
- process_webhook_batch,
7
+ from django_webhook_subscriber import models, tasks
8
+
9
+ from .factories import (
10
+ ContentTypeFactory,
11
+ WebhookDeliveryLogFactory,
12
+ WebhookSubscriberFactory,
13
+ WebhookSubscriptionFactory,
11
14
  )
12
15
 
13
16
 
14
- class AsyncWebhookTaskTests(TestCase):
17
+ class DeliverWebhookTasksTests(TestCase):
15
18
  def setUp(self):
16
- # Create a content type for the User model
17
- self.example_content_type = ContentType.objects.get_for_model(User)
18
-
19
- # Create test webhooks
20
- self.webhook = WebhookRegistry.objects.create(
21
- name='Test Webhook',
22
- content_type=self.example_content_type,
23
- event_signals=['CREATE', 'UPDATE'],
24
- endpoint='https://example.com/webhook',
25
- secret='test-secret-key',
26
- )
27
-
28
- # Create test payload
29
- self.payload = {
30
- 'pk': 1,
31
- 'model': 'auth.user',
32
- 'event_signal': 'created',
33
- 'fields': {'username': 'testuser', 'email': 'test@example.com'},
34
- }
35
-
36
- self.event_signal = 'created'
19
+ self.subscription = WebhookSubscriptionFactory()
37
20
 
38
- @patch('django_webhook_subscriber.delivery.deliver_webhook')
39
- def test_async_deliver_webhook_success(self, mock_deliver):
40
- # Mock delivery to return a log object
41
- mock_log = MagicMock(id=1)
42
- mock_deliver.return_value = mock_log
43
-
44
- # Call the task function
45
- result = async_deliver_webhook(
46
- self.webhook.id, self.payload, self.event_signal
21
+ def mock_response(self, status_code=200, json_data=None):
22
+ mock_resp = requests.Response()
23
+ mock_resp.status_code = status_code
24
+ mock_resp._content = (
25
+ b'{"key": "value"}' if json_data is None else json_data
47
26
  )
48
-
49
- # Verify the delivery was attempted
50
- mock_deliver.assert_called_once_with(
51
- self.webhook,
52
- self.payload,
53
- self.event_signal,
27
+ return mock_resp
28
+
29
+ def test_deliver_webhook_on_subscription_does_not_exist_error(self):
30
+ result = tasks.deliver_webhook("", {}, subscription_id=9999)
31
+ self.assertIn("error", result)
32
+ self.assertIn("Subscription not found", result["error"])
33
+
34
+ def test_deliver_webhook_on_no_active_subscription_error(self):
35
+ # deactivate subscription
36
+ self.subscription.is_active = False
37
+ self.subscription.save()
38
+ result = tasks.deliver_webhook(
39
+ url="",
40
+ payload={},
41
+ subscription_id=self.subscription.id,
54
42
  )
55
-
56
- # Verify the result is the log object
57
- self.assertEqual(result, 1)
58
-
59
- @patch('django_webhook_subscriber.delivery.deliver_webhook')
60
- def test_async_deliver_inactive_webhook(self, mock_deliver):
61
- # Set webhook to inactive
62
- self.webhook.is_active = False
63
- self.webhook.save()
64
-
65
- # Call the task function
66
- result = async_deliver_webhook(
67
- self.webhook.id,
68
- self.payload,
69
- self.event_signal,
43
+ self.assertIn("skipped", result)
44
+ self.assertIn("Subscription inactive", result["skipped"])
45
+
46
+ @patch("django_webhook_subscriber.tasks.webhook_session")
47
+ def test_deliver_webhook_on_successful_delivery(
48
+ self, mock_session_context
49
+ ):
50
+ # Mocking the session.post request to return a successful response
51
+ mock_session = mock_session_context.return_value.__enter__.return_value
52
+ mock_session.post.return_value = self.mock_response(200)
53
+
54
+ result = tasks.deliver_webhook(
55
+ url="http://example.com/webhook",
56
+ payload={"key": "value"},
57
+ subscription_id=self.subscription.id,
70
58
  )
71
-
72
- # Verify no delivery was attempted
73
- mock_deliver.assert_not_called()
74
-
75
- # Verify the result is None
76
- self.assertIsNone(result)
77
-
78
- @patch('django_webhook_subscriber.delivery.deliver_webhook')
79
- def test_async_deliver_nonexistent_webhook(self, mock_deliver):
80
- # Use a non-existent webhook ID
81
- non_existent_id = 999
82
-
83
- # Call the task function
84
- result = async_deliver_webhook(
85
- non_existent_id,
86
- self.payload,
87
- self.event_signal,
59
+ # Asserting result indicates success
60
+ mock_session.post.assert_called_once_with(
61
+ "http://example.com/webhook",
62
+ json={"key": "value"},
63
+ headers=tasks.generate_headers(self.subscription.subscriber),
64
+ timeout=30,
88
65
  )
66
+ # Asserting result indicates success
67
+ self.assertTrue(result["success"])
68
+ self.assertEqual(result["status_code"], 200)
69
+ self.assertIn("duration_ms", result)
70
+ self.assertEqual(result["attempt"], 1)
71
+
72
+ # Asserting DeliveryLog was created
73
+ log = models.WebhookDeliveryLog.objects.first()
74
+ self.assertIsNotNone(log)
75
+ self.assertEqual(log.subscription, self.subscription)
76
+ self.assertEqual(log.response_status, 200)
77
+ self.assertEqual(log.response_body, '{"key": "value"}')
78
+
79
+ @patch("django_webhook_subscriber.tasks.webhook_session")
80
+ def test_deliver_webhook_on_failed_delivery(self, mock_session_context):
81
+ # Mocking the session.post request to return a successful response
82
+ mock_session = mock_session_context.return_value.__enter__.return_value
83
+ mock_session.post.return_value = self.mock_response(400)
89
84
 
90
- # Verify no delivery was attempted
91
- mock_deliver.assert_not_called()
85
+ with patch(
86
+ "django_webhook_subscriber.tasks._should_retry_delivery"
87
+ ) as mock_should_retry:
88
+ mock_should_retry.return_value = True
89
+ result = tasks.deliver_webhook(
90
+ url="http://example.com/webhook",
91
+ payload={"key": "value"},
92
+ subscription_id=self.subscription.id,
93
+ )
94
+ # Asserting result indicates success
95
+ mock_session.post.assert_called_once_with(
96
+ "http://example.com/webhook",
97
+ json={"key": "value"},
98
+ headers=tasks.generate_headers(self.subscription.subscriber),
99
+ timeout=30,
100
+ )
101
+ # Asserting result indicates success
102
+ self.assertFalse(result["success"])
103
+ self.assertEqual(result["status_code"], 400)
104
+ self.assertIn("duration_ms", result)
105
+ self.assertEqual(result["attempt"], 1)
106
+ self.assertTrue(result["will_retry"])
107
+
108
+ # Asserting DeliveryLog was created
109
+ log = models.WebhookDeliveryLog.objects.first()
110
+ self.assertIsNotNone(log)
111
+ self.assertEqual(log.subscription, self.subscription)
112
+ self.assertEqual(log.response_status, 400)
113
+ self.assertEqual(log.response_body, '{"key": "value"}')
114
+
115
+ @patch("django_webhook_subscriber.tasks.webhook_session")
116
+ def test_deliver_webhook_on_timeout_error_during_delivery(
117
+ self, mock_session_context
118
+ ):
119
+ # Mocking the session.post request to return a successful response
120
+ mock_session = mock_session_context.return_value.__enter__.return_value
121
+ mock_session.post.side_effect = requests.exceptions.Timeout("Timeout!")
122
+
123
+ result = tasks.deliver_webhook(
124
+ url="http://example.com/webhook",
125
+ payload={"key": "value"},
126
+ subscription_id=self.subscription.id,
127
+ )
128
+ # Asserting result indicates success
129
+ self.assertIn("error", result)
130
+ self.assertIn("Timeout error", result["error"])
131
+ self.assertEqual(result["attempt"], 1)
132
+ # Asserting DeliveryLog was created
133
+ log = models.WebhookDeliveryLog.objects.first()
134
+ self.assertIsNotNone(log)
135
+ self.assertEqual(log.subscription, self.subscription)
136
+ self.assertEqual(log.error_message, "Timeout after 30s")
137
+
138
+ @patch("django_webhook_subscriber.tasks.webhook_session")
139
+ def test_deliver_webhook_on_connection_error_during_delivery(
140
+ self, mock_session_context
141
+ ):
142
+ # Mocking the session.post request to return a successful response
143
+ mock_session = mock_session_context.return_value.__enter__.return_value
144
+ mock_session.post.side_effect = requests.exceptions.ConnectionError(
145
+ "Connection error!"
146
+ )
92
147
 
93
- # Verify the result is None
94
- self.assertIsNone(result)
148
+ result = tasks.deliver_webhook(
149
+ url="http://example.com/webhook",
150
+ payload={"key": "value"},
151
+ subscription_id=self.subscription.id,
152
+ )
153
+ # Asserting result indicates success
154
+ self.assertIn("error", result)
155
+ self.assertIn("Connection error", result["error"])
156
+ self.assertEqual(result["attempt"], 1)
157
+ # Asserting DeliveryLog was created
158
+ log = models.WebhookDeliveryLog.objects.first()
159
+ self.assertIsNotNone(log)
160
+ self.assertEqual(log.subscription, self.subscription)
161
+ self.assertIn("Connection error", log.error_message)
162
+
163
+ @patch("django_webhook_subscriber.tasks.webhook_session")
164
+ def test_deliver_webhook_on_exception_during_delivery(
165
+ self, mock_session_context
166
+ ):
167
+ # Mocking the session.post request to return a successful response
168
+ mock_session = mock_session_context.return_value.__enter__.return_value
169
+ mock_session.post.side_effect = Exception("Some error!")
170
+
171
+ result = tasks.deliver_webhook(
172
+ url="http://example.com/webhook",
173
+ payload={"key": "value"},
174
+ subscription_id=self.subscription.id,
175
+ )
176
+ # Asserting result indicates success
177
+ self.assertIn("error", result)
178
+ self.assertIn("Unexpected error", result["error"])
179
+ self.assertEqual(result["attempt"], 1)
180
+ # Asserting DeliveryLog was created
181
+ log = models.WebhookDeliveryLog.objects.first()
182
+ self.assertIsNotNone(log)
183
+ self.assertEqual(log.subscription, self.subscription)
184
+ self.assertIn("Unexpected error", log.error_message)
185
+
186
+ def test_should_retry_delivery_on_max_retries_exceeded(self):
187
+ should_retry = tasks._should_retry_delivery(
188
+ self.subscription,
189
+ attempt=self.subscription.subscriber.max_retries + 1,
190
+ )
191
+ self.assertFalse(should_retry)
95
192
 
96
- @patch('django_webhook_subscriber.tasks.async_deliver_webhook')
97
- def test_process_webhook_batch(self, mock_async_task):
98
- # Create a second webhook
99
- second_webhook = WebhookRegistry.objects.create(
100
- name='Second Webhook',
101
- content_type=self.example_content_type,
102
- event_signals=['CREATE'],
103
- endpoint='https://example2.com/webhook',
104
- secret='test-secret-key-2',
193
+ def test_should_retry_delivery_on_status_code_retryable(self):
194
+ should_retry = tasks._should_retry_delivery(
195
+ self.subscription, attempt=1, status_code=500
105
196
  )
197
+ self.assertTrue(should_retry)
106
198
 
107
- # Mock the group object and its apply_async method
108
- mock_group = MagicMock()
109
- mock_group.apply_async.return_value = 'group-result'
199
+ def test_should_retry_delivery_on_status_code_not_retryable(self):
200
+ should_retry = tasks._should_retry_delivery(
201
+ self.subscription, attempt=1, status_code=429
202
+ )
203
+ self.assertFalse(should_retry)
204
+
205
+ @patch("django_webhook_subscriber.tasks.deliver_webhook")
206
+ def test_schedule_retry_on_successful_scheduling(self, mock_deliver):
207
+ tasks._schedule_retry(
208
+ url="http://example.com/webhook",
209
+ payload={"key": "value"},
210
+ subscription_id=self.subscription.id,
211
+ current_attempt=1,
212
+ )
213
+ mock_deliver.apply_async.assert_called_once_with(
214
+ args=(
215
+ "http://example.com/webhook",
216
+ {"key": "value"},
217
+ self.subscription.id,
218
+ ),
219
+ kwargs={"attempt": 2},
220
+ countdown=60, # default
221
+ )
110
222
 
111
- # Create a mock for the signature function returned by .s()
112
- mock_signature = MagicMock()
113
- mock_async_task.s.return_value = mock_signature
223
+ @patch("django_webhook_subscriber.tasks.deliver_webhook")
224
+ def test_schedule_retry_on_failure(self, mock_deliver):
225
+ mock_deliver.apply_async.side_effect = Exception("Some error!")
226
+ # Asserting no raise
227
+ tasks._schedule_retry(
228
+ url="http://example.com/webhook",
229
+ payload={"key": "value"},
230
+ subscription_id=self.subscription.id,
231
+ current_attempt=1,
232
+ )
233
+ mock_deliver.apply_async.assert_called_once()
114
234
 
115
- # Mock the group constructor
235
+ @patch("django_webhook_subscriber.tasks._schedule_retry")
236
+ def test_handle_delivery_exception_on_retry(self, mock_schedule_retry):
237
+ log = WebhookDeliveryLogFactory(subscription=self.subscription)
116
238
  with patch(
117
- 'celery.group', return_value=mock_group
118
- ) as mock_group_constructor:
119
- # Call the batch processing task
120
- webhook_ids = [self.webhook.id, second_webhook.id]
121
- result = process_webhook_batch(
122
- webhook_ids,
123
- self.payload,
124
- self.event_signal,
239
+ "django_webhook_subscriber.tasks._should_retry_delivery"
240
+ ) as mock_should_retry:
241
+ mock_should_retry.return_value = True
242
+ tasks._handle_delivery_exception(
243
+ log,
244
+ self.subscription,
245
+ "Some error!",
246
+ url="http://example.com/webhook",
247
+ payload={"key": "value"},
248
+ attempt=1,
249
+ )
250
+ mock_should_retry.assert_called_once_with(self.subscription, 1)
251
+ mock_schedule_retry.assert_called_once_with(
252
+ "http://example.com/webhook",
253
+ {"key": "value"},
254
+ self.subscription.id,
255
+ 1,
125
256
  )
126
257
 
127
- # Verify signatures were created for each webhook
128
- self.assertEqual(mock_async_task.s.call_count, 2)
129
-
130
- # Verify the group was created with the signatures
131
- mock_group_constructor.assert_called_once()
132
- args = mock_group_constructor.call_args[0][0]
133
- self.assertEqual(len(args), 2)
134
- self.assertEqual(args[0], mock_signature)
135
- self.assertEqual(args[1], mock_signature)
136
-
137
- # Verify apply_async was called
138
- mock_group.apply_async.assert_called_once()
139
-
140
- # Verify the result is the group result
141
- self.assertEqual(result, 'group-result')
142
-
143
- @patch('django_webhook_subscriber.tasks.async_deliver_webhook')
144
- def test_process_webhook_batch_empty(self, mock_async_task):
145
- # Mock the group constructor
146
- with patch('celery.group') as mock_group_constructor:
147
- # Call with empty webhook IDs
148
- result = process_webhook_batch([], self.payload, self.event_signal)
149
-
150
- # Verify no signatures were created
151
- mock_async_task.s.assert_not_called()
152
-
153
- # Verify no group was created
154
- mock_group_constructor.assert_not_called()
155
-
156
- # Verify the result is None
157
- self.assertIsNone(result)
158
-
159
- @patch('django_webhook_subscriber.tasks.async_deliver_webhook')
160
- def test_process_webhook_batch_integration(self, mock_async_task):
161
- # Create multiple test webhooks
162
- webhooks = [
163
- self.webhook, # Already created in setUp
164
- WebhookRegistry.objects.create(
165
- name='Second Webhook',
166
- content_type=self.example_content_type,
167
- event_signals=['CREATE'],
168
- endpoint='https://example2.com/webhook',
169
- secret='test-secret-key-2',
170
- ),
171
- WebhookRegistry.objects.create(
172
- name='Third Webhook',
173
- content_type=self.example_content_type,
174
- event_signals=['UPDATE'], # Doesn't match our test event
175
- endpoint='https://example3.com/webhook',
176
- secret='test-secret-key-3',
177
- ),
178
- WebhookRegistry.objects.create(
179
- name='Inactive Webhook',
180
- content_type=self.example_content_type,
181
- event_signals=['CREATE'],
182
- endpoint='https://example4.com/webhook',
183
- secret='test-secret-key-4',
184
- is_active=False,
185
- ),
258
+
259
+ class ProcessWebhookDeliveryBatchTasksTests(TestCase):
260
+ def setUp(self):
261
+ content_type = ContentTypeFactory()
262
+ subscriber = WebhookSubscriberFactory(content_type=content_type)
263
+ self.subscriptions = WebhookSubscriptionFactory.create_batch(
264
+ 3,
265
+ subscriber=subscriber,
266
+ event_name="created",
267
+ )
268
+
269
+ def test_process_webhook_delivery_batch_on_no_subscriptions(self):
270
+ results = tasks.process_webhook_delivery_batch([])
271
+ self.assertIn("processed", results)
272
+ self.assertEqual(results["processed"], 0)
273
+ self.assertIn("error", results)
274
+ self.assertEqual(results["error"], "Empty batch")
275
+
276
+ @patch("django_webhook_subscriber.tasks._validate_subscription_data")
277
+ def test_process_webhook_delivery_batch_on_invalid_subscriptions(
278
+ self, mock_validate
279
+ ):
280
+ mock_validate.return_value = False
281
+ results = tasks.process_webhook_delivery_batch([{"id": 9999}])
282
+ self.assertIn("processed", results)
283
+ self.assertEqual(results["processed"], 0)
284
+ self.assertIn("error", results)
285
+ self.assertEqual(results["error"], "No valid subscriptions")
286
+
287
+ @patch("django_webhook_subscriber.tasks._validate_subscription_data")
288
+ @patch("django_webhook_subscriber.tasks.group")
289
+ @patch("django_webhook_subscriber.tasks.deliver_webhook")
290
+ def test_process_webhook_delivery_batch_on_successful_processing(
291
+ self, mock_deliver, mock_group, mock_validate
292
+ ):
293
+ mock_validate.return_value = True
294
+
295
+ # Mock the group execution to return successful results
296
+ mock_job = mock_group.return_value
297
+ mock_job.apply_async.return_value.get.return_value = [
298
+ {"success": True, "status_code": 200} for _ in self.subscriptions
186
299
  ]
187
300
 
188
- # Mock the signature function
189
- mock_signature = MagicMock()
190
- mock_async_task.s.return_value = mock_signature
301
+ results = tasks.process_webhook_delivery_batch(
302
+ [
303
+ {
304
+ "id": sub.id,
305
+ "url": "https://example.com/webhooks",
306
+ "payload": {"key": "value"},
307
+ }
308
+ for sub in self.subscriptions
309
+ ]
310
+ )
191
311
 
192
- # Mock the group and its apply_async
193
- mock_group_result = MagicMock()
194
- mock_group = MagicMock()
195
- mock_group.apply_async.return_value = mock_group_result
312
+ # Verify signatures were created
313
+ self.assertEqual(mock_deliver.s.call_count, 3)
314
+
315
+ # Verify group was executed
316
+ mock_group.assert_called_once()
317
+ mock_job.apply_async.assert_called_once()
318
+
319
+ # Assert results
320
+ self.assertIn("batch_id", results)
321
+ self.assertIn("task_ids", results)
322
+ self.assertEqual(results["processed"], 3)
323
+ self.assertEqual(results["total_requested"], 3)
324
+
325
+ @patch("django_webhook_subscriber.tasks._validate_subscription_data")
326
+ @patch("django_webhook_subscriber.tasks.group")
327
+ @patch("django_webhook_subscriber.tasks.deliver_webhook")
328
+ def test_process_webhook_delivery_batch_on_exception_during_processing(
329
+ self, mock_deliver, mock_group, mock_validate
330
+ ):
331
+ mock_validate.return_value = True
332
+
333
+ # Mock the group execution to raise an exception
334
+ mock_job = mock_group.return_value
335
+ mock_job.apply_async.side_effect = Exception("Some error!")
336
+
337
+ results = tasks.process_webhook_delivery_batch(
338
+ [
339
+ {
340
+ "id": sub.id,
341
+ "url": "https://example.com/webhooks",
342
+ "payload": {"key": "value"},
343
+ }
344
+ for sub in self.subscriptions
345
+ ]
346
+ )
196
347
 
197
- # Run the test
198
- with patch('celery.group', return_value=mock_group):
199
- # Get IDs of all webhooks
200
- webhook_ids = [webhook.id for webhook in webhooks]
348
+ # Verify signatures were created
349
+ self.assertEqual(mock_deliver.s.call_count, 3)
201
350
 
202
- # Call the batch function
203
- result = process_webhook_batch(
204
- webhook_ids, self.payload, self.event_signal
205
- )
351
+ # Verify group was executed
352
+ mock_group.assert_called_once()
353
+ mock_job.apply_async.assert_called_once()
206
354
 
207
- # Verify the result matches the expected group result
208
- self.assertEqual(result, mock_group_result)
355
+ # Assert results
356
+ self.assertEqual(results["error"], "e=Exception('Some error!')")
357
+ self.assertEqual(results["processed"], 0)
358
+
359
+ def test_validate_subscription_data_on_valid_data(self):
360
+ results = tasks._validate_subscription_data(
361
+ {
362
+ "id": self.subscriptions[0].id,
363
+ "url": "https://example.com",
364
+ "payload": {},
365
+ }
366
+ )
367
+ self.assertTrue(results)
368
+
369
+ def test_validate_subscription_data_on_invalid_data(self):
370
+ results = tasks._validate_subscription_data(
371
+ {
372
+ "id": 9999,
373
+ "payload": {},
374
+ }
375
+ )
376
+ self.assertFalse(results)
209
377
 
210
- # Verify signatures were created for each webhook
211
- self.assertEqual(mock_async_task.s.call_count, len(webhook_ids))
212
378
 
213
- # Verify the correct arguments were passed to each signature
214
- calls = mock_async_task.s.call_args_list
215
- for i, call in enumerate(calls):
216
- args = call[0]
217
- self.assertEqual(args[0], webhook_ids[i])
218
- self.assertEqual(args[1], self.payload)
219
- self.assertEqual(args[2], self.event_signal)
379
+ class TaskMaintenanceTasksTests(TestCase):
380
+ def setUp(self):
381
+ # Default 30 days retention
382
+ self.timestamp_created = timezone.now() - timezone.timedelta(days=30)
383
+ self.log = WebhookDeliveryLogFactory()
384
+ self.subscription = self.log.subscription
385
+ self.subscriber = self.subscription.subscriber
386
+
387
+ def mock_response(self, status_code=200, json_data=None):
388
+ mock_resp = requests.Response()
389
+ mock_resp.status_code = status_code
390
+ mock_resp._content = (
391
+ b'{"key": "value"}' if json_data is None else json_data
392
+ )
393
+ return mock_resp
394
+
395
+ def test_cleanup_webhook_logs_on_empty_query(self):
396
+ results = tasks.cleanup_webhook_logs(30)
397
+ self.assertIn("deleted", results)
398
+ self.assertEqual(results["deleted"], 0)
399
+ self.assertIn("cutoff_date", results)
400
+
401
+ def test_cleanup_webhook_logs_on_successful_deletion(self):
402
+ # Changing created timestamp to older than 30 days
403
+ self.log.created_at = self.timestamp_created
404
+ self.log.save()
405
+ results = tasks.cleanup_webhook_logs(days=1)
406
+ self.assertIn("deleted", results)
407
+ self.assertEqual(results["deleted"], 1)
408
+ self.assertIn("cutoff_date", results)
409
+ # Assert logs are deleted
410
+ remaining_logs = models.WebhookDeliveryLog.objects.count()
411
+ self.assertEqual(remaining_logs, 0)
412
+
413
+ def test_cleanup_webhook_logs_passing_subscription(self):
414
+ # Changing created timestamp to older than 30 days
415
+ self.log.created_at = self.timestamp_created
416
+ self.log.save()
417
+ results = tasks.cleanup_webhook_logs(
418
+ days=1, subscription_id=self.subscription.id
419
+ )
420
+ self.assertIn("deleted", results)
421
+ self.assertEqual(results["deleted"], 1)
422
+ self.assertIn("cutoff_date", results)
423
+ # Assert logs are deleted
424
+ remaining_logs = models.WebhookDeliveryLog.objects.count()
425
+ self.assertEqual(remaining_logs, 0)
426
+
427
+ def test_cleanup_webhook_logs_on_exception_during_cleanup(self):
428
+ # Setting exception raise
429
+ with patch(
430
+ "django_webhook_subscriber."
431
+ "models.WebhookDeliveryLog.objects.filter"
432
+ ) as mock_filter:
433
+ mock_filter.side_effect = Exception("Some error!")
434
+ results = tasks.cleanup_webhook_logs(30)
435
+ self.assertIn("error", results)
436
+ self.assertIn("Some error!", results["error"])
437
+
438
+ def test_webhook_connectivity_check_on_no_subscribers(self):
439
+ results = tasks.test_webhook_connectivity([9999])
440
+ self.assertEqual(results["tested"], 0)
441
+ self.assertEqual(results["results"], [])
442
+ self.assertEqual(results["error"], "No active subscribers found")
443
+
444
+ @patch("django_webhook_subscriber.tasks._test_single_endpoint")
445
+ def test_webhook_connectivity_check_on_successful_check(self, mock_test):
446
+ # Mocking the session.post request to return a successful response
447
+ mock_test.return_value = {
448
+ "subscriber_id": self.subscriber.id,
449
+ "subscriber_name": self.subscriber.name,
450
+ "url": self.subscriber.target_url,
451
+ "success": True,
452
+ "status_code": 400,
453
+ "duration_ms": 123,
454
+ "error": None,
455
+ }
456
+ results = tasks.test_webhook_connectivity([self.subscriber.id])
457
+
458
+ self.assertEqual(results["tested"], 1)
459
+ self.assertEqual(results["successful_responses"], 0)
460
+ self.assertEqual(results["successful"], 1)
461
+ self.assertEqual(results["failed"], 0)
462
+ self.assertIn("results", results)
463
+
464
+ @patch("django_webhook_subscriber.tasks._test_single_endpoint")
465
+ def test_webhook_connectivity_check_on_exception_raise(self, mock_test):
466
+ # Mocking the session.post request to return a successful response
467
+ mock_test.side_effect = Exception("Some error!")
468
+ results = tasks.test_webhook_connectivity([self.subscriber.id])
469
+
470
+ self.assertIn("error", results)
471
+ self.assertIn("Some error!", results["error"])
472
+
473
+ def test_single_endpoint_check_on_successful_check(self):
474
+ session = Mock()
475
+ session.head.return_value = self.mock_response(200)
476
+ result = tasks._test_single_endpoint(session, self.subscriber)
477
+ self.assertEqual(result["subscriber_id"], self.subscriber.id)
478
+ self.assertEqual(result["subscriber_name"], self.subscriber.name)
479
+ self.assertEqual(result["url"], self.subscriber.target_url)
480
+ self.assertTrue(result["success"])
481
+ self.assertEqual(result["status_code"], 200)
482
+ self.assertIn("duration_ms", result)
483
+ self.assertIsNone(result["error"])
484
+
485
+ def test_single_endpoint_check_on_failed_check(self):
486
+ session = Mock()
487
+ session.head.return_value = self.mock_response(500)
488
+ result = tasks._test_single_endpoint(session, self.subscriber)
489
+ self.assertEqual(result["subscriber_id"], self.subscriber.id)
490
+ self.assertEqual(result["subscriber_name"], self.subscriber.name)
491
+ self.assertEqual(result["url"], self.subscriber.target_url)
492
+ self.assertTrue(result["success"])
493
+ self.assertEqual(result["status_code"], 500)
494
+ self.assertIn("duration_ms", result)
495
+ self.assertIsNone(result["error"])
496
+
497
+ def test_single_endpoint_check_on_exception_check(self):
498
+ session = Mock()
499
+ session.head.side_effect = Exception("Some error!")
500
+ result = tasks._test_single_endpoint(session, self.subscriber)
501
+ self.assertEqual(result["subscriber_id"], self.subscriber.id)
502
+ self.assertEqual(result["subscriber_name"], self.subscriber.name)
503
+ self.assertEqual(result["url"], self.subscriber.target_url)
504
+ self.assertFalse(result["success"])
505
+ self.assertIsNone(result["status_code"])
506
+ self.assertIn("duration_ms", result)
507
+ self.assertIn("Some error!", result["error"])