"""Unit tests for WebhookService. TDD Cycle: RED → GREEN → REFACTOR """ from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest from notebooklm_agent.core.exceptions import ( NotFoundError, ValidationError, ) from notebooklm_agent.services.webhook_service import WebhookService @pytest.mark.unit class TestWebhookServiceRegister: """Test suite for webhook registration.""" async def test_register_webhook_returns_webhook(self): """Should register webhook and return it.""" # Arrange service = WebhookService() url = "https://example.com/webhook" events = ["artifact.completed", "source.ready"] secret = "my-secret-key" # Act result = await service.register(url, events, secret) # Assert assert result.url == url assert result.events == events assert result.secret is True assert result.active is True async def test_register_without_secret(self): """Should register webhook without secret.""" # Arrange service = WebhookService() url = "https://example.com/webhook" events = ["artifact.completed"] # Act result = await service.register(url, events) # Assert assert result.secret is False async def test_register_http_url_raises_validation_error(self): """Should raise ValidationError for HTTP URL.""" # Arrange service = WebhookService() url = "http://example.com/webhook" events = ["artifact.completed"] # Act & Assert with pytest.raises(ValidationError) as exc_info: await service.register(url, events) assert "HTTPS" in str(exc_info.value) async def test_register_invalid_event_raises_validation_error(self): """Should raise ValidationError for invalid event.""" # Arrange service = WebhookService() url = "https://example.com/webhook" events = ["invalid.event"] # Act & Assert with pytest.raises(ValidationError) as exc_info: await service.register(url, events) assert "Invalid events" in str(exc_info.value) @pytest.mark.unit class TestWebhookServiceList: """Test suite for listing webhooks.""" async def test_list_returns_empty_list_initially(self): """Should return empty list when no webhooks.""" # Arrange service = WebhookService() # Act result = await service.list() # Assert assert result == [] async def test_list_returns_registered_webhooks(self): """Should return list of registered webhooks.""" # Arrange service = WebhookService() await service.register("https://example.com/webhook1", ["artifact.completed"]) await service.register("https://example.com/webhook2", ["source.ready"]) # Act result = await service.list() # Assert assert len(result) == 2 @pytest.mark.unit class TestWebhookServiceGet: """Test suite for getting webhook.""" async def test_get_returns_webhook(self): """Should return webhook by ID.""" # Arrange service = WebhookService() webhook = await service.register("https://example.com/webhook", ["artifact.completed"]) # Act result = await service.get(str(webhook.id)) # Assert assert result.id == webhook.id async def test_get_not_found_raises_not_found(self): """Should raise NotFoundError if webhook not found.""" # Arrange service = WebhookService() # Act & Assert with pytest.raises(NotFoundError) as exc_info: await service.get("non-existent-id") assert "non-existent-id" in str(exc_info.value) @pytest.mark.unit class TestWebhookServiceDelete: """Test suite for deleting webhooks.""" async def test_delete_removes_webhook(self): """Should delete webhook.""" # Arrange service = WebhookService() webhook = await service.register("https://example.com/webhook", ["artifact.completed"]) # Act await service.delete(str(webhook.id)) # Assert with pytest.raises(NotFoundError): await service.get(str(webhook.id)) async def test_delete_not_found_raises_not_found(self): """Should raise NotFoundError if webhook not found.""" # Arrange service = WebhookService() # Act & Assert with pytest.raises(NotFoundError): await service.delete("non-existent-id") @pytest.mark.unit class TestWebhookServiceSignature: """Test suite for HMAC signature generation.""" def test_generate_signature(self): """Should generate HMAC-SHA256 signature.""" # Arrange service = WebhookService() payload = '{"event": "test"}' secret = "my-secret" # Act signature = service._generate_signature(payload, secret) # Assert assert len(signature) == 64 # SHA256 hex is 64 chars assert all(c in "0123456789abcdef" for c in signature) def test_generate_signature_deterministic(self): """Should generate same signature for same input.""" # Arrange service = WebhookService() payload = '{"event": "test"}' secret = "my-secret" # Act sig1 = service._generate_signature(payload, secret) sig2 = service._generate_signature(payload, secret) # Assert assert sig1 == sig2 @pytest.mark.unit class TestWebhookServiceDispatch: """Test suite for event dispatching.""" async def test_dispatch_event_sends_to_matching_webhooks(self): """Should dispatch event to webhooks subscribed to that event.""" # Arrange service = WebhookService() webhook = await service.register("https://example.com/webhook", ["artifact.completed"]) with patch.object(service, "_send_webhook") as mock_send: mock_send.return_value = True # Act await service.dispatch_event("artifact.completed", {"artifact_id": "123"}) # Assert mock_send.assert_called_once() async def test_dispatch_event_skips_non_matching_webhooks(self): """Should not dispatch to webhooks not subscribed to event.""" # Arrange service = WebhookService() await service.register("https://example.com/webhook", ["source.ready"]) with patch.object(service, "_send_webhook") as mock_send: # Act await service.dispatch_event("artifact.completed", {"artifact_id": "123"}) # Assert mock_send.assert_not_called() @pytest.mark.unit class TestWebhookServiceTest: """Test suite for testing webhooks.""" async def test_webhook_sends_test_event(self): """Should send test event to webhook.""" # Arrange service = WebhookService() webhook = await service.register("https://example.com/webhook", ["artifact.completed"]) with patch.object(service, "_send_webhook") as mock_send: mock_send.return_value = True # Act result = await service.test_webhook(str(webhook.id)) # Assert assert result is True mock_send.assert_called_once() # Check it's a test event call_args = mock_send.call_args assert call_args[0][1] == "webhook.test" async def test_webhook_not_found_raises_not_found(self): """Should raise NotFoundError if webhook not found.""" # Arrange service = WebhookService() # Act & Assert with pytest.raises(NotFoundError): await service.test_webhook("non-existent-id")