@@ -2,7 +2,12 @@ package external
22
33import (
44 "context"
5+ "encoding/json"
6+ "net/http"
7+ "net/http/httptest"
8+ "strings"
59 "testing"
10+ "time"
611
712 state "github.com/jguan/aima/internal"
813 "github.com/jguan/aima/internal/proxy"
@@ -76,6 +81,112 @@ func TestReconcileBackendsPreservesHTTPSScheme(t *testing.T) {
7681 }
7782}
7883
84+ func TestReconciledNestedV1BasePathForwardsThroughProxy (t * testing.T ) {
85+ type chatRequest struct {
86+ Path string
87+ Model string
88+ }
89+
90+ chatRequests := make (chan chatRequest , 1 )
91+ upstream := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , r * http.Request ) {
92+ switch r .URL .Path {
93+ case "/api/v1/models" :
94+ _ = json .NewEncoder (w ).Encode (map [string ]any {
95+ "object" : "list" ,
96+ "data" : []map [string ]any {{"id" : "nested-model" }},
97+ })
98+ case "/api/v1/chat/completions" :
99+ var req struct {
100+ Model string `json:"model"`
101+ }
102+ _ = json .NewDecoder (r .Body ).Decode (& req )
103+ chatRequests <- chatRequest {Path : r .URL .Path , Model : req .Model }
104+ _ = json .NewEncoder (w ).Encode (map [string ]any {"id" : "chatcmpl-1" })
105+ default :
106+ http .NotFound (w , r )
107+ }
108+ }))
109+ defer upstream .Close ()
110+
111+ ctx := context .Background ()
112+ svc , err := Probe (ctx , upstream .URL + "/api/v1/models" , upstream .Client ())
113+ if err != nil {
114+ t .Fatalf ("Probe: %v" , err )
115+ }
116+ if svc .BaseURL != upstream .URL + "/api/v1" {
117+ t .Fatalf ("BaseURL = %q, want %q" , svc .BaseURL , upstream .URL + "/api/v1" )
118+ }
119+
120+ proxyServer := proxy .NewServer (proxy .WithAddr ("127.0.0.1:0" ))
121+ imported , err := ReconcileBackends (proxyServer , OverviewFromScan (svc ), svc .Models )
122+ if err != nil {
123+ t .Fatalf ("ReconcileBackends: %v" , err )
124+ }
125+ if imported != 1 {
126+ t .Fatalf ("imported = %d, want 1" , imported )
127+ }
128+
129+ proxyCtx , cancelProxy := context .WithCancel (context .Background ())
130+ ready := make (chan string , 1 )
131+ proxyErr := make (chan error , 1 )
132+ proxyServer .SetOnReady (func (addr string ) {
133+ ready <- addr
134+ })
135+ go func () {
136+ proxyErr <- proxyServer .Start (proxyCtx )
137+ }()
138+ defer func () {
139+ cancelProxy ()
140+ select {
141+ case err := <- proxyErr :
142+ if err != nil {
143+ t .Errorf ("proxy Start: %v" , err )
144+ }
145+ case <- time .After (time .Second ):
146+ t .Error ("proxy did not stop after context cancellation" )
147+ }
148+ }()
149+
150+ var proxyAddr string
151+ select {
152+ case proxyAddr = <- ready :
153+ case err := <- proxyErr :
154+ t .Fatalf ("proxy stopped before ready: %v" , err )
155+ case <- time .After (2 * time .Second ):
156+ t .Fatal ("proxy did not become ready" )
157+ }
158+
159+ body := `{"model":"nested-model","messages":[{"role":"user","content":"hi"}]}`
160+ requestCtx , cancelRequest := context .WithTimeout (context .Background (), 2 * time .Second )
161+ defer cancelRequest ()
162+ req , err := http .NewRequestWithContext (requestCtx , http .MethodPost , "http://" + proxyAddr + "/v1/chat/completions" , strings .NewReader (body ))
163+ if err != nil {
164+ t .Fatalf ("NewRequest: %v" , err )
165+ }
166+ req .Header .Set ("Content-Type" , "application/json" )
167+ client := & http.Client {Timeout : 2 * time .Second }
168+ resp , err := client .Do (req )
169+ if err != nil {
170+ t .Fatalf ("proxy request: %v" , err )
171+ }
172+ defer resp .Body .Close ()
173+ if resp .StatusCode != http .StatusOK {
174+ t .Fatalf ("proxy status = %d, want %d" , resp .StatusCode , http .StatusOK )
175+ }
176+
177+ select {
178+ case got := <- chatRequests :
179+ if got .Path != "/api/v1/chat/completions" {
180+ t .Fatalf ("upstream path = %q, want /api/v1/chat/completions" , got .Path )
181+ }
182+ if got .Model != "nested-model" {
183+ t .Fatalf ("upstream model = %q, want nested-model" , got .Model )
184+ }
185+ case <- time .After (time .Second ):
186+ t .Fatal ("upstream did not receive chat request" )
187+ }
188+ }
189+
79190func TestReconcileBackendsRejectsHealthzService (t * testing.T ) {
80191 proxyServer := proxy .NewServer ()
81192 service := Overview {
0 commit comments