package live import ( "bytes" "context" "encoding/json" "errors" "io" "net/http" "net/http/httptest" "strings" "testing" "time" "github.com/gin-gonic/gin" "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" ) func TestCreateClientSecretMapsStandardRealtimeModel(t *testing.T) { gin.SetMode(gin.TestMode) handler := &Handler{clientSecrets: newClientSecretStore()} router := gin.New() router.POST("/v1/realtime/client_secrets", func(c *gin.Context) { c.Set("userApiKey", "issuer-key") c.Set("accessProvider", "static") c.Next() }, handler.CreateClientSecret) request := httptest.NewRequest(http.MethodPost, "/v1/realtime/client_secrets", strings.NewReader(`{ "session":{"type":"realtime","model":"gpt-realtime","instructions":"help"}, "expires_after":{"anchor":"created_at","seconds":60} }`)) request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusOK, recorder.Body.String()) } var response struct { Value string `json:"value"` ExpiresAt int64 `json:"expires_at"` Session struct { ID string `json:"id"` Object string `json:"object"` Type string `json:"type"` Model string `json:"model"` Instructions string `json:"instructions"` } `json:"session"` } if errUnmarshal := json.Unmarshal(recorder.Body.Bytes(), &response); errUnmarshal != nil { t.Fatalf("unmarshal response: %v", errUnmarshal) } if !strings.HasPrefix(response.Value, clientSecretPrefix) { t.Fatalf("client secret = %q", response.Value) } if response.ExpiresAt <= time.Now().Unix() { t.Fatalf("expires_at = %d", response.ExpiresAt) } if response.Session.ID == "" || response.Session.Object != "realtime.session" || response.Session.Type != "realtime" { t.Fatalf("session = %+v", response.Session) } if response.Session.Model != "gpt-realtime" || response.Session.Instructions != "help" { t.Fatalf("client session = %+v", response.Session) } authRequest := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", nil) authRequest.Header.Set("Authorization", "Bearer "+response.Value) authorization, matched, errAuthenticate := handler.AuthenticateClientSecret(authRequest) if errAuthenticate != nil || !matched { t.Fatalf("AuthenticateClientSecret() matched=%t error=%v", matched, errAuthenticate) } if authorization.Principal != response.Session.ID { t.Fatalf("principal = %q, want %q", authorization.Principal, response.Session.ID) } if authorization.IssuerPrincipal != "issuer-key" || authorization.IssuerProvider != "static" { t.Fatalf("issuer = %q/%q", authorization.IssuerProvider, authorization.IssuerPrincipal) } if got := modelFromJSON(authorization.Session); got != defaultLiveModel { t.Fatalf("upstream session model = %q, want %q", got, defaultLiveModel) } } func TestStandardRealtimeCallMapsModelAndLocation(t *testing.T) { gin.SetMode(gin.TestMode) manager := auth.NewManager(nil, nil, nil) executor := &captureExecutor{responseBody: io.NopCloser(strings.NewReader("v=0\r\n"))} manager.RegisterExecutor(executor) if _, errRegister := manager.Register(context.Background(), &auth.Auth{ ID: "codex-oauth", Provider: "codex", Status: auth.StatusActive, Metadata: map[string]any{"access_token": "oauth-token"}, }); errRegister != nil { t.Fatalf("register auth: %v", errRegister) } handler := NewHandler(manager, nil) router := gin.New() router.POST("/v1/realtime/calls", handler.Handle) const boundary = "standard-realtime-boundary" body := multipartBody(boundary, "v=0\r\n", `{"type":"realtime","model":"gpt-realtime"}`) request := httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", strings.NewReader(body)) request.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusCreated { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String()) } if recorder.Header().Get("Location") != "/v1/realtime/calls/call-123" { t.Fatalf("Location = %q", recorder.Header().Get("Location")) } if got := modelFromJSON(executor.body); got != defaultLiveModel { t.Fatalf("upstream model = %q, want %q; body=%s", got, defaultLiveModel, executor.body) } } func TestClientSecretStoreRejectsExpiredToken(t *testing.T) { store := newClientSecretStore() now := time.Unix(1700000000, 0) store.now = func() time.Time { return now } token, _, _, errCreate := store.create(json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex"}`), time.Minute, "issuer", "test") if errCreate != nil { t.Fatalf("create() error = %v", errCreate) } if _, errAuthenticate := store.authenticate(token); errAuthenticate != nil { t.Fatalf("authenticate() error = %v", errAuthenticate) } now = now.Add(time.Minute) if _, errAuthenticate := store.authenticate(token); errAuthenticate == nil { t.Fatal("authenticate() accepted expired token") } } func TestNormalizeClientSecretSessionHandlesWhitespaceNullAndRejectsArrays(t *testing.T) { clientSession, upstreamSession, errNormalize := normalizeClientSecretSession(json.RawMessage(" null \n")) if errNormalize != nil { t.Fatalf("normalize whitespace null: %v", errNormalize) } if modelFromJSON(clientSession) != "gpt-realtime" || modelFromJSON(upstreamSession) != defaultLiveModel { t.Fatalf("client=%s upstream=%s", clientSession, upstreamSession) } if _, _, errNormalize = normalizeClientSecretSession(json.RawMessage(`[]`)); errNormalize == nil { t.Fatal("normalize accepted an array session") } } func TestReadClientSecretBodyRejectsOversizedSession(t *testing.T) { _, errRead := readClientSecretBody(bytes.NewReader(make([]byte, clientSecretMaxBodySize+1))) if !errors.Is(errRead, errBodyTooLarge) { t.Fatalf("readClientSecretBody() error = %v", errRead) } } func TestCreateClientSecretRejectsUnsupportedSessionType(t *testing.T) { gin.SetMode(gin.TestMode) handler := &Handler{clientSecrets: newClientSecretStore()} router := gin.New() router.POST("/v1/realtime/client_secrets", handler.CreateClientSecret) request := httptest.NewRequest(http.MethodPost, "/v1/realtime/client_secrets", strings.NewReader(`{"session":{"type":"transcription","model":"gpt-4o-transcribe"}}`)) recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusNotImplemented { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusNotImplemented, recorder.Body.String()) } if !strings.Contains(recorder.Body.String(), "realtime_capability_not_supported") { t.Fatalf("body = %s", recorder.Body.String()) } } func TestLiveSelectionHeadersRemoveLocalClientSecret(t *testing.T) { gin.SetMode(gin.TestMode) ginContext, _ := gin.CreateTestContext(httptest.NewRecorder()) ginContext.Request = httptest.NewRequest(http.MethodPost, "/v1/realtime/calls", nil) ginContext.Request.Header.Set("Authorization", "Bearer ek_secret") ginContext.Request.Header.Set("OpenAI-Safety-Identifier", "safe-user") ginContext.Set(ClientSecretPrincipalContextKey, "sess_123") headers := liveSelectionHeaders(ginContext) if headers.Get("Authorization") != "" { t.Fatalf("Authorization leaked: %q", headers.Get("Authorization")) } if headers.Get("OpenAI-Safety-Identifier") != "safe-user" { t.Fatalf("safety identifier = %q", headers.Get("OpenAI-Safety-Identifier")) } } func TestSidebandRejectsClientSecretScopeMismatch(t *testing.T) { gin.SetMode(gin.TestMode) handler := NewHandler(auth.NewManager(nil, nil, nil), nil) handler.sessions.put("call-123", liveSession{ authID: "codex-oauth", model: defaultLiveModel, clientSecretPrincipal: "sess_expected", }) router := gin.New() router.GET("/v1/realtime/calls/:call_id", func(c *gin.Context) { c.Set(ClientSecretPrincipalContextKey, "sess_other") c.Next() }, handler.HandleSideband) request := httptest.NewRequest(http.MethodGet, "/v1/realtime/calls/call-123", nil) request.Header.Set("Connection", "Upgrade") request.Header.Set("Upgrade", "websocket") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String()) } claimed, claim := handler.sessions.claim("call-123") if claim != sessionClaimAcquired { t.Fatalf("session claim = %v", claim) } handler.sessions.release(claimed) } func TestSidebandRejectsStandardPrincipalScopeMismatch(t *testing.T) { gin.SetMode(gin.TestMode) handler := NewHandler(auth.NewManager(nil, nil, nil), nil) handler.sessions.put("call-123", liveSession{ authID: "codex-oauth", model: defaultLiveModel, ownerPrincipal: "owner-key", ownerProvider: "static", }) router := gin.New() router.GET("/v1/realtime/calls/:call_id", func(c *gin.Context) { c.Set("userApiKey", "other-key") c.Set("accessProvider", "static") c.Next() }, handler.HandleSideband) request := httptest.NewRequest(http.MethodGet, "/v1/realtime/calls/call-123", nil) request.Header.Set("Connection", "Upgrade") request.Header.Set("Upgrade", "websocket") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusForbidden { t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusForbidden, recorder.Body.String()) } } func TestApplyClientSecretCallSession(t *testing.T) { session := json.RawMessage(`{"type":"realtime","model":"gpt-live-1-codex","instructions":"help"}`) body, contentType, model, errApply := applyClientSecretCallSession([]byte("v=0\r\n"), "application/sdp", defaultLiveModel, session) if errApply != nil { t.Fatalf("applyClientSecretCallSession() error = %v", errApply) } if contentType != "application/json" || model != defaultLiveModel { t.Fatalf("contentType=%q model=%q", contentType, model) } var payload struct { SDP string `json:"sdp"` Session struct { Instructions string `json:"instructions"` } `json:"session"` } if errUnmarshal := json.Unmarshal(body, &payload); errUnmarshal != nil { t.Fatalf("unmarshal body: %v", errUnmarshal) } if payload.SDP != "v=0\r\n" || payload.Session.Instructions != "help" { t.Fatalf("payload = %+v", payload) } }