Files
TimeTracker/tests/test_utils/test_webhook_service.py
Dries Peeters a18de04a6a feat: Add webhook system for real-time event notifications
Implement comprehensive webhook system supporting 40+ event types with automatic retries, HMAC signatures, delivery tracking, REST API, and admin UI. Integrates with Activity logging for automatic event triggering.

- Database: Add webhooks and webhook_deliveries tables (migration 046)

- API: Full CRUD endpoints with read:webhooks/write:webhooks scopes

- UI: Admin interface for webhook management and testing

- Service: Automatic retry with exponential backoff every 5 minutes

- Security: HMAC-SHA256 signature verification

- Tests: Model and service tests included

- Docs: Complete integration guide with examples
2025-11-14 13:52:56 +01:00

162 lines
5.3 KiB
Python

"""Tests for webhook service"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from app.models import Webhook, WebhookDelivery, User
from app.utils.webhook_service import WebhookService, WebhookDeliveryError
from app import db
@pytest.fixture
def test_user(db_session):
"""Create a test user"""
user = User(username='testuser', role='admin')
db_session.add(user)
db_session.commit()
return user
@pytest.fixture
def test_webhook(db_session, test_user):
"""Create a test webhook"""
webhook = Webhook(
name='Test Webhook',
url='https://example.com/webhook',
events=['project.created'],
user_id=test_user.id,
is_active=True
)
webhook.set_secret()
db_session.add(webhook)
db_session.commit()
return webhook
class TestWebhookService:
"""Test WebhookService"""
@patch('app.utils.webhook_service.requests.post')
def test_deliver_webhook_success(self, mock_post, db_session, test_webhook):
"""Test successful webhook delivery"""
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = 'OK'
mock_response.headers = {}
mock_post.return_value = mock_response
payload = {'event': 'project.created', 'data': {'id': 1}}
delivery = WebhookService.deliver_webhook(
webhook=test_webhook,
event_type='project.created',
payload=payload
)
assert delivery.status == 'success'
assert delivery.response_status_code == 200
assert test_webhook.successful_deliveries == 1
@patch('app.utils.webhook_service.requests.post')
def test_deliver_webhook_http_error(self, mock_post, db_session, test_webhook):
"""Test webhook delivery with HTTP error"""
mock_response = Mock()
mock_response.status_code = 500
mock_response.text = 'Internal Server Error'
mock_response.headers = {}
mock_post.return_value = mock_response
payload = {'event': 'project.created'}
delivery = WebhookService.deliver_webhook(
webhook=test_webhook,
event_type='project.created',
payload=payload
)
assert delivery.status == 'failed'
assert delivery.response_status_code == 500
assert test_webhook.failed_deliveries == 1
@patch('app.utils.webhook_service.requests.post')
def test_deliver_webhook_timeout(self, mock_post, db_session, test_webhook):
"""Test webhook delivery timeout"""
import requests
mock_post.side_effect = requests.exceptions.Timeout('Request timeout')
payload = {'event': 'project.created'}
delivery = WebhookService.deliver_webhook(
webhook=test_webhook,
event_type='project.created',
payload=payload
)
assert delivery.status == 'failed'
assert delivery.error_type == 'timeout'
assert test_webhook.failed_deliveries == 1
def test_deliver_webhook_inactive(self, db_session, test_webhook):
"""Test delivering to inactive webhook"""
test_webhook.is_active = False
db_session.commit()
payload = {'event': 'project.created'}
with pytest.raises(WebhookDeliveryError):
WebhookService.deliver_webhook(
webhook=test_webhook,
event_type='project.created',
payload=payload
)
def test_deliver_webhook_not_subscribed(self, db_session, test_webhook):
"""Test delivering event webhook doesn't subscribe to"""
payload = {'event': 'project.updated'}
with pytest.raises(WebhookDeliveryError):
WebhookService.deliver_webhook(
webhook=test_webhook,
event_type='project.updated',
payload=payload
)
def test_get_available_events(self):
"""Test getting available events"""
events = WebhookService.get_available_events()
assert isinstance(events, list)
assert len(events) > 0
assert 'project.created' in events
assert 'task.created' in events
@patch('app.utils.webhook_service.requests.post')
def test_retry_failed_deliveries(self, mock_post, db_session, test_webhook):
"""Test retrying failed deliveries"""
from app.utils.timezone import now_in_app_timezone
from datetime import timedelta
# Create a failed delivery scheduled for retry
delivery = WebhookDelivery(
webhook_id=test_webhook.id,
event_type='project.created',
payload='{"test": "data"}',
status='retrying',
next_retry_at=now_in_app_timezone() - timedelta(minutes=1),
retry_count=1
)
db_session.add(delivery)
db_session.commit()
# Mock successful response
mock_response = Mock()
mock_response.status_code = 200
mock_response.text = 'OK'
mock_response.headers = {}
mock_post.return_value = mock_response
retried = WebhookService.retry_failed_deliveries(max_deliveries=10)
assert retried == 1
db_session.refresh(delivery)
assert delivery.status == 'success'