| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243 |
- package service
- import (
- "context"
- "encoding/json"
- "errors"
- "io"
- "net/http"
- "strings"
- "testing"
- "time"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
- coderws "github.com/coder/websocket"
- "github.com/stretchr/testify/require"
- )
- type liveHTTPUpstreamStub struct {
- request *http.Request
- body []byte
- }
- type liveAttestationStub struct {
- header string
- err error
- }
- func (s liveAttestationStub) Check(context.Context) error {
- return s.err
- }
- func (s liveAttestationStub) Generate(context.Context) (string, error) {
- return s.header, s.err
- }
- func (s *liveHTTPUpstreamStub) Do(
- request *http.Request,
- _ string,
- _ int64,
- _ int,
- ) (*http.Response, error) {
- s.request = request
- body, err := io.ReadAll(request.Body)
- if err != nil {
- return nil, err
- }
- s.body = body
- return &http.Response{
- StatusCode: http.StatusOK,
- Header: http.Header{
- "Location": {"/backend-api/codex/call_test"},
- },
- Body: io.NopCloser(strings.NewReader("v=0\r\n")),
- }, nil
- }
- func (s *liveHTTPUpstreamStub) DoWithTLS(
- request *http.Request,
- proxyURL string,
- accountID int64,
- accountConcurrency int,
- _ *tlsfingerprint.Profile,
- ) (*http.Response, error) {
- return s.Do(request, proxyURL, accountID, accountConcurrency)
- }
- func TestLiveCapabilityOnlyAllowsOpenAIOAuth(t *testing.T) {
- require.True(t, (&Account{Platform: PlatformOpenAI, Type: AccountTypeOAuth}).SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive))
- require.False(t, (&Account{Platform: PlatformOpenAI, Type: AccountTypeAPIKey}).SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive))
- require.False(t, (&Account{Platform: PlatformGrok, Type: AccountTypeOAuth}).SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive))
- require.False(t, (&Account{
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- openAIAuthModeCredentialKey: OpenAIAuthModePersonalAccessToken,
- },
- }).SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive))
- require.False(t, (&Account{
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Credentials: map[string]any{
- openAIAuthModeCredentialKey: OpenAIAuthModeAgentIdentity,
- },
- }).SupportsOpenAIEndpointCapability(OpenAIEndpointCapabilityLive))
- }
- func TestValidateLiveCallRequestDoesNotRequireDelegation(t *testing.T) {
- request := &LiveCallRequest{
- SDP: "v=0\r\n",
- Session: json.RawMessage(`{"model":"gpt-live-test","instructions":"hello"}`),
- }
- require.NoError(t, ValidateLiveCallRequest(request))
- require.NotContains(t, string(request.Session), "delegation")
- }
- func TestCreateUpstreamLiveCallPreservesSession(t *testing.T) {
- upstream := &liveHTTPUpstreamStub{}
- service := &OpenAIGatewayService{
- cfg: &config.Config{},
- httpUpstream: upstream,
- }
- account := &Account{
- ID: 7,
- Platform: PlatformOpenAI,
- Type: AccountTypeOAuth,
- Concurrency: 2,
- Credentials: map[string]any{
- "access_token": "test-access-token",
- "chatgpt_account_id": "acct_test",
- },
- }
- session := json.RawMessage(`{
- "model":"gpt-live-test",
- "delegation":{"type":"client"},
- "custom":{"keep":true}
- }`)
- created, err := service.createUpstreamLiveCall(context.Background(), account, &LiveCallRequest{
- SDP: "v=offer\r\n",
- Session: session,
- }, `{"v":1,"s":0,"t":"v1.test"}`)
- require.NoError(t, err)
- require.Equal(t, "call_test", created.CallID)
- require.Equal(t, []byte("v=0\r\n"), created.SDP)
- var forwarded struct {
- SDP string `json:"sdp"`
- Session json.RawMessage `json:"session"`
- }
- require.NoError(t, json.Unmarshal(upstream.body, &forwarded))
- require.Equal(t, "v=offer\r\n", forwarded.SDP)
- require.JSONEq(t, string(session), string(forwarded.Session))
- require.Equal(t, "Bearer test-access-token", upstream.request.Header.Get("Authorization"))
- require.Equal(t, "acct_test", upstream.request.Header.Get("Chatgpt-Account-Id"))
- require.Equal(t, "quicksilver=v2", upstream.request.Header.Get("OpenAI-Alpha"))
- require.Equal(t, `{"v":1,"s":0,"t":"v1.test"}`, upstream.request.Header.Get(liveAttestationHeader))
- require.NotEmpty(t, upstream.request.Header.Get("Session-Id"))
- require.NotEmpty(t, upstream.request.Header.Get("Thread-Id"))
- require.Empty(t, upstream.request.Header.Get("OpenAI-Beta"))
- require.Equal(t, HTTPUpstreamProfileOpenAI, HTTPUpstreamProfileFromContext(upstream.request.Context()))
- require.True(t, HTTPUpstreamRedirectsDisabled(upstream.request.Context()))
- }
- func TestLiveAttestationCipherRoundTripAndRejectsOtherInstanceKey(t *testing.T) {
- first := newLiveAttestationCipher(&config.Config{
- JWT: config.JWTConfig{Secret: "first-live-secret"},
- })
- second := newLiveAttestationCipher(&config.Config{
- JWT: config.JWTConfig{Secret: "second-live-secret"},
- })
- require.NotNil(t, first)
- require.NotNil(t, second)
- ciphertext, err := first.Encrypt(`{"v":1,"s":0,"t":"v1.opaque"}`)
- require.NoError(t, err)
- require.NotContains(t, ciphertext, "opaque")
- plaintext, err := first.Decrypt(ciphertext)
- require.NoError(t, err)
- require.Equal(t, `{"v":1,"s":0,"t":"v1.opaque"}`, plaintext)
- _, err = second.Decrypt(ciphertext)
- require.Error(t, err)
- }
- func TestPrepareLiveAttestationEncryptsHeaderAndReturnsExplicitProviderError(t *testing.T) {
- cipher := newLiveAttestationCipher(&config.Config{
- JWT: config.JWTConfig{Secret: "live-attestation-test-secret"},
- })
- service := &OpenAIGatewayService{
- liveAttestation: liveAttestationStub{header: `{"v":1,"s":0,"t":"v1.test"}`},
- liveAttestationCipher: cipher,
- }
- header, ciphertext, err := service.prepareLiveAttestation(context.Background())
- require.NoError(t, err)
- require.Equal(t, `{"v":1,"s":0,"t":"v1.test"}`, header)
- require.NotContains(t, ciphertext, "v1.test")
- decrypted, err := cipher.Decrypt(ciphertext)
- require.NoError(t, err)
- require.Equal(t, header, decrypted)
- service.liveAttestation = liveAttestationStub{err: errors.New("macOS app missing")}
- _, _, err = service.prepareLiveAttestation(context.Background())
- var unavailable *LiveAttestationUnavailableError
- require.ErrorAs(t, err, &unavailable)
- require.Contains(t, unavailable.Error(), "macOS app missing")
- }
- func TestLiveMaxSessionDurationDefaultsAndOverrides(t *testing.T) {
- require.Equal(t, defaultLiveMaxSessionDuration, (&OpenAIGatewayService{}).liveMaxSessionDuration())
- require.Equal(
- t,
- 90*time.Second,
- (&OpenAIGatewayService{cfg: &config.Config{
- Gateway: config.GatewayConfig{
- Live: config.GatewayLiveConfig{MaxSessionDurationSeconds: 90},
- },
- }}).liveMaxSessionDuration(),
- )
- }
- func TestLiveSidebandNormalCloseEndsCall(t *testing.T) {
- normalClose := coderws.CloseError{Code: coderws.StatusNormalClosure}
- require.ErrorIs(t, liveSidebandReadError(normalClose), ErrLiveCallNotFound)
- abnormalClose := coderws.CloseError{Code: coderws.StatusInternalError}
- require.Equal(t, abnormalClose, liveSidebandReadError(abnormalClose))
- }
- func TestLiveCreateFailoverUsesExistingOpenAIPolicy(t *testing.T) {
- service := &OpenAIGatewayService{}
- account := newOpenAIUpstreamErrorTestAccount()
- require.False(t, service.shouldFailoverLiveCreateError(account, &UpstreamFailoverError{
- StatusCode: http.StatusBadRequest,
- ResponseBody: []byte(`{"error":{"message":"invalid session"}}`),
- }))
- require.True(t, service.shouldFailoverLiveCreateError(account, &UpstreamFailoverError{
- StatusCode: http.StatusForbidden,
- }))
- require.True(t, service.shouldFailoverLiveCreateError(account, &UpstreamFailoverError{
- StatusCode: http.StatusBadGateway,
- }))
- require.True(t, service.shouldFailoverLiveCreateError(account, errors.New("transport failed")))
- }
- func TestLiveCallIDFromLocation(t *testing.T) {
- callID, err := liveCallIDFromLocation("https://chatgpt.com/backend-api/codex/call_123?intent=quicksilver")
- require.NoError(t, err)
- require.Equal(t, "call_123", callID)
- callID, err = liveCallIDFromLocation("/backend-api/codex/call_456")
- require.NoError(t, err)
- require.Equal(t, "call_456", callID)
- }
- func TestRequestTypeLive(t *testing.T) {
- require.True(t, RequestTypeLive.IsValid())
- require.Equal(t, "live", RequestTypeLive.String())
- parsed, err := ParseUsageRequestType("live")
- require.NoError(t, err)
- require.Equal(t, RequestTypeLive, parsed)
- }
|