@@ -18,6 +18,7 @@ import (
1818 "github.com/stretchr/testify/require"
1919
2020 "github.com/envoyproxy/ai-gateway/internal/apischema/openai"
21+ "github.com/envoyproxy/ai-gateway/internal/extproc/backendauth"
2122 "github.com/envoyproxy/ai-gateway/internal/extproc/translator"
2223 "github.com/envoyproxy/ai-gateway/internal/filterapi"
2324 "github.com/envoyproxy/ai-gateway/internal/internalapi"
@@ -316,3 +317,151 @@ func Test_responsesProcessorUpstreamFilter_SetBackend(t *testing.T) {
316317 require .True (t , p2 .stream )
317318 require .NotNil (t , p2 .translator )
318319}
320+
321+ func Test_responsesProcessorUpstreamFilter_ProcessRequestHeaders (t * testing.T ) {
322+ t .Run ("ok sets header/body mutation and metrics" , func (t * testing.T ) {
323+ // prepare translator to expect raw body and return header/body mutation
324+ mt := & mockResponsesTranslator {t : t , expBody : []byte (`{"model":"m1"}` )}
325+ expHeadMut := & extprocv3.HeaderMutation {
326+ SetHeaders : []* corev3.HeaderValueOption {{Header : & corev3.HeaderValue {Key : "x-test" , Value : "v" }}},
327+ }
328+ expBodyMut := & extprocv3.BodyMutation {}
329+ mt .retHeaderMutation = expHeadMut
330+ mt .retBodyMutation = expBodyMut
331+
332+ mm := & mockResponsesMetrics {}
333+
334+ // build upstream filter with original request body present (as router would set)
335+ headers := map [string ]string {":path" : "/foo" }
336+ p := & responsesProcessorUpstreamFilter {translator : mt , metrics : mm , requestHeaders : headers , config : & processorConfig {}}
337+ p .originalRequestBodyRaw = []byte (`{"model":"m1"}` )
338+ p .originalRequestBody = & openai.ResponseRequest {Model : "m1" }
339+
340+ res , err := p .ProcessRequestHeaders (t .Context (), nil )
341+ require .NoError (t , err )
342+ require .NotNil (t , res )
343+ rh := res .Response .(* extprocv3.ProcessingResponse_RequestHeaders ).RequestHeaders .Response
344+ require .Equal (t , expHeadMut , rh .HeaderMutation )
345+ require .Equal (t , expBodyMut , rh .BodyMutation )
346+ // metrics should have been started and models set
347+ require .True (t , mm .started )
348+ require .Equal (t , internalapi .OriginalModel ("m1" ), mm .originalModel )
349+ require .Equal (t , internalapi .RequestModel ("m1" ), mm .requestModel )
350+ })
351+
352+ t .Run ("translator error records failure" , func (t * testing.T ) {
353+ mt := & mockResponsesTranslator {t : t }
354+ mt .retErr = errors .New ("translate fail" )
355+ mm := & mockResponsesMetrics {}
356+ // Ensure logger is non-nil so deferred error logging doesn't panic
357+ p := & responsesProcessorUpstreamFilter {translator : mt , metrics : mm , requestHeaders : map [string ]string {}, config : & processorConfig {}, logger : slog .Default ()}
358+ p .originalRequestBodyRaw = []byte (`{"model":"m1"}` )
359+ p .originalRequestBody = & openai.ResponseRequest {Model : "m1" }
360+
361+ _ , err := p .ProcessRequestHeaders (t .Context (), nil )
362+ require .Error (t , err )
363+ mm .RequireRequestFailure (t )
364+ })
365+ }
366+
367+ func Test_responsesProcessorUpstreamFilter_ProcessRequestBody_panics (t * testing.T ) {
368+ p := & responsesProcessorUpstreamFilter {}
369+ require .Panics (t , func () {
370+ _ , _ = p .ProcessRequestBody (t .Context (), & extprocv3.HttpBody {})
371+ })
372+ }
373+
374+ // processorFunc is a lightweight test helper implementing Processor via function fields.
375+ type processorFunc struct {
376+ processRequestHeadersFunc func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error )
377+ processRequestBodyFunc func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error )
378+ processResponseHeadersFunc func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error )
379+ processResponseBodyFunc func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error )
380+ setBackendFunc func (context.Context , * filterapi.Backend , backendauth.Handler , Processor ) error
381+ }
382+
383+ func (p processorFunc ) ProcessRequestHeaders (ctx context.Context , h * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
384+ if p .processRequestHeadersFunc != nil {
385+ return p .processRequestHeadersFunc (ctx , h )
386+ }
387+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_RequestHeaders {}}, nil
388+ }
389+
390+ func (p processorFunc ) ProcessRequestBody (ctx context.Context , b * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
391+ if p .processRequestBodyFunc != nil {
392+ return p .processRequestBodyFunc (ctx , b )
393+ }
394+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_RequestBody {}}, nil
395+ }
396+
397+ func (p processorFunc ) ProcessResponseHeaders (ctx context.Context , h * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
398+ if p .processResponseHeadersFunc != nil {
399+ return p .processResponseHeadersFunc (ctx , h )
400+ }
401+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseHeaders {}}, nil
402+ }
403+
404+ func (p processorFunc ) ProcessResponseBody (ctx context.Context , b * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
405+ if p .processResponseBodyFunc != nil {
406+ return p .processResponseBodyFunc (ctx , b )
407+ }
408+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseBody {}}, nil
409+ }
410+
411+ func (p processorFunc ) SetBackend (ctx context.Context , be * filterapi.Backend , h backendauth.Handler , rp Processor ) error {
412+ if p .setBackendFunc != nil {
413+ return p .setBackendFunc (ctx , be , h , rp )
414+ }
415+ return nil
416+ }
417+
418+ func Test_responsesProcessorRouterFilter_ResponseDelegation (t * testing.T ) {
419+ t .Run ("passThrough when upstreamFilter nil" , func (t * testing.T ) {
420+ // router filter with nil upstreamFilter should delegate to passThroughProcessor
421+ r := & responsesProcessorRouterFilter {}
422+
423+ // ProcessResponseHeaders should return a ResponseHeaders-type ProcessingResponse
424+ hdrResp , err := r .ProcessResponseHeaders (t .Context (), nil )
425+ require .NoError (t , err )
426+ require .NotNil (t , hdrResp )
427+ _ , ok := hdrResp .Response .(* extprocv3.ProcessingResponse_ResponseHeaders )
428+ require .True (t , ok )
429+
430+ // ProcessResponseBody should return a ResponseBody-type ProcessingResponse
431+ bodyResp , err := r .ProcessResponseBody (t .Context (), & extprocv3.HttpBody {})
432+ require .NoError (t , err )
433+ require .NotNil (t , bodyResp )
434+ _ , ok = bodyResp .Response .(* extprocv3.ProcessingResponse_ResponseBody )
435+ require .True (t , ok )
436+ })
437+
438+ t .Run ("delegates to upstreamFilter when set" , func (t * testing.T ) {
439+ // Create a spy upstream filter that records calls and returns distinct responses
440+ called := struct { hdr , body bool }{}
441+ // Replace methods via function values by wrapping an implementation of Processor
442+ var upstream Processor = processorFunc {
443+ processResponseHeadersFunc : func (context.Context , * corev3.HeaderMap ) (* extprocv3.ProcessingResponse , error ) {
444+ called .hdr = true
445+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseHeaders {}}, nil
446+ },
447+ processResponseBodyFunc : func (context.Context , * extprocv3.HttpBody ) (* extprocv3.ProcessingResponse , error ) {
448+ called .body = true
449+ return & extprocv3.ProcessingResponse {Response : & extprocv3.ProcessingResponse_ResponseBody {}}, nil
450+ },
451+ }
452+
453+ r := & responsesProcessorRouterFilter {upstreamFilter : upstream }
454+
455+ hdrResp , err := r .ProcessResponseHeaders (t .Context (), nil )
456+ require .NoError (t , err )
457+ require .True (t , called .hdr )
458+ _ , ok := hdrResp .Response .(* extprocv3.ProcessingResponse_ResponseHeaders )
459+ require .True (t , ok )
460+
461+ bodyResp , err := r .ProcessResponseBody (t .Context (), & extprocv3.HttpBody {})
462+ require .NoError (t , err )
463+ require .True (t , called .body )
464+ _ , ok = bodyResp .Response .(* extprocv3.ProcessingResponse_ResponseBody )
465+ require .True (t , ok )
466+ })
467+ }
0 commit comments