11import json
2+ import logging
23from unittest .mock import patch
34
45
@@ -18,7 +19,10 @@ def test_workflow_job_webhook(self, mock_webhook_service, mock_verify, client, s
1819 '/webhook' ,
1920 data = json .dumps (sample_workflow_job_payload ),
2021 content_type = 'application/json' ,
21- headers = {'X-GitHub-Event' : 'workflow_job' }
22+ headers = {
23+ 'X-GitHub-Event' : 'workflow_job' ,
24+ 'X-GitHub-Delivery' : 'abc-123-def'
25+ }
2226 )
2327
2428 assert response .status_code == 200
@@ -51,7 +55,10 @@ def test_workflow_job_webhook_created_response(self, mock_webhook_service, mock_
5155 '/webhook' ,
5256 data = json .dumps (payload ),
5357 content_type = 'application/json' ,
54- headers = {'X-GitHub-Event' : 'workflow_job' }
58+ headers = {
59+ 'X-GitHub-Event' : 'workflow_job' ,
60+ 'X-GitHub-Delivery' : 'delivery-created-001'
61+ }
5562 )
5663
5764 assert response .status_code == 200
@@ -79,7 +86,10 @@ def test_workflow_job_webhook_deleted_response(self, mock_webhook_service, mock_
7986 '/webhook' ,
8087 data = json .dumps (payload ),
8188 content_type = 'application/json' ,
82- headers = {'X-GitHub-Event' : 'workflow_job' }
89+ headers = {
90+ 'X-GitHub-Event' : 'workflow_job' ,
91+ 'X-GitHub-Delivery' : 'delivery-deleted-001'
92+ }
8393 )
8494
8595 assert response .status_code == 200
@@ -111,7 +121,10 @@ def test_workflow_job_webhook_ignored_response(self, mock_webhook_service, mock_
111121 '/webhook' ,
112122 data = json .dumps (payload ),
113123 content_type = 'application/json' ,
114- headers = {'X-GitHub-Event' : 'workflow_job' }
124+ headers = {
125+ 'X-GitHub-Event' : 'workflow_job' ,
126+ 'X-GitHub-Delivery' : 'delivery-ignored-001'
127+ }
115128 )
116129
117130 assert response .status_code == 200
@@ -128,7 +141,10 @@ def test_installation_webhook(self, mock_verify, client, sample_installation_pay
128141 '/webhook' ,
129142 data = json .dumps (sample_installation_payload ),
130143 content_type = 'application/json' ,
131- headers = {'X-GitHub-Event' : 'installation' }
144+ headers = {
145+ 'X-GitHub-Event' : 'installation' ,
146+ 'X-GitHub-Delivery' : 'delivery-install-001'
147+ }
132148 )
133149
134150 assert response .status_code == 200
@@ -144,7 +160,10 @@ def test_unknown_webhook_event(self, mock_verify, client):
144160 '/webhook' ,
145161 data = json .dumps (payload ),
146162 content_type = 'application/json' ,
147- headers = {'X-GitHub-Event' : 'unknown_event' }
163+ headers = {
164+ 'X-GitHub-Event' : 'unknown_event' ,
165+ 'X-GitHub-Delivery' : 'delivery-unknown-001'
166+ }
148167 )
149168
150169 assert response .status_code == 200
@@ -162,7 +181,10 @@ def test_workflow_job_webhook_error(self, mock_webhook_service, mock_verify, cli
162181 '/webhook' ,
163182 data = json .dumps (sample_workflow_job_payload ),
164183 content_type = 'application/json' ,
165- headers = {'X-GitHub-Event' : 'workflow_job' }
184+ headers = {
185+ 'X-GitHub-Event' : 'workflow_job' ,
186+ 'X-GitHub-Delivery' : 'delivery-error-001'
187+ }
166188 )
167189
168190 assert response .status_code == 500
@@ -181,7 +203,8 @@ def test_webhook_invalid_signature(self, mock_verify, client):
181203 content_type = 'application/json' ,
182204 headers = {
183205 'X-GitHub-Event' : 'workflow_job' ,
184- 'X-Hub-Signature-256' : 'invalid'
206+ 'X-Hub-Signature-256' : 'invalid' ,
207+ 'X-GitHub-Delivery' : 'delivery-sig-001'
185208 }
186209 )
187210
@@ -194,8 +217,168 @@ def test_webhook_ping_event(self, client):
194217 '/webhook' ,
195218 data = json .dumps ({'zen' : 'test' }),
196219 content_type = 'application/json' ,
197- headers = {'X-GitHub-Event' : 'ping' }
220+ headers = {
221+ 'X-GitHub-Event' : 'ping' ,
222+ 'X-GitHub-Delivery' : 'delivery-ping-001'
223+ }
198224 )
199225
200226 assert response .status_code == 200
201227 assert response .json ['status' ] == 'success'
228+
229+
230+ class TestWebhookDeliveryIdLogging :
231+ """Tests to verify that X-GitHub-Delivery header is logged for correlation."""
232+
233+ @patch ('app.routes.webhook.verify_github_signature' )
234+ @patch ('app.routes.webhook.WebhookService' )
235+ def test_delivery_id_logged_on_workflow_job (
236+ self , mock_webhook_service , mock_verify , client , caplog
237+ ):
238+ """Test that delivery_id is logged when processing a workflow_job event."""
239+ mock_verify .return_value = True
240+ mock_service_instance = mock_webhook_service .return_value
241+ mock_service_instance .handle_workflow_job .return_value = {
242+ "action" : "created" ,
243+ "runner_name" : "runner-abc123"
244+ }
245+
246+ payload = {
247+ 'action' : 'queued' ,
248+ 'workflow_job' : {'labels' : ['gcp-ubuntu-24.04' ]},
249+ 'repository' : {
250+ 'html_url' : 'https://github.com/owner/repo' ,
251+ 'full_name' : 'owner/repo'
252+ }
253+ }
254+
255+ with caplog .at_level (logging .INFO , logger = 'app.routes.webhook' ):
256+ client .post (
257+ '/webhook' ,
258+ data = json .dumps (payload ),
259+ content_type = 'application/json' ,
260+ headers = {
261+ 'X-GitHub-Event' : 'workflow_job' ,
262+ 'X-GitHub-Delivery' : 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
263+ }
264+ )
265+
266+ assert any (
267+ 'f47ac10b-58cc-4372-a567-0e02b2c3d479' in record .message
268+ for record in caplog .records
269+ ), "delivery_id was not found in log output"
270+
271+ @patch ('app.routes.webhook.verify_github_signature' )
272+ def test_delivery_id_logged_on_unknown_event (self , mock_verify , client , caplog ):
273+ """Test that delivery_id is logged for unknown event types."""
274+ mock_verify .return_value = True
275+
276+ with caplog .at_level (logging .WARNING , logger = 'app.routes.webhook' ):
277+ client .post (
278+ '/webhook' ,
279+ data = json .dumps ({'action' : 'test' }),
280+ content_type = 'application/json' ,
281+ headers = {
282+ 'X-GitHub-Event' : 'unknown_event' ,
283+ 'X-GitHub-Delivery' : 'aaaabbbb-cccc-dddd-eeee-ffffffffffff'
284+ }
285+ )
286+
287+ assert any (
288+ 'aaaabbbb-cccc-dddd-eeee-ffffffffffff' in record .message
289+ for record in caplog .records
290+ ), "delivery_id was not found in log output for unknown event"
291+
292+ @patch ('app.routes.webhook.verify_github_signature' )
293+ def test_delivery_id_logged_on_invalid_signature (self , mock_verify , client , caplog ):
294+ """Test that delivery_id is logged when signature verification fails."""
295+ mock_verify .return_value = False
296+
297+ with caplog .at_level (logging .ERROR , logger = 'app.routes.webhook' ):
298+ client .post (
299+ '/webhook' ,
300+ data = json .dumps ({'test' : 'data' }),
301+ content_type = 'application/json' ,
302+ headers = {
303+ 'X-GitHub-Event' : 'workflow_job' ,
304+ 'X-Hub-Signature-256' : 'invalid' ,
305+ 'X-GitHub-Delivery' : '11112222-3333-4444-5555-666677778888'
306+ }
307+ )
308+
309+ assert any (
310+ '11112222-3333-4444-5555-666677778888' in record .message
311+ for record in caplog .records
312+ ), "delivery_id was not found in log output for invalid signature"
313+
314+ @patch ('app.routes.webhook.verify_github_signature' )
315+ @patch ('app.routes.webhook.WebhookService' )
316+ def test_delivery_id_logged_on_webhook_error (
317+ self , mock_webhook_service , mock_verify , client , caplog
318+ ):
319+ """Test that delivery_id is logged when webhook processing fails."""
320+ mock_verify .return_value = True
321+ mock_service_instance = mock_webhook_service .return_value
322+ mock_service_instance .handle_workflow_job .side_effect = Exception ("boom" )
323+
324+ payload = {
325+ 'action' : 'queued' ,
326+ 'workflow_job' : {'labels' : ['gcp-ubuntu-24.04' ]},
327+ 'repository' : {
328+ 'html_url' : 'https://github.com/owner/repo' ,
329+ 'full_name' : 'owner/repo'
330+ }
331+ }
332+
333+ with caplog .at_level (logging .ERROR , logger = 'app.routes.webhook' ):
334+ client .post (
335+ '/webhook' ,
336+ data = json .dumps (payload ),
337+ content_type = 'application/json' ,
338+ headers = {
339+ 'X-GitHub-Event' : 'workflow_job' ,
340+ 'X-GitHub-Delivery' : 'error-delivery-id-999'
341+ }
342+ )
343+
344+ assert any (
345+ 'error-delivery-id-999' in record .message
346+ for record in caplog .records
347+ ), "delivery_id was not found in error log output"
348+
349+ @patch ('app.routes.webhook.verify_github_signature' )
350+ @patch ('app.routes.webhook.WebhookService' )
351+ def test_delivery_id_none_when_header_missing (
352+ self , mock_webhook_service , mock_verify , client , caplog
353+ ):
354+ """Test that delivery_id is logged as None when header is missing."""
355+ mock_verify .return_value = True
356+ mock_service_instance = mock_webhook_service .return_value
357+ mock_service_instance .handle_workflow_job .return_value = {
358+ "action" : "created" ,
359+ "runner_name" : "runner-no-delivery"
360+ }
361+
362+ payload = {
363+ 'action' : 'queued' ,
364+ 'workflow_job' : {'labels' : ['gcp-ubuntu-24.04' ]},
365+ 'repository' : {
366+ 'html_url' : 'https://github.com/owner/repo' ,
367+ 'full_name' : 'owner/repo'
368+ }
369+ }
370+
371+ with caplog .at_level (logging .INFO , logger = 'app.routes.webhook' ):
372+ response = client .post (
373+ '/webhook' ,
374+ data = json .dumps (payload ),
375+ content_type = 'application/json' ,
376+ headers = {'X-GitHub-Event' : 'workflow_job' }
377+ )
378+
379+ assert response .status_code == 200
380+ # delivery_id should be None in the log since header was not sent
381+ assert any (
382+ 'delivery_id: None' in record .message
383+ for record in caplog .records
384+ ), "delivery_id: None was not found in log output when header is missing"
0 commit comments