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,73 +1,451 @@
|
|
|
1
1
|
"""Tasks for Django Webhook Subscriber."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
from datetime import timedelta
|
|
6
|
+
|
|
7
|
+
import requests
|
|
8
|
+
from celery import group, shared_task
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
|
|
11
|
+
from .conf import rest_webhook_settings
|
|
12
|
+
from .http import webhook_session
|
|
13
|
+
from .utils import generate_headers
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# =============================================================================
|
|
19
|
+
# Webhook delivery task with retry logic
|
|
20
|
+
# =============================================================================
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@shared_task(bind=True)
|
|
24
|
+
def deliver_webhook(self, url, payload, subscription_id, attempt=1, **kwargs):
|
|
25
|
+
"""Deliver a webhook with proper retry logic and error handling."""
|
|
26
|
+
|
|
27
|
+
from .models import WebhookDeliveryLog, WebhookSubscription
|
|
28
|
+
|
|
29
|
+
# Load subscription with error handling
|
|
30
|
+
try:
|
|
31
|
+
subscription = WebhookSubscription.objects.select_related(
|
|
32
|
+
"subscriber"
|
|
33
|
+
).get(id=subscription_id)
|
|
34
|
+
|
|
35
|
+
except WebhookSubscription.DoesNotExist:
|
|
36
|
+
logger.error(f"Subscription {subscription_id} not found")
|
|
37
|
+
return {"error": "Subscription not found"}
|
|
38
|
+
|
|
39
|
+
# Check if subscription is still active
|
|
40
|
+
if not subscription.is_active or not subscription.subscriber.is_active:
|
|
41
|
+
logger.info(
|
|
42
|
+
f"Skipping delivery for inactive subscription {subscription_id}"
|
|
43
|
+
)
|
|
44
|
+
return {"skipped": "Subscription inactive"}
|
|
45
|
+
|
|
46
|
+
start_time = time.time()
|
|
47
|
+
|
|
48
|
+
# Create delivery log entry
|
|
49
|
+
log = WebhookDeliveryLog.objects.create(
|
|
50
|
+
subscription=subscription,
|
|
51
|
+
payload=payload,
|
|
52
|
+
delivery_url=url,
|
|
53
|
+
attempt_number=attempt,
|
|
54
|
+
is_retry=bool(attempt > 1),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
with webhook_session() as session:
|
|
59
|
+
# Generate headers for this specific request
|
|
60
|
+
headers = generate_headers(subscription.subscriber)
|
|
61
|
+
|
|
62
|
+
# Make the HTTP request
|
|
63
|
+
response = session.post(
|
|
64
|
+
url,
|
|
65
|
+
json=payload,
|
|
66
|
+
headers=headers,
|
|
67
|
+
timeout=subscription.subscriber.timeout,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
# Calculate delivery duration
|
|
71
|
+
delivery_duration = int((time.time() - start_time) * 1000)
|
|
72
|
+
|
|
73
|
+
# Update log with response data
|
|
74
|
+
log.response_status = response.status_code
|
|
75
|
+
log.response_body = response.text[:10240] # Limit size
|
|
76
|
+
log.response_headers = dict(response.headers)
|
|
77
|
+
log.delivery_duration_ms = delivery_duration
|
|
78
|
+
log.save()
|
|
79
|
+
|
|
80
|
+
# Determine if delivery was successful
|
|
81
|
+
is_success = 200 <= response.status_code < 300
|
|
82
|
+
|
|
83
|
+
subscription.record_delivery_attempt(
|
|
84
|
+
success=is_success,
|
|
85
|
+
response_text=response.content,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Update subscriber stats
|
|
89
|
+
if is_success:
|
|
90
|
+
subscription.subscriber.record_success()
|
|
91
|
+
logger.info(
|
|
92
|
+
f"Webhook delivered successfully to {url} "
|
|
93
|
+
f"(attempt {attempt}, {delivery_duration}ms)"
|
|
94
|
+
)
|
|
95
|
+
return {
|
|
96
|
+
"success": True,
|
|
97
|
+
"status_code": response.status_code,
|
|
98
|
+
"duration_ms": delivery_duration,
|
|
99
|
+
"attempt": attempt,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
else:
|
|
103
|
+
# Handle HTTP error responses
|
|
104
|
+
subscription.subscriber.record_failure()
|
|
105
|
+
logger.warning(
|
|
106
|
+
f"Webhook delivery failed: {response.status_code} "
|
|
107
|
+
f"for {url} (attempt {attempt})"
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
should_retry = _should_retry_delivery(
|
|
111
|
+
subscription, attempt, response.status_code
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
if should_retry:
|
|
115
|
+
_schedule_retry(url, payload, subscription_id, attempt)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
"success": False,
|
|
119
|
+
"status_code": response.status_code,
|
|
120
|
+
"duration_ms": delivery_duration,
|
|
121
|
+
"attempt": attempt,
|
|
122
|
+
"will_retry": should_retry,
|
|
123
|
+
}
|
|
124
|
+
except requests.exceptions.Timeout as e:
|
|
125
|
+
error_msg = f"Timeout after {subscription.subscriber.timeout}s"
|
|
126
|
+
_handle_delivery_exception(
|
|
127
|
+
log, subscription, error_msg, url, payload, attempt
|
|
128
|
+
)
|
|
129
|
+
return {"error": f"Timeout error: {e=}", "attempt": attempt}
|
|
130
|
+
|
|
131
|
+
except requests.exceptions.ConnectionError as e:
|
|
132
|
+
error_msg = f"Connection error: {e=}"
|
|
133
|
+
_handle_delivery_exception(
|
|
134
|
+
log, subscription, error_msg, url, payload, attempt
|
|
135
|
+
)
|
|
136
|
+
return {"error": f"Connection error: {e=}", "attempt": attempt}
|
|
137
|
+
|
|
138
|
+
except Exception as e:
|
|
139
|
+
error_msg = f"Unexpected error: {e=}"
|
|
140
|
+
_handle_delivery_exception(
|
|
141
|
+
log, subscription, error_msg, url, payload, attempt
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
logger.error(
|
|
145
|
+
f"Unexpected error delivering webhook: {e=}",
|
|
146
|
+
exc_info=True,
|
|
147
|
+
)
|
|
148
|
+
return {"error": f"Unexpected error: {e=}", "attempt": attempt}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _should_retry_delivery(subscription, attempt, status_code=None):
|
|
152
|
+
"""Determine if a delivery should be retried."""
|
|
153
|
+
|
|
154
|
+
# Don't retry if max attempts reached
|
|
155
|
+
if attempt >= subscription.subscriber.max_retries + 1:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# Don't retry client errors (4xx) - these won't succeed on retry
|
|
159
|
+
if status_code and 400 <= status_code < 500:
|
|
160
|
+
logger.info(f"Not retrying client error {status_code}")
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
# Retry server errors (5xx) and network issues
|
|
164
|
+
return True
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _schedule_retry(url, payload, subscription_id, current_attempt):
|
|
168
|
+
"""Schedule a retry for a failed delivery."""
|
|
169
|
+
|
|
170
|
+
from .models import WebhookSubscription
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
subscription = WebhookSubscription.objects.get(id=subscription_id)
|
|
174
|
+
retry_delay = subscription.subscriber.retry_delay
|
|
175
|
+
next_attempt = current_attempt + 1
|
|
176
|
+
|
|
177
|
+
logger.info(
|
|
178
|
+
f"Scheduling retry {next_attempt} for subscription "
|
|
179
|
+
f"{subscription_id} in {retry_delay} seconds"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
deliver_webhook.apply_async(
|
|
183
|
+
args=(url, payload, subscription_id),
|
|
184
|
+
kwargs={"attempt": next_attempt},
|
|
185
|
+
countdown=retry_delay,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Failed to schedule retry: {e=}")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _handle_delivery_exception(
|
|
193
|
+
log, subscription, error_msg, url, payload, attempt
|
|
194
|
+
):
|
|
195
|
+
"""Handle exceptions during delivery."""
|
|
196
|
+
|
|
197
|
+
# Update log with error details
|
|
198
|
+
log.error_message = error_msg
|
|
199
|
+
log.save()
|
|
200
|
+
|
|
201
|
+
# Update subscription and subscriber
|
|
202
|
+
subscription.record_delivery_attempt(success=False)
|
|
203
|
+
subscription.subscriber.record_failure()
|
|
204
|
+
|
|
205
|
+
logger.error(f"Webhook delivery error: {error_msg}")
|
|
206
|
+
|
|
207
|
+
# schedule retry if appropriate
|
|
208
|
+
should_retry = _should_retry_delivery(subscription, attempt)
|
|
209
|
+
if should_retry:
|
|
210
|
+
_schedule_retry(url, payload, subscription.id, attempt)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# =============================================================================
|
|
214
|
+
# Batch processing task
|
|
215
|
+
# =============================================================================
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
@shared_task
|
|
219
|
+
def process_webhook_delivery_batch(subscriptions):
|
|
220
|
+
if not subscriptions:
|
|
221
|
+
logger.warning("Empty subscription batch received")
|
|
222
|
+
return {"processed": 0, "error": "Empty batch"}
|
|
26
223
|
|
|
27
224
|
try:
|
|
28
|
-
|
|
225
|
+
batch_size = len(subscriptions)
|
|
226
|
+
batch_id = f"batch_{timezone.now().strftime('%Y%m%d_%H%M%S')}"
|
|
227
|
+
|
|
228
|
+
logger.info(
|
|
229
|
+
f"Processing webhook batch {batch_id} with {batch_size} deliveries"
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# Validate subscriptions before processing
|
|
233
|
+
valid_subscriptions = []
|
|
234
|
+
for sub in subscriptions:
|
|
235
|
+
if _validate_subscription_data(sub):
|
|
236
|
+
valid_subscriptions.append(sub)
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
logger.warning(f"Invalid subscription data: {sub}")
|
|
240
|
+
|
|
241
|
+
if not valid_subscriptions:
|
|
242
|
+
logger.error("No valid subscriptions in batch")
|
|
243
|
+
return {"processed": 0, "error": "No valid subscriptions"}
|
|
244
|
+
|
|
245
|
+
# Create delivery tasks
|
|
246
|
+
delivery_tasks = []
|
|
247
|
+
for sub in valid_subscriptions:
|
|
248
|
+
delivery_tasks.append(
|
|
249
|
+
deliver_webhook.s(
|
|
250
|
+
url=sub["url"],
|
|
251
|
+
payload=sub["payload"],
|
|
252
|
+
subscription_id=sub["id"],
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Execute tasks in parallel with error handling
|
|
257
|
+
job = group(delivery_tasks)
|
|
258
|
+
result = job.apply_async()
|
|
29
259
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
self.default_retry_delay = webhook.retry_delay
|
|
260
|
+
logger.info(
|
|
261
|
+
f"Batch {batch_id} queued successfully with {len(delivery_tasks)} "
|
|
262
|
+
"tasks"
|
|
263
|
+
)
|
|
35
264
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
265
|
+
return {
|
|
266
|
+
"batch_id": batch_id,
|
|
267
|
+
"processed": len(delivery_tasks),
|
|
268
|
+
"total_requested": batch_size,
|
|
269
|
+
"task_ids": (
|
|
270
|
+
[task.id for task in delivery_tasks]
|
|
271
|
+
if hasattr(result, "children")
|
|
272
|
+
else []
|
|
273
|
+
),
|
|
274
|
+
}
|
|
40
275
|
|
|
41
|
-
|
|
276
|
+
except Exception as e:
|
|
277
|
+
logger.error(
|
|
278
|
+
f"Error processing webhook delivery batch: {e=}", exc_info=True
|
|
279
|
+
)
|
|
280
|
+
return {"processed": 0, "error": f"{e=}"}
|
|
42
281
|
|
|
43
|
-
return delivery_log.id
|
|
44
282
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
283
|
+
def _validate_subscription_data(subscription):
|
|
284
|
+
"""Validate subscription data before processing."""
|
|
285
|
+
|
|
286
|
+
required_fields = ["id", "url", "payload"]
|
|
287
|
+
for field in required_fields:
|
|
288
|
+
if field not in subscription:
|
|
289
|
+
logger.warning(
|
|
290
|
+
f"Missing required field '{field}' in subscription data"
|
|
291
|
+
)
|
|
292
|
+
return False
|
|
293
|
+
|
|
294
|
+
return True
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# =============================================================================
|
|
298
|
+
# Cleanup and maintenance tasks
|
|
299
|
+
# =============================================================================
|
|
51
300
|
|
|
52
301
|
|
|
53
302
|
@shared_task
|
|
54
|
-
def
|
|
55
|
-
"""
|
|
303
|
+
def cleanup_webhook_logs(subscription_id=None, days=None):
|
|
304
|
+
"""Cleanup old webhook delivery logs."""
|
|
305
|
+
from .models import WebhookDeliveryLog
|
|
306
|
+
|
|
307
|
+
try:
|
|
308
|
+
# Get retention period
|
|
309
|
+
retention_days = days or rest_webhook_settings.LOG_RETENTION_DAYS
|
|
310
|
+
cutoff_date = timezone.now() - timedelta(days=retention_days)
|
|
311
|
+
|
|
312
|
+
# Build query
|
|
313
|
+
query = WebhookDeliveryLog.objects.filter(created_at__lt=cutoff_date)
|
|
314
|
+
|
|
315
|
+
if subscription_id:
|
|
316
|
+
query = query.filter(subscription_id=subscription_id)
|
|
317
|
+
|
|
318
|
+
# Count before deletion
|
|
319
|
+
count = query.count()
|
|
320
|
+
|
|
321
|
+
if count == 0:
|
|
322
|
+
logger.info("No old webhook logs to clean up")
|
|
323
|
+
return {"deleted": 0, "cutoff_date": cutoff_date.isoformat()}
|
|
324
|
+
|
|
325
|
+
# Delete in batches to avoid memory issues
|
|
326
|
+
batch_size = 1000
|
|
327
|
+
total_deleted = 0
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
batch_ids = list(query.values_list("id", flat=True)[:batch_size])
|
|
331
|
+
if not batch_ids:
|
|
332
|
+
break
|
|
333
|
+
|
|
334
|
+
deleted_count = WebhookDeliveryLog.objects.filter(
|
|
335
|
+
id__in=batch_ids
|
|
336
|
+
).delete()[0]
|
|
337
|
+
total_deleted += deleted_count
|
|
338
|
+
|
|
339
|
+
logger.info(
|
|
340
|
+
f"Deleted {deleted_count} webhook logs (total: "
|
|
341
|
+
f"{total_deleted})"
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
"deleted": total_deleted,
|
|
346
|
+
"cutoff_date": cutoff_date.isoformat(),
|
|
347
|
+
"retention_days": retention_days,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
except Exception as e:
|
|
351
|
+
logger.error(f"Error during webhook log cleanup: {e=}", exc_info=True)
|
|
352
|
+
return {"error": f"{e=}"}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@shared_task
|
|
356
|
+
def test_webhook_connectivity(subscriber_ids=None):
|
|
357
|
+
"""Test connectivity to webhook endpoints."""
|
|
358
|
+
|
|
359
|
+
from .models import WebhookSubscriber
|
|
360
|
+
|
|
361
|
+
try:
|
|
362
|
+
# Get subscribers to test
|
|
363
|
+
query = WebhookSubscriber.objects.filter(is_active=True)
|
|
364
|
+
if subscriber_ids:
|
|
365
|
+
query = query.filter(id__in=subscriber_ids)
|
|
366
|
+
|
|
367
|
+
subscribers = list(query)
|
|
368
|
+
|
|
369
|
+
if not subscribers:
|
|
370
|
+
return {
|
|
371
|
+
"tested": 0,
|
|
372
|
+
"results": [],
|
|
373
|
+
"error": "No active subscribers found",
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
results = []
|
|
377
|
+
|
|
378
|
+
with webhook_session() as session:
|
|
379
|
+
for subscriber in subscribers:
|
|
380
|
+
result = _test_single_endpoint(session, subscriber)
|
|
381
|
+
results.append(result)
|
|
382
|
+
|
|
383
|
+
success_count = sum(1 for r in results if r["success"])
|
|
384
|
+
|
|
385
|
+
logger.info(
|
|
386
|
+
"Connectivity test completed: "
|
|
387
|
+
f"{success_count}/{len(subscribers)} successful"
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
return {
|
|
391
|
+
"tested": len(results),
|
|
392
|
+
"successful": success_count,
|
|
393
|
+
"successful_responses": sum(
|
|
394
|
+
1
|
|
395
|
+
for r in results
|
|
396
|
+
if r["success"]
|
|
397
|
+
and r["status_code"]
|
|
398
|
+
and 200 <= r["status_code"] < 300
|
|
399
|
+
),
|
|
400
|
+
"failed": len(results) - success_count,
|
|
401
|
+
"results": results,
|
|
402
|
+
}
|
|
403
|
+
except Exception as e:
|
|
404
|
+
logger.error(f"Error during connectivity test: {e=}", exc_info=True)
|
|
405
|
+
return {"error": f"{e=}"}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def _test_single_endpoint(session, subscriber):
|
|
409
|
+
"""Test connectivity to a single subscriber endpoint."""
|
|
410
|
+
|
|
411
|
+
start_time = time.time()
|
|
412
|
+
|
|
413
|
+
try:
|
|
414
|
+
# Use HEAD request for testing (less intrusive)
|
|
415
|
+
headers = generate_headers(subscriber)
|
|
416
|
+
|
|
417
|
+
response = session.head(
|
|
418
|
+
subscriber.target_url,
|
|
419
|
+
headers=headers,
|
|
420
|
+
timeout=min(subscriber.timeout, 10),
|
|
421
|
+
)
|
|
56
422
|
|
|
57
|
-
|
|
58
|
-
to be delivered. It will create a group of tasks for each webhook
|
|
59
|
-
and deliver them asynchronously.
|
|
60
|
-
"""
|
|
423
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
61
424
|
|
|
62
|
-
|
|
425
|
+
return {
|
|
426
|
+
"subscriber_id": subscriber.id,
|
|
427
|
+
"subscriber_name": subscriber.name,
|
|
428
|
+
"url": subscriber.target_url,
|
|
429
|
+
"success": True,
|
|
430
|
+
"status_code": response.status_code,
|
|
431
|
+
"duration_ms": duration_ms,
|
|
432
|
+
"error": None,
|
|
433
|
+
}
|
|
63
434
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
for webhook_id in webhook_ids
|
|
67
|
-
]
|
|
435
|
+
except Exception as e:
|
|
436
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
68
437
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
438
|
+
logger.error(
|
|
439
|
+
f"Error testing webhook endpoint {subscriber.id}: {e=}",
|
|
440
|
+
exc_info=True,
|
|
441
|
+
)
|
|
72
442
|
|
|
73
|
-
|
|
443
|
+
return {
|
|
444
|
+
"subscriber_id": subscriber.id,
|
|
445
|
+
"subscriber_name": subscriber.name,
|
|
446
|
+
"url": subscriber.target_url,
|
|
447
|
+
"success": False,
|
|
448
|
+
"status_code": None,
|
|
449
|
+
"duration_ms": duration_ms,
|
|
450
|
+
"error": f"{e=}",
|
|
451
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import factory
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ContentTypeFactory(factory.django.DjangoModelFactory):
|
|
5
|
+
class Meta:
|
|
6
|
+
model = "contenttypes.ContentType"
|
|
7
|
+
django_get_or_create = ("app_label", "model")
|
|
8
|
+
|
|
9
|
+
app_label = factory.Faker("word")
|
|
10
|
+
model = factory.Faker("word")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class WebhookSubscriberFactory(factory.django.DjangoModelFactory):
|
|
14
|
+
class Meta:
|
|
15
|
+
model = "django_webhook_subscriber.WebhookSubscriber"
|
|
16
|
+
django_get_or_create = ("target_url",)
|
|
17
|
+
|
|
18
|
+
name = factory.Faker("company")
|
|
19
|
+
description = factory.Faker("sentence")
|
|
20
|
+
content_type = factory.SubFactory(ContentTypeFactory)
|
|
21
|
+
target_url = factory.Faker("url")
|
|
22
|
+
secret = factory.Faker("sha256")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class WebhookSubscriptionFactory(factory.django.DjangoModelFactory):
|
|
26
|
+
class Meta:
|
|
27
|
+
model = "django_webhook_subscriber.WebhookSubscription"
|
|
28
|
+
django_get_or_create = ("subscriber", "event_name")
|
|
29
|
+
|
|
30
|
+
subscriber = factory.SubFactory(WebhookSubscriberFactory)
|
|
31
|
+
event_name = factory.Faker("word")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class WebhookDeliveryLogFactory(factory.django.DjangoModelFactory):
|
|
35
|
+
class Meta:
|
|
36
|
+
model = "django_webhook_subscriber.WebhookDeliveryLog"
|
|
37
|
+
|
|
38
|
+
subscription = factory.SubFactory(WebhookSubscriptionFactory)
|
|
39
|
+
payload = factory.Faker("json")
|
|
40
|
+
delivery_url = factory.Faker("url")
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
SECRET_KEY =
|
|
1
|
+
SECRET_KEY = "test_secret_key"
|
|
2
2
|
INSTALLED_APPS = [
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"django.contrib.contenttypes",
|
|
4
|
+
"django.contrib.auth",
|
|
5
|
+
"django.contrib.sessions",
|
|
6
|
+
"django_webhook_subscriber",
|
|
7
7
|
]
|
|
8
8
|
|
|
9
9
|
DATABASES = {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
"default": {
|
|
11
|
+
"ENGINE": "django.db.backends.sqlite3",
|
|
12
|
+
"NAME": ":memory:",
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -17,3 +17,22 @@ USE_TZ = True
|
|
|
17
17
|
|
|
18
18
|
CELERY_TASK_ALWAYS_EAGER = True # Celery executes tasks synchronously
|
|
19
19
|
CELERY_TASK_EAGER_PROPAGATES = True # Propagate exceptions to the caller
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Disabling logging for tests
|
|
23
|
+
LOGGING = {
|
|
24
|
+
"version": 1,
|
|
25
|
+
"disable_existing_loggers": True,
|
|
26
|
+
"handlers": {
|
|
27
|
+
"null": {
|
|
28
|
+
"class": "logging.NullHandler",
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
"loggers": {
|
|
32
|
+
"django_webhook_subscriber": {
|
|
33
|
+
"handlers": ["null"],
|
|
34
|
+
"level": "DEBUG",
|
|
35
|
+
"propagate": False,
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|