1+ import asyncio
2+ import json
3+ import os
4+ import pytest
5+ from unittest .mock import AsyncMock , MagicMock , patch
6+
7+ from fastapi .testclient import TestClient
8+
9+ from webhook_relay .common .config import (
10+ AWSSQSConfig ,
11+ CollectorConfig ,
12+ ForwarderConfig ,
13+ GCPPubSubConfig ,
14+ QueueType ,
15+ )
16+ from webhook_relay .common .models import QueueMessage , WebhookMetadata , WebhookPayload
17+ from webhook_relay .common .queue import QueueClient
18+ from webhook_relay .collector .server import create_app
19+
20+
21+ class MockQueueClient (QueueClient ):
22+ """Mock implementation of QueueClient for testing."""
23+
24+ def __init__ (self ):
25+ self .sent_messages = []
26+ self .available_messages = []
27+ self .deleted_messages = []
28+
29+ # Create mocks that we can use to override behavior in tests
30+ self ._send_message_mock = MagicMock (side_effect = self ._send_message_impl )
31+ self ._receive_message_mock = MagicMock (side_effect = self ._receive_message_impl )
32+ self ._delete_message_mock = MagicMock (side_effect = self ._delete_message_impl )
33+
34+ async def send_message (self , payload ):
35+ """Implementation of abstract method that delegates to a mockable method."""
36+ return await self ._send_message_mock (payload )
37+
38+ async def receive_message (self ):
39+ """Implementation of abstract method that delegates to a mockable method."""
40+ return await self ._receive_message_mock ()
41+
42+ async def delete_message (self , message_id ):
43+ """Implementation of abstract method that delegates to a mockable method."""
44+ return await self ._delete_message_mock (message_id )
45+
46+ async def _send_message_impl (self , payload ):
47+ """Actual implementation for send_message."""
48+ message_id = f"mock-message-{ len (self .sent_messages )} "
49+ self .sent_messages .append ((message_id , payload ))
50+ return message_id
51+
52+ async def _receive_message_impl (self ):
53+ """Actual implementation for receive_message."""
54+ if not self .available_messages :
55+ return None
56+ return self .available_messages .pop (0 )
57+
58+ async def _delete_message_impl (self , message_id ):
59+ """Actual implementation for delete_message."""
60+ self .deleted_messages .append (message_id )
61+ return True
62+
63+ def add_message_to_queue (self , message ):
64+ """Helper method to add messages to the mock queue."""
65+ self .available_messages .append (message )
66+
67+
68+ @pytest .fixture
69+ def mock_queue_client ():
70+ """Fixture that provides a mock queue client."""
71+ return MockQueueClient ()
72+
73+
74+ @pytest .fixture
75+ def sample_webhook_payload ():
76+ """Fixture that provides a sample webhook payload."""
77+ metadata = WebhookMetadata (
78+ source = "github" ,
79+ headers = {"X-GitHub-Event" : "push" },
80+ signature = "sha256=abc123" ,
81+ )
82+ content = {
83+ "repository" : {"name" : "test-repo" , "full_name" : "owner/test-repo" },
84+ "ref" : "refs/heads/main" ,
85+ "commits" : [
86+ {
87+ "id" : "123456" ,
88+ "message" : "Test commit" ,
89+ "author" : {
"name" :
"Test User" ,
"email" :
"[email protected] " },
90+ }
91+ ],
92+ }
93+ return WebhookPayload (metadata = metadata , content = content )
94+
95+
96+ @pytest .fixture
97+ def sample_queue_message (sample_webhook_payload ):
98+ """Fixture that provides a sample queue message."""
99+ return QueueMessage (
100+ id = "test-message-id" ,
101+ payload = sample_webhook_payload ,
102+ attempts = 0 ,
103+ )
104+
105+
106+ @pytest .fixture
107+ def gcp_config ():
108+ """Fixture that provides a sample GCP configuration."""
109+ return GCPPubSubConfig (
110+ project_id = "test-project" ,
111+ topic_id = "test-topic" ,
112+ subscription_id = "test-subscription" ,
113+ )
114+
115+
116+ @pytest .fixture
117+ def aws_config ():
118+ """Fixture that provides a sample AWS configuration."""
119+ return AWSSQSConfig (
120+ region_name = "us-west-2" ,
121+ queue_url = "https://sqs.us-west-2.amazonaws.com/123456789012/test-queue" ,
122+ )
123+
124+
125+ @pytest .fixture
126+ def collector_config ():
127+ """Fixture that provides a sample collector configuration."""
128+ return CollectorConfig (
129+ host = "0.0.0.0" ,
130+ port = 8000 ,
131+ log_level = "INFO" ,
132+ queue_type = QueueType .GCP_PUBSUB ,
133+ gcp_config = GCPPubSubConfig (
134+ project_id = "test-project" ,
135+ topic_id = "test-topic" ,
136+ ),
137+ webhook_sources = [
138+ {"name" : "github" , "secret" : "test-secret" , "signature_header" : "X-Hub-Signature-256" },
139+ {"name" : "gitlab" , "secret" : "test-secret" , "signature_header" : "X-Gitlab-Token" },
140+ {"name" : "custom" }, # No signature verification
141+ ],
142+ )
143+
144+
145+ @pytest .fixture
146+ def forwarder_config ():
147+ """Fixture that provides a sample forwarder configuration."""
148+ return ForwarderConfig (
149+ log_level = "INFO" ,
150+ queue_type = QueueType .GCP_PUBSUB ,
151+ gcp_config = GCPPubSubConfig (
152+ project_id = "test-project" ,
153+ topic_id = "test-topic" ,
154+ subscription_id = "test-subscription" ,
155+ ),
156+ target_url = "http://internal-service:8080/webhook" ,
157+ headers = {"X-Webhook-Relay" : "true" , "Authorization" : "Bearer test-token" },
158+ retry_attempts = 3 ,
159+ retry_delay = 1 ,
160+ timeout = 5 ,
161+ )
162+
163+
164+ @pytest .fixture
165+ def collector_app (collector_config , mock_queue_client ):
166+ """Fixture that provides a configured collector FastAPI app."""
167+ with patch ("webhook_relay.collector.app.get_app_config" ) as mock_get_config , \
168+ patch ("webhook_relay.collector.app.get_queue_client" ) as mock_get_queue :
169+ mock_get_config .return_value = collector_config
170+ mock_get_queue .return_value = mock_queue_client
171+ app = create_app (collector_config )
172+ yield app
173+
174+
175+ @pytest .fixture
176+ def collector_client (collector_app ):
177+ """Fixture that provides a test client for the collector API."""
178+ return TestClient (collector_app )
179+
180+
181+ # Define a fixture to provide an async event loop for testing asynchronous functions
182+ @pytest .fixture
183+ def event_loop ():
184+ loop = asyncio .get_event_loop_policy ().new_event_loop ()
185+ yield loop
186+ loop .close ()
0 commit comments