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,301 +1,391 @@
1
+ from django.core.exceptions import ValidationError
1
2
  from django.test import TestCase
2
- from django.contrib.contenttypes.models import ContentType
3
- from django.utils import timezone
4
- from django.contrib.auth.models import User
5
3
 
6
- from django_webhook_subscriber import models
4
+ from django_webhook_subscriber import managers, models, validators
7
5
 
6
+ from .factories import (
7
+ ContentTypeFactory,
8
+ WebhookSubscriberFactory,
9
+ WebhookSubscriptionFactory,
10
+ )
8
11
 
9
- class WebhookModelTests(TestCase):
10
12
 
13
+ class WebhookSubscriberModelTests(TestCase):
11
14
  def setUp(self):
12
- # Get content type for User model for testing
13
- self.user_content_type = ContentType.objects.get_for_model(User)
14
-
15
- # Create a basic webhook for testing
16
- self.webhook = models.WebhookRegistry.objects.create(
17
- name='Test Webhook',
18
- content_type=self.user_content_type,
19
- endpoint='https://example.com/webhook',
15
+ self.content_type = ContentTypeFactory()
16
+ self.subscriber = models.WebhookSubscriber.objects.create(
17
+ name="Test Subscriber",
18
+ target_url="https://example.com/webhook",
19
+ secret="supersecret",
20
+ content_type=self.content_type,
20
21
  )
21
22
 
22
- def test_webhook_creation(self):
23
- self.assertEqual(self.webhook.name, 'Test Webhook')
24
- self.assertEqual(self.webhook.content_type, self.user_content_type)
25
- self.assertEqual(self.webhook.endpoint, 'https://example.com/webhook')
26
- self.assertEqual(self.webhook.event_signals, [])
27
- self.assertTrue(self.webhook.is_active)
28
- self.assertTrue(self.webhook.secret)
29
- self.assertEqual(self.webhook.headers, {})
30
-
31
- def test_webhook_string_representation(self):
23
+ def test_subscriber_model_is_created_successfully(self):
24
+ self.subscriber.full_clean()
25
+ self.assertIsNotNone(self.subscriber.id)
26
+ self.assertEqual(self.subscriber.name, "Test Subscriber")
32
27
  self.assertEqual(
33
- str(self.webhook), 'Test Webhook - https://example.com/webhook'
28
+ self.subscriber.target_url,
29
+ "https://example.com/webhook",
34
30
  )
31
+ self.assertEqual(self.subscriber.secret, "supersecret")
32
+ self.assertEqual(self.subscriber.content_type, self.content_type)
33
+ # Asserting Default Values
34
+ self.assertTrue(self.subscriber.is_active)
35
+ self.assertEqual(self.subscriber.description, "")
36
+ self.assertEqual(self.subscriber.headers, {})
37
+ self.assertIsNone(self.subscriber.last_success)
38
+ self.assertIsNone(self.subscriber.last_failure)
39
+ self.assertEqual(self.subscriber.max_retries, 3)
40
+ self.assertEqual(self.subscriber.retry_delay, 60)
41
+ # Asserting Meta Fields
42
+ self.assertIsNotNone(self.subscriber.created_at)
43
+ self.assertIsNotNone(self.subscriber.updated_at)
44
+
45
+ def test_subscriber_model_unique_together_constraint(self):
46
+ with self.assertRaises(Exception) as context:
47
+ models.WebhookSubscriber.objects.create(
48
+ name="Duplicate Subscriber",
49
+ content_type=self.content_type,
50
+ target_url="https://example.com/webhook",
51
+ secret="anothersecret",
52
+ )
53
+ self.assertIn("already exists", str(context.exception).lower())
35
54
 
36
- def test_webhook_with_headers(self):
37
- custom_headers = {'Authorization': 'Bearer token123'}
38
- webhook = models.WebhookRegistry.objects.create(
39
- name='Webhook with Headers',
40
- content_type=self.user_content_type,
41
- event_signals=['DELETE'],
42
- endpoint='https://example.com/webhook2',
43
- headers=custom_headers,
55
+ def test_subscriber_model_str_representation(self):
56
+ self.assertEqual(
57
+ str(self.subscriber),
58
+ "Test Subscriber (https://example.com/webhook)",
44
59
  )
45
- self.assertEqual(webhook.headers, custom_headers)
46
-
47
- def test_event_signals_jsonfield(self):
48
- # Test with a single event type
49
- webhook = models.WebhookRegistry.objects.create(
50
- name='Single Event Webhook',
51
- content_type=self.user_content_type,
52
- event_signals=['CREATE'],
53
- endpoint='https://example.com/create-only',
60
+
61
+ def test_subscriber_model_clean_method_validates_timeout(self):
62
+ invalid_subscriber = models.WebhookSubscriber(
63
+ name="Invalid Timeout Subscriber",
64
+ target_url="https://example.com/webhook",
65
+ secret="secret",
66
+ content_type=self.content_type,
67
+ timeout=301, # Invalid timeout (> 300)
54
68
  )
55
- self.assertEqual(webhook.event_signals, ['CREATE'])
56
-
57
- # Test with multiple event types
58
- webhook = models.WebhookRegistry.objects.create(
59
- name='All Events Webhook',
60
- content_type=self.user_content_type,
61
- event_signals=['CREATE', 'UPDATE', 'DELETE'],
62
- endpoint='https://example.com/all-events',
69
+ with self.assertRaises(Exception) as context:
70
+ invalid_subscriber.full_clean()
71
+ self.assertIn(
72
+ "timeout cannot exceed 300 seconds", str(context.exception).lower()
63
73
  )
64
- self.assertEqual(webhook.event_signals, ['CREATE', 'UPDATE', 'DELETE'])
65
74
 
66
- # Test with empty list (should use default)
67
- webhook = models.WebhookRegistry.objects.create(
68
- name='Empty Events Webhook',
69
- content_type=self.user_content_type,
70
- endpoint='https://example.com/no-events',
75
+ def test_subscriber_model_clean_method_validates_retry_delay(self):
76
+ invalid_subscriber = models.WebhookSubscriber(
77
+ name="Invalid Timeout Subscriber",
78
+ target_url="https://example.com/webhook",
79
+ secret="secret",
80
+ content_type=self.content_type,
81
+ retry_delay=3601, # Invalid timeout (> 1 hour)
71
82
  )
72
- self.assertEqual(webhook.event_signals, [])
73
-
74
- def test_updating_webhook(self):
75
- # Update several fields
76
- self.webhook.name = 'Updated Webhook'
77
- self.webhook.endpoint = 'https://updated-example.com/webhook'
78
- self.webhook.event_signals = ['CREATE', 'UPDATE']
79
- self.webhook.is_active = False
80
- self.webhook.save()
81
-
82
- # Retrieve the webhook from the database again to verify updates
83
- updated_webhook = models.WebhookRegistry.objects.get(
84
- pk=self.webhook.pk
83
+ with self.assertRaises(Exception) as context:
84
+ invalid_subscriber.full_clean()
85
+ self.assertIn(
86
+ "retry_delay cannot exceed 3600 seconds",
87
+ str(context.exception).lower(),
85
88
  )
86
89
 
87
- self.assertEqual(updated_webhook.name, 'Updated Webhook')
90
+ def test_subscriber_model_model_class_property(self):
88
91
  self.assertEqual(
89
- updated_webhook.endpoint,
90
- 'https://updated-example.com/webhook',
92
+ self.subscriber.model_class,
93
+ self.content_type.model_class(),
91
94
  )
95
+
96
+ def test_subscriber_model_model_name_property(self):
92
97
  self.assertEqual(
93
- updated_webhook.event_signals,
94
- ['CREATE', 'UPDATE'],
95
- )
96
- self.assertFalse(updated_webhook.is_active)
97
-
98
- def test_tracking_fields(self):
99
- # Initially, tracking fields should be None
100
- self.assertTrue(self.webhook.keep_last_response)
101
- self.assertIsNone(self.webhook.last_response)
102
- self.assertIsNone(self.webhook.last_success)
103
- self.assertIsNone(self.webhook.last_failure)
104
-
105
- # Update tracking fields
106
- now = timezone.now()
107
- self.webhook.last_response = 'Success: 200 OK'
108
- self.webhook.last_success = now
109
- self.webhook.save()
110
-
111
- # Retrieve the webhook and verify updates
112
- updated_webhook = models.WebhookRegistry.objects.get(
113
- pk=self.webhook.pk
98
+ self.subscriber.model_name,
99
+ f"{self.content_type.app_label}.{self.content_type.model}",
114
100
  )
115
- self.assertEqual(updated_webhook.last_response, 'Success: 200 OK')
116
- self.assertEqual(updated_webhook.last_success.date(), now.date())
117
- self.assertIsNone(updated_webhook.last_failure)
118
-
119
- # Update failure tracking
120
- self.webhook.last_response = 'Error: 500 Internal Server Error'
121
- self.webhook.last_failure = now
122
- self.webhook.save()
123
-
124
- # Retrieve and verify again
125
- updated_webhook = models.WebhookRegistry.objects.get(
126
- pk=self.webhook.pk
101
+
102
+ def test_subscriber_model_record_success_method(self):
103
+ self.subscriber.record_success()
104
+ self.subscriber.refresh_from_db()
105
+ self.assertIsNotNone(self.subscriber.last_success)
106
+ self.assertIsNone(self.subscriber.last_failure)
107
+
108
+ def test_subscriber_model_record_failure_method(self):
109
+ self.subscriber.record_failure()
110
+ self.subscriber.refresh_from_db()
111
+ self.assertIsNotNone(self.subscriber.last_failure)
112
+ self.assertIsNone(self.subscriber.last_success)
113
+ # Test consecutive failures increment
114
+ self.subscriber.record_failure()
115
+ self.subscriber.refresh_from_db()
116
+ self.assertEqual(self.subscriber.consecutive_failures, 2)
117
+ self.subscriber.auto_disable_after_failures = 3
118
+ self.subscriber.record_failure()
119
+ self.subscriber.refresh_from_db()
120
+ self.assertFalse(self.subscriber.is_active)
121
+ self.assertEqual(self.subscriber.consecutive_failures, 3)
122
+
123
+
124
+ class WebhookSubscriptionModelTests(TestCase):
125
+ def setUp(self):
126
+ self.subscriber = WebhookSubscriberFactory(name="Test Subscriber")
127
+ self.subscription = models.WebhookSubscription.objects.create(
128
+ subscriber=self.subscriber,
129
+ event_name="created",
127
130
  )
131
+
132
+ def test_subscription_model_is_created_successfully(self):
133
+ self.subscription.full_clean()
134
+ self.assertIsNotNone(self.subscription.id)
135
+ self.assertEqual(self.subscription.subscriber, self.subscriber)
136
+ self.assertEqual(self.subscription.event_name, "created")
137
+ # Asserting Default Values
138
+ self.assertEqual(self.subscription.custom_endpoint, "")
139
+ self.assertTrue(self.subscription.is_active)
140
+ # Asserting Meta Fields
141
+ self.assertIsNotNone(self.subscription.created_at)
142
+ self.assertIsNotNone(self.subscription.updated_at)
143
+
144
+ def test_subscription_model_str_representation(self):
128
145
  self.assertEqual(
129
- updated_webhook.last_response,
130
- 'Error: 500 Internal Server Error',
146
+ str(self.subscription),
147
+ "Test Subscriber - created",
131
148
  )
132
- self.assertEqual(updated_webhook.last_failure.date(), now.date())
133
-
134
- def test_async_delivery_settings(self):
135
- # Default values
136
- self.assertEqual(self.webhook.max_retries, 3)
137
- self.assertEqual(self.webhook.retry_delay, 60)
138
- self.assertIsNone(self.webhook.use_async)
139
-
140
- # Update async settings
141
- self.webhook.max_retries = 5
142
- self.webhook.retry_delay = 120
143
- self.webhook.use_async = True
144
- self.webhook.save()
145
-
146
- # Retrieve and verify
147
- updated_webhook = models.WebhookRegistry.objects.get(
148
- pk=self.webhook.pk
149
- )
150
- self.assertEqual(updated_webhook.max_retries, 5)
151
- self.assertEqual(updated_webhook.retry_delay, 120)
152
- self.assertTrue(updated_webhook.use_async)
153
149
 
150
+ def test_subscription_model_unique_together_constraint(self):
151
+ with self.assertRaises(Exception) as context:
152
+ models.WebhookSubscription.objects.create(
153
+ subscriber=self.subscriber,
154
+ event_name="created",
155
+ )
156
+ self.assertIn("unique constraint", str(context.exception).lower())
157
+
158
+ def test_subscription_model_clean_method_validates_event_type(self):
159
+ pass
154
160
 
155
- class WebhookDeliveryLogModelTests(TestCase):
156
- def setUp(self):
157
- # Get content type for User model for testing
158
- self.user_content_type = ContentType.objects.get_for_model(User)
159
-
160
- # Create a webhook for testing
161
- self.webhook = models.WebhookRegistry.objects.create(
162
- name='Test Webhook',
163
- content_type=self.user_content_type,
164
- event_signals=['CREATE', 'UPDATE'],
165
- endpoint='https://example.com/webhook',
161
+ def test_subscription_model_endpoint_property(self):
162
+ self.subscriber.target_url = "https://example.com/webhook/"
163
+ self.subscriber.save()
164
+ self.subscription.custom_endpoint = "/events/create"
165
+ self.subscription.full_clean()
166
+ self.assertEqual(
167
+ self.subscription.endpoint,
168
+ "https://example.com/webhook/events/create",
166
169
  )
167
170
 
168
- # Create a successful log entry
169
- self.success_log = models.WebhookDeliveryLog.objects.create(
170
- webhook=self.webhook,
171
- event_signal='created',
172
- payload={'id': 1, 'username': 'testuser', 'event': 'created'},
173
- response_status=200,
174
- response_body='OK',
171
+ # Remove endpoint
172
+ self.subscription.custom_endpoint = ""
173
+ self.subscription.save()
174
+ self.assertEqual(
175
+ self.subscription.endpoint,
176
+ "https://example.com/webhook/",
175
177
  )
176
178
 
177
- # Create a failed log entry
178
- self.failed_log = models.WebhookDeliveryLog.objects.create(
179
- webhook=self.webhook,
180
- event_signal='updated',
181
- payload={'id': 1, 'username': 'testuser', 'event': 'updated'},
182
- response_status=500,
183
- response_body='Internal Server Error',
184
- error_message='Server returned 500 status code',
179
+ # Full endpoint
180
+ self.subscription.custom_endpoint = "https://another.com/hook"
181
+ self.subscription.full_clean()
182
+ self.assertEqual(
183
+ self.subscription.endpoint,
184
+ "https://another.com/hook",
185
185
  )
186
186
 
187
- def test_log_creation(self):
188
- # Verify the successful log entry
189
- self.assertEqual(self.success_log.webhook, self.webhook)
190
- self.assertEqual(self.success_log.event_signal, 'created')
191
- self.assertEqual(self.success_log.payload['username'], 'testuser')
192
- self.assertEqual(self.success_log.response_status, 200)
193
- self.assertEqual(self.success_log.response_body, 'OK')
194
- self.assertIsNone(self.success_log.error_message)
195
-
196
- # Verify the failed log entry
197
- self.assertEqual(self.failed_log.webhook, self.webhook)
198
- self.assertEqual(self.failed_log.event_signal, 'updated')
199
- self.assertEqual(self.failed_log.payload['username'], 'testuser')
200
- self.assertEqual(self.failed_log.response_status, 500)
187
+ def test_subscription_model_model_name_property(self):
201
188
  self.assertEqual(
202
- self.failed_log.response_body,
203
- 'Internal Server Error',
189
+ self.subscription.model_name,
190
+ self.subscriber.model_name,
204
191
  )
192
+
193
+ def test_subscription_model_content_type_property(self):
205
194
  self.assertEqual(
206
- self.failed_log.error_message,
207
- 'Server returned 500 status code',
195
+ self.subscription.content_type,
196
+ self.subscriber.content_type,
208
197
  )
209
198
 
210
- def test_string_representation(self):
211
- # For successful delivery
212
- expected_success_str = (
213
- f'{self.webhook} - {self.success_log.event_signal} - 200'
199
+ def test_subscription_model_serializer_class_property(self):
200
+ self.assertEqual(
201
+ self.subscription.serializer_class,
202
+ self.subscriber.serializer_class,
214
203
  )
215
- self.assertEqual(str(self.success_log), expected_success_str)
216
204
 
217
- # For failed delivery
218
- expected_failed_str = (
219
- f'{self.webhook} - {self.failed_log.event_signal} - 500'
220
- )
221
- self.assertEqual(str(self.failed_log), expected_failed_str)
222
-
223
- # For log with no status (completely failed)
224
- no_status_log = models.WebhookDeliveryLog.objects.create(
225
- webhook=self.webhook,
226
- event_signal='deleted',
227
- payload={'id': 1, 'username': 'testuser', 'event': 'deleted'},
228
- error_message='Connection refused',
229
- )
230
- expected_no_status_str = (
231
- f'{self.webhook} - {no_status_log.event_signal} - Failed'
205
+ def test_subscription_model_success_rate_property(self):
206
+ self.assertIsNone(self.subscription.success_rate)
207
+ self.subscription.total_deliveries = 10
208
+ self.subscription.successful_deliveries = 7
209
+ self.subscription.save()
210
+ self.assertEqual(self.subscription.success_rate, 70.0)
211
+ self.subscription.successful_deliveries = 10
212
+ self.subscription.save()
213
+ self.assertEqual(self.subscription.success_rate, 100.0)
214
+
215
+ def test_subscription_model_record_delivery_attempt_method(self):
216
+ self.subscription.record_delivery_attempt(success=True)
217
+ self.subscription.refresh_from_db()
218
+ self.assertEqual(self.subscription.total_deliveries, 1)
219
+ self.assertEqual(self.subscription.successful_deliveries, 1)
220
+ self.assertEqual(self.subscription.consecutive_failures, 0)
221
+
222
+ self.subscription.record_delivery_attempt(
223
+ success=False, response_text=str("s") * 1025
232
224
  )
233
- self.assertEqual(str(no_status_log), expected_no_status_str)
234
-
235
- def test_json_payload_handling(self):
236
- # Test with an empty payload
237
- empty_payload_log = models.WebhookDeliveryLog.objects.create(
238
- webhook=self.webhook,
239
- event_signal='updated',
240
- payload={},
241
- response_status=200,
225
+ self.subscription.refresh_from_db()
226
+ self.assertEqual(self.subscription.total_deliveries, 2)
227
+ self.assertEqual(self.subscription.successful_deliveries, 1)
228
+ self.assertEqual(self.subscription.consecutive_failures, 1)
229
+ self.assertEqual(len(self.subscription.last_response), 1024)
230
+
231
+
232
+ class WebhookDeliveryLogsModelTests(TestCase):
233
+ def setUp(self):
234
+ self.subscription = WebhookSubscriptionFactory(event_name="created")
235
+ self.log = models.WebhookDeliveryLog.objects.create(
236
+ subscription=self.subscription,
237
+ payload={"key": "value"},
238
+ delivery_url=self.subscription.endpoint,
242
239
  )
243
- self.assertEqual(empty_payload_log.payload, {})
244
-
245
- # Test with a nested payload
246
- nested_payload_log = models.WebhookDeliveryLog.objects.create(
247
- webhook=self.webhook,
248
- event_signal='created',
249
- payload={
250
- 'user': {
251
- 'id': 5,
252
- 'username': 'nesteduser',
253
- 'profile': {'age': 30, 'interests': ['python', 'django']},
254
- },
255
- 'timestamp': '2023-01-01T12:00:00Z',
256
- },
257
- response_status=200,
240
+
241
+ def test_delivery_log_model_is_created_successfully(self):
242
+ self.log.full_clean()
243
+ self.assertIsNotNone(self.log.id)
244
+ self.assertEqual(self.log.subscription, self.subscription)
245
+ self.assertEqual(self.log.payload, {"key": "value"})
246
+ self.assertIsNotNone(self.log.delivery_url, self.subscription.endpoint)
247
+ # Asserting Default Values
248
+ self.assertIsNone(self.log.response_status)
249
+ self.assertEqual(self.log.response_body, "")
250
+ self.assertEqual(self.log.response_headers, {})
251
+ self.assertEqual(self.log.error_message, "")
252
+ # Asserting Meta Fields
253
+ self.assertIsNotNone(self.log.created_at)
254
+
255
+ def test_delivery_log_model_str_representation(self):
256
+ self.assertEqual(str(self.log), f"{self.subscription}")
257
+ self.log.response_status = 200
258
+ self.assertEqual(str(self.log), f"{self.subscription} (200)")
259
+ self.log.is_retry = True
260
+ self.log.attempt_number = 2
261
+ self.assertEqual(str(self.log), f"{self.subscription} (200) (retry 2)")
262
+
263
+ def test_delivery_log_model_uses_custom_manager(self):
264
+ self.assertIsInstance(
265
+ models.WebhookDeliveryLog.objects,
266
+ managers.WebhookDeliveryLogManager,
258
267
  )
259
268
 
260
- # Verify nested data is preserved
261
- payload = nested_payload_log.payload
262
- self.assertEqual(payload['user']['username'], 'nesteduser')
263
- self.assertEqual(payload['user']['profile']['age'], 30)
264
- self.assertEqual(payload['user']['profile']['interests'][0], 'python')
265
-
266
- # Test with array in payload
267
- array_payload_log = models.WebhookDeliveryLog.objects.create(
268
- webhook=self.webhook,
269
- event_signal='created',
270
- payload=[
271
- {'id': 1, 'name': 'Item 1'},
272
- {'id': 2, 'name': 'Item 2'},
273
- {'id': 3, 'name': 'Item 3'},
274
- ],
275
- response_status=200,
269
+ def test_delivery_log_model_event_name_property(self):
270
+ self.assertEqual(self.log.event_name, "created")
271
+
272
+ def test_delivery_log_model_is_success_property(self):
273
+ self.assertFalse(self.log.is_success)
274
+ self.log.response_status = 200
275
+ self.assertTrue(self.log.is_success)
276
+ self.log.response_status = 404
277
+ self.assertFalse(self.log.is_success)
278
+ self.log.error_message = "Timeout"
279
+ self.log.response_status = 200
280
+ self.assertFalse(self.log.is_success) # Exceptions are not successes
281
+
282
+ def test_delivery_log_model_is_client_error_property(self):
283
+ self.assertFalse(self.log.is_client_error)
284
+ self.log.response_status = 404
285
+ self.assertTrue(self.log.is_client_error)
286
+ self.log.response_status = 500
287
+ self.assertFalse(self.log.is_client_error)
288
+ self.log.error_message = "Timeout"
289
+ self.assertFalse(
290
+ self.log.is_client_error
291
+ ) # Exceptions are not client errors
292
+
293
+ def test_delivery_log_model_is_server_error_property(self):
294
+ self.assertFalse(self.log.is_server_error)
295
+ self.log.response_status = 500
296
+ self.assertTrue(self.log.is_server_error)
297
+ self.log.response_status = 404
298
+ self.assertFalse(self.log.is_server_error)
299
+ self.log.error_message = "Timeout"
300
+ self.assertFalse(
301
+ self.log.is_server_error
302
+ ) # Exceptions are not server errors
303
+
304
+
305
+ class ValidateClassPathValidatorTests(TestCase):
306
+ def test_validate_class_path_on_valid_path(self):
307
+ try:
308
+ validators.validate_class_path(
309
+ "rest_framework.serializers.ModelSerializer"
310
+ )
311
+ except ValidationError:
312
+ self.fail(
313
+ "validate_class_path raised ValidationError unexpectedly!"
314
+ )
315
+
316
+ def test_validate_class_path_on_empty_value(self):
317
+ try:
318
+ validators.validate_class_path("")
319
+ except ValidationError:
320
+ self.fail(
321
+ "validate_class_path raised ValidationError unexpectedly!"
322
+ )
323
+
324
+ def test_validate_class_path_on_invalid_path(self):
325
+ with self.assertRaises(ValidationError) as context:
326
+ validators.validate_class_path("non.existent.ClassName")
327
+
328
+ self.assertIn("Cannot import class from path", str(context.exception))
329
+
330
+ def test_validate_class_path_on_non_serializer_class(self):
331
+ with self.assertRaises(ValueError) as context:
332
+ validators.validate_class_path(
333
+ "django.core.validators.URLValidator"
334
+ )
335
+
336
+ self.assertIn(
337
+ "field_serializer must be a subclass of",
338
+ str(context.exception),
276
339
  )
277
340
 
278
- # Verify array is preserved
279
- payload = array_payload_log.payload
280
- self.assertEqual(len(payload), 3)
281
- self.assertEqual(payload[0]['name'], 'Item 1')
282
- self.assertEqual(payload[2]['id'], 3)
283
-
284
- def test_multiple_logs_for_webhook(self):
285
- # Create several more logs for the same webhook
286
- for i in range(5):
287
- models.WebhookDeliveryLog.objects.create(
288
- webhook=self.webhook,
289
- event_signal='created',
290
- payload={'id': i + 10, 'name': f'User {i}'},
291
- response_status=200,
341
+
342
+ class ValidateHeadersValidatorTests(TestCase):
343
+ def test_validate_headers_on_valid_dict(self):
344
+ try:
345
+ validators.validate_headers(
346
+ {
347
+ "Content-Type": "application/json",
348
+ "X-Custom-Header": "Value",
349
+ "X-Empty-Header": None,
350
+ }
292
351
  )
352
+ except ValidationError:
353
+ self.fail("validate_headers raised ValidationError unexpectedly!")
354
+
355
+ def test_validate_headers_on_empty_dict(self):
356
+ try:
357
+ validators.validate_headers({})
358
+ except ValidationError:
359
+ self.fail("validate_headers raised ValidationError unexpectedly!")
360
+
361
+ def test_validate_headers_on_non_dict(self):
362
+ with self.assertRaises(ValidationError) as context:
363
+ validators.validate_headers(["Not", "a", "dict"])
364
+
365
+ self.assertIn("Headers must be a dictionary", str(context.exception))
366
+
367
+ def test_validate_headers_on_non_string_key(self):
368
+ with self.assertRaises(ValidationError) as context:
369
+ validators.validate_headers({123: "value"})
293
370
 
294
- # Verify through the related_name
295
- webhook_logs = self.webhook.delivery_logs.all()
371
+ self.assertIn(
372
+ "Header key '123' must be a string", str(context.exception)
373
+ )
374
+
375
+ def test_validate_headers_on_non_string_value(self):
376
+ with self.assertRaises(ValidationError) as context:
377
+ validators.validate_headers({"X-Header": 456})
296
378
 
297
- # Should have 7 logs total (2 from setUp + 5 new ones)
298
- self.assertEqual(webhook_logs.count(), 7)
379
+ self.assertIn(
380
+ "Header value for 'X-Header' must be a string or None",
381
+ str(context.exception),
382
+ )
299
383
 
300
- # Verify newest log is first in the queryset
301
- self.assertEqual(webhook_logs.first().payload['id'], 14)
384
+ def test_validate_headers_on_invalid_header_name_format(self):
385
+ with self.assertRaises(ValidationError) as context:
386
+ validators.validate_headers({"Invalid Header!": "value"})
387
+
388
+ self.assertIn(
389
+ "Invalid header name format: 'Invalid Header!'",
390
+ str(context.exception),
391
+ )