@@ -11,8 +11,11 @@ import (
1111
1212 "github.com/github/gh-aw-mcpg/internal/difc"
1313 "github.com/github/gh-aw-mcpg/internal/guard"
14+ "github.com/github/gh-aw-mcpg/internal/tracing"
1415 "github.com/stretchr/testify/assert"
1516 "github.com/stretchr/testify/require"
17+ sdktrace "go.opentelemetry.io/otel/sdk/trace"
18+ "go.opentelemetry.io/otel/sdk/trace/tracetest"
1619)
1720
1821// stubGuard is a configurable test double for guard.Guard.
@@ -536,3 +539,123 @@ func tagsAsStrings(tags []difc.Tag) []string {
536539 }
537540 return result
538541}
542+
543+ // newRecordingProxyHandler creates a proxyHandler with an in-memory recording tracer
544+ // injected. The returned function flushes and returns all recorded spans.
545+ func newRecordingProxyHandler (t * testing.T , s * Server ) (* proxyHandler , func () []tracetest.SpanStub ) {
546+ t .Helper ()
547+ exporter := tracetest .NewInMemoryExporter ()
548+ sp := sdktrace .NewSimpleSpanProcessor (exporter )
549+ tp := sdktrace .NewTracerProvider (
550+ sdktrace .WithSpanProcessor (sp ),
551+ sdktrace .WithSampler (sdktrace .AlwaysSample ()),
552+ )
553+ t .Cleanup (func () { _ = tp .Shutdown (t .Context ()) })
554+ h := & proxyHandler {
555+ server : s ,
556+ CachedTracer : tracing.CachedTracer {Tracer : tp .Tracer ("test" )},
557+ }
558+ return h , func () []tracetest.SpanStub { return exporter .GetSpans () }
559+ }
560+
561+ // spanByName returns the first recorded span with the given name, or nil.
562+ func spanByName (spans []tracetest.SpanStub , name string ) * tracetest.SpanStub {
563+ for i := range spans {
564+ if spans [i ].Name == name {
565+ return & spans [i ]
566+ }
567+ }
568+ return nil
569+ }
570+
571+ // ─── Span error recording: guard not initialized → 503 ───────────────────────
572+
573+ func TestHandleWithDIFC_GuardNotInitialized_SetsSpanError (t * testing.T ) {
574+ s := & Server {
575+ guard : guard .NewNoopGuard (),
576+ DIFCComponents : difc.DIFCComponents {
577+ Mode : difc .EnforcementFilter ,
578+ Evaluator : difc .NewEvaluatorWithMode (difc .EnforcementFilter ),
579+ AgentRegistry : difc .NewAgentRegistryWithDefaults (nil , nil ),
580+ Capabilities : difc .NewCapabilities (),
581+ },
582+ githubAPIURL : "http://unused" ,
583+ httpClient : & http.Client {},
584+ guardInitialized : false ,
585+ }
586+ h , getSpans := newRecordingProxyHandler (t , s )
587+
588+ req := httptest .NewRequest (http .MethodGet , "/repos/org/repo/issues" , nil )
589+ w := httptest .NewRecorder ()
590+ h .handleWithDIFC (w , req , "/repos/org/repo/issues" , "list_issues" ,
591+ map [string ]interface {}{}, nil )
592+
593+ assert .Equal (t , http .StatusServiceUnavailable , w .Code )
594+
595+ spans := getSpans ()
596+ difcSpan := spanByName (spans , "proxy.difc_pipeline" )
597+ require .NotNil (t , difcSpan , "proxy.difc_pipeline span must be recorded" )
598+ assert .Equal (t , "Error" , difcSpan .Status .Code .String (), "difc span status must be Error" )
599+ assert .Equal (t , "proxy enforcement not configured" , difcSpan .Status .Description )
600+ require .NotEmpty (t , difcSpan .Events , "difc span must have exception event" )
601+ assert .Equal (t , "exception" , difcSpan .Events [0 ].Name )
602+ }
603+
604+ // ─── Span error recording: RunPipelinePrePhases failure → 502 ────────────────
605+
606+ func TestHandleWithDIFC_LabelResourceError_SetsSpanError (t * testing.T ) {
607+ upstream := mockUpstream (t , http .StatusOK , []interface {}{map [string ]interface {}{"id" : 1 }})
608+ defer upstream .Close ()
609+
610+ g := & stubGuard {
611+ labelResourceErr : errors .New ("guard unavailable" ),
612+ }
613+ s := newTestServerWithStub (t , upstream .URL , g , difc .EnforcementFilter )
614+ h , getSpans := newRecordingProxyHandler (t , s )
615+
616+ req := httptest .NewRequest (http .MethodGet , "/repos/org/repo/issues" , nil )
617+ w := httptest .NewRecorder ()
618+ h .handleWithDIFC (w , req , "/repos/org/repo/issues" , "list_issues" ,
619+ map [string ]interface {}{"owner" : "org" , "repo" : "repo" }, nil )
620+
621+ assert .Equal (t , http .StatusBadGateway , w .Code )
622+
623+ spans := getSpans ()
624+ difcSpan := spanByName (spans , "proxy.difc_pipeline" )
625+ require .NotNil (t , difcSpan , "proxy.difc_pipeline span must be recorded" )
626+ assert .Equal (t , "Error" , difcSpan .Status .Code .String (), "difc span status must be Error" )
627+ assert .Equal (t , "resource labeling failed" , difcSpan .Status .Description )
628+ require .NotEmpty (t , difcSpan .Events , "difc span must have exception event" )
629+ assert .Equal (t , "exception" , difcSpan .Events [0 ].Name )
630+ }
631+
632+ // ─── Span error recording: upstream forwarding failure → 502 ─────────────────
633+
634+ func TestHandleWithDIFC_UpstreamFailure_SetsSpanError (t * testing.T ) {
635+ // Point at a port that refuses connections so forwardAndReadBody returns nil.
636+ g := & stubGuard {
637+ labelResourceResult : publicResource (),
638+ labelResourceOp : difc .OperationRead ,
639+ }
640+ s := newTestServerWithStub (t , "http://127.0.0.1:1" , g , difc .EnforcementFilter )
641+ h , getSpans := newRecordingProxyHandler (t , s )
642+
643+ req := httptest .NewRequest (http .MethodGet , "/repos/org/repo/issues" , nil )
644+ w := httptest .NewRecorder ()
645+ h .handleWithDIFC (w , req , "/repos/org/repo/issues" , "list_issues" ,
646+ map [string ]interface {}{"owner" : "org" , "repo" : "repo" }, nil )
647+
648+ assert .Equal (t , http .StatusBadGateway , w .Code )
649+
650+ spans := getSpans ()
651+
652+ difcSpan := spanByName (spans , "proxy.difc_pipeline" )
653+ require .NotNil (t , difcSpan , "proxy.difc_pipeline span must be recorded" )
654+ assert .Equal (t , "Error" , difcSpan .Status .Code .String (), "difc span status must be Error" )
655+ assert .Equal (t , "upstream request failed" , difcSpan .Status .Description )
656+
657+ fwdSpan := spanByName (spans , "proxy.backend.forward" )
658+ require .NotNil (t , fwdSpan , "proxy.backend.forward span must be recorded" )
659+ assert .Equal (t , "Error" , fwdSpan .Status .Code .String (), "fwd span status must be Error" )
660+ assert .Equal (t , "upstream request failed" , fwdSpan .Status .Description )
661+ }
0 commit comments