88 "crypto/hmac"
99 "crypto/sha256"
1010 "encoding/hex"
11+ "encoding/json"
12+ "errors"
1113 "fmt"
1214 "io"
1315 "net/http"
@@ -22,6 +24,11 @@ import (
2224 "go.uber.org/zap"
2325)
2426
27+ const (
28+ integrationRunStartedAt = "2023-01-01T00:00:00Z"
29+ integrationRunCompletedAt = "2023-01-01T01:00:00Z"
30+ )
31+
2532func TestIntegrationWebhookMetrics (t * testing.T ) {
2633 testCases := []struct {
2734 name string
@@ -188,6 +195,58 @@ func TestIntegrationDuplicateDeliveryDoesNotInflateMetrics(t *testing.T) {
188195 }
189196}
190197
198+ func TestIntegrationDeliveryStoreFailurePreventsWebhookProcessing (t * testing.T ) {
199+ server := newIntegrationTestServerWithStateStore (t , asyncProcessorConfig {WorkerCount : 1 , QueueSize : 8 }, failingDeliveryStateStore {
200+ inMemoryStateStore : newInMemoryStateStore (),
201+ })
202+ defer server .Close ()
203+
204+ body := mustReadFixture (t , "workflow_run.json" )
205+ resp := sendWebhookRequest (t , server .URL , githubEventWorkflowRun , body , "delivery-store-failure" )
206+ assertResponseStatus (t , resp , http .StatusInternalServerError )
207+
208+ metrics := mustFetchMetrics (t , server .URL )
209+ if strings .Contains (metrics , `promgithub_workflow_status{branch="main",conclusion="success",repository="user/repo",workflow_name="CI",workflow_status="completed"} 1` ) {
210+ t .Fatalf ("workflow metrics changed after delivery store failure:\n %s" , metrics )
211+ }
212+ if strings .Contains (metrics , `promgithub_event_processed_total{event_type="workflow_run"}` ) {
213+ t .Fatalf ("event was enqueued after delivery store failure:\n %s" , metrics )
214+ }
215+ }
216+
217+ func TestIntegrationWorkflowRunLifecycleBalancesStatefulMetrics (t * testing.T ) {
218+ server := newIntegrationTestServer (t )
219+ defer server .Close ()
220+
221+ queuedBody := workflowRunFixtureWithStatus (t , statusQueued , "" , integrationRunStartedAt )
222+ queuedResp := sendWebhookRequest (t , server .URL , githubEventWorkflowRun , queuedBody , "delivery-lifecycle-queued" )
223+ assertResponseStatus (t , queuedResp , http .StatusAccepted )
224+ waitForMetricsSubstring (t , server .URL , `promgithub_workflow_queued{branch="main",repository="user/repo",workflow_name="CI"} 1` )
225+
226+ inProgressBody := workflowRunFixtureWithStatus (t , statusInProgress , "" , integrationRunStartedAt )
227+ inProgressResp := sendWebhookRequest (t , server .URL , githubEventWorkflowRun , inProgressBody , "delivery-lifecycle-in-progress" )
228+ assertResponseStatus (t , inProgressResp , http .StatusAccepted )
229+ waitForMetricsSubstring (t , server .URL , `promgithub_workflow_in_progress{branch="main",repository="user/repo",workflow_name="CI"} 1` )
230+
231+ completedBody := workflowRunFixtureWithStatus (t , statusCompleted , testConclusionSuccess , integrationRunCompletedAt )
232+ completedResp := sendWebhookRequest (t , server .URL , githubEventWorkflowRun , completedBody , "delivery-lifecycle-completed" )
233+ assertResponseStatus (t , completedResp , http .StatusAccepted )
234+
235+ metrics := waitForMetricsSubstring (t , server .URL , `promgithub_workflow_completed{branch="main",repository="user/repo",workflow_conclusion="success",workflow_name="CI"} 1` )
236+ expectedMetrics := []string {
237+ `promgithub_workflow_queued{branch="main",repository="user/repo",workflow_name="CI"} 0` ,
238+ `promgithub_workflow_in_progress{branch="main",repository="user/repo",workflow_name="CI"} 0` ,
239+ `promgithub_workflow_completed{branch="main",repository="user/repo",workflow_conclusion="success",workflow_name="CI"} 1` ,
240+ `promgithub_workflow_duration_sum{branch="main",conclusion="success",repository="user/repo",workflow_name="CI",workflow_status="completed"} 3600` ,
241+ `promgithub_workflow_duration_count{branch="main",conclusion="success",repository="user/repo",workflow_name="CI",workflow_status="completed"} 1` ,
242+ }
243+ for _ , expected := range expectedMetrics {
244+ if ! strings .Contains (metrics , expected ) {
245+ t .Fatalf ("expected metrics to contain %q, got:\n %s" , expected , metrics )
246+ }
247+ }
248+ }
249+
191250func TestIntegrationAsyncQueueFullReturnsUnavailableAndExposesQueueDropMetrics (t * testing.T ) {
192251 server := newIntegrationTestServerWithAsyncConfig (t , asyncProcessorConfig {WorkerCount : 1 , QueueSize : 1 })
193252 defer server .Close ()
@@ -315,11 +374,16 @@ func newIntegrationTestServer(t *testing.T) *httptest.Server {
315374}
316375
317376func newIntegrationTestServerWithAsyncConfig (t * testing.T , cfg asyncProcessorConfig ) * httptest.Server {
377+ t .Helper ()
378+ return newIntegrationTestServerWithStateStore (t , cfg , newInMemoryStateStore ())
379+ }
380+
381+ func newIntegrationTestServerWithStateStore (t * testing.T , cfg asyncProcessorConfig , store StateStore ) * httptest.Server {
318382 t .Helper ()
319383 resetIntegrationTestMetrics ()
320384
321385 githubWebhookSecret = []byte ("integration-test-secret" )
322- stateStore = newInMemoryStateStore ()
386+ stateStore = store
323387 eventProcessor = newAsyncEventProcessor (cfg , zap .NewNop ())
324388 eventProcessor .Start ()
325389 t .Cleanup (func () {
@@ -334,6 +398,14 @@ func newIntegrationTestServerWithAsyncConfig(t *testing.T, cfg asyncProcessorCon
334398 return httptest .NewServer (router )
335399}
336400
401+ type failingDeliveryStateStore struct {
402+ * inMemoryStateStore
403+ }
404+
405+ func (s failingDeliveryStateStore ) MarkDeliveryProcessed (context.Context , string ) (bool , error ) {
406+ return false , errors .New ("delivery store unavailable" )
407+ }
408+
337409func assertResponseStatus (t * testing.T , resp * http.Response , expectedStatus int ) {
338410 t .Helper ()
339411 defer func () { _ = resp .Body .Close () }()
@@ -390,6 +462,24 @@ func mustReadFixture(t *testing.T, name string) []byte {
390462 return body
391463}
392464
465+ func workflowRunFixtureWithStatus (t * testing.T , status , conclusion , updatedAt string ) []byte {
466+ t .Helper ()
467+
468+ var payload GithubWorkflow
469+ if err := json .Unmarshal (mustReadFixture (t , "workflow_run.json" ), & payload ); err != nil {
470+ t .Fatalf ("failed to unmarshal workflow fixture: %v" , err )
471+ }
472+ payload .Workflow .Status = status
473+ payload .Workflow .Conclusion = conclusion
474+ payload .Workflow .UpdatedAt = updatedAt
475+
476+ body , err := json .Marshal (payload )
477+ if err != nil {
478+ t .Fatalf ("failed to marshal workflow fixture: %v" , err )
479+ }
480+ return body
481+ }
482+
393483func sendWebhookRequest (t * testing.T , serverURL , eventType string , body []byte , deliveryID string ) * http.Response {
394484 t .Helper ()
395485 signature := webhookSignature (body , githubWebhookSecret )
0 commit comments