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,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
|
-
|
|
13
|
-
self.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
23
|
-
self.
|
|
24
|
-
self.
|
|
25
|
-
self.assertEqual(self.
|
|
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
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
content_type=self.
|
|
52
|
-
|
|
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.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
name=
|
|
69
|
-
|
|
70
|
-
|
|
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.
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
90
|
+
def test_subscriber_model_model_class_property(self):
|
|
88
91
|
self.assertEqual(
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
self.
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
self.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
self.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
#
|
|
169
|
-
self.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
#
|
|
178
|
-
self.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
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.
|
|
203
|
-
|
|
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.
|
|
207
|
-
|
|
195
|
+
self.subscription.content_type,
|
|
196
|
+
self.subscriber.content_type,
|
|
208
197
|
)
|
|
209
198
|
|
|
210
|
-
def
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
self.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
)
|
|
230
|
-
|
|
231
|
-
|
|
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.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
self.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
298
|
-
|
|
379
|
+
self.assertIn(
|
|
380
|
+
"Header value for 'X-Header' must be a string or None",
|
|
381
|
+
str(context.exception),
|
|
382
|
+
)
|
|
299
383
|
|
|
300
|
-
|
|
301
|
-
self.
|
|
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
|
+
)
|