| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- package service
- import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "github.com/Wei-Shaw/sub2api/internal/config"
- "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
- "github.com/gin-gonic/gin"
- "github.com/stretchr/testify/require"
- )
- func mode1TestAccount(id int64) *Account {
- return &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Concurrency: 4,
- Extra: map[string]any{"codex_fingerprint_seed": newCodexFingerprintSeed(), "codex_fingerprint_mode": "device", "enable_tls_fingerprint": true, "tls_fingerprint_builtin": "nodejs24",
- "anti_degrade": map[string]any{"enabled": true, "mode": "mode1", "policy_version": 2, "max_concurrency": 4}}}
- }
- func TestMode1IdentityIsUniquePersistentAndDoesNotMergeThreads(t *testing.T) {
- ctx := context.Background()
- repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{}}
- admin := &adminServiceImpl{accountRepo: repo}
- svc := NewAntiDegradeService(admin)
- seen := map[string]bool{}
- for id := int64(1); id <= 32; id++ {
- repo.accounts[id] = &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Status: StatusActive, Concurrency: 2, Extra: map[string]any{"custom": "keep"}}
- a, err := svc.ApplyAntiDegradeMode(ctx, id, AntiDegradeMode1)
- require.NoError(t, err)
- seed := requireValidCodexFingerprintSeed(t, a.Extra)
- require.False(t, seen[seed])
- seen[seed] = true
- // Simulate the JSON representation stored by the database and a new service instance.
- stored, err := json.Marshal(a)
- require.NoError(t, err)
- var loaded Account
- require.NoError(t, json.Unmarshal(stored, &loaded))
- repo.accounts[id] = &loaded
- updated, err := admin.UpdateAccount(ctx, id, &UpdateAccountInput{Name: "renamed", Extra: map[string]any{"custom": "edited"}})
- require.NoError(t, err)
- require.Equal(t, seed, requireValidCodexFingerprintSeed(t, updated.Extra))
- require.True(t, isMode1ProtectionEnabled(updated))
- for _, session := range []string{"session-a", "session-b"} {
- headers := http.Header{}
- headers.Set("session-id", session)
- headers.Set("thread-id", "thread-"+session)
- ids := resolveCodexFingerprintIDsFromRequest(updated, headers)
- applyCodexFingerprintHeaders(headers, ids)
- require.NotEmpty(t, headers.Get("x-codex-installation-id"))
- require.Equal(t, session, headers.Get("session-id"))
- require.Equal(t, "thread-"+session, headers.Get("thread-id"))
- }
- a, err = NewAntiDegradeService(admin).RevertAntiDegrade(ctx, id)
- require.NoError(t, err)
- require.False(t, antiDegradeEnabled(a))
- require.Equal(t, seed, requireValidCodexFingerprintSeed(t, a.Extra))
- require.Equal(t, "edited", a.Extra["custom"])
- a, err = svc.ApplyAntiDegradeMode(ctx, id, AntiDegradeMode1)
- require.NoError(t, err)
- require.Equal(t, seed, requireValidCodexFingerprintSeed(t, a.Extra))
- }
- }
- func TestMode1RuntimeRejectsConfigurationDrift(t *testing.T) {
- a := mode1TestAccount(1)
- p, err := resolveMode1TLSProfile(a)
- require.NoError(t, err)
- require.NotNil(t, p)
- a.Concurrency = 100
- require.Equal(t, 100, a.Mode1EffectiveConcurrency())
- a.Concurrency = 1
- require.Equal(t, 1, a.Mode1EffectiveConcurrency())
- a.Extra["enable_tls_fingerprint"] = false
- p, err = resolveMode1TLSProfile(a)
- require.NoError(t, err)
- require.Nil(t, p)
- a.Extra["enable_tls_fingerprint"] = true
- delete(a.Extra, "codex_fingerprint_seed")
- _, err = resolveMode1TLSProfile(a)
- require.Error(t, err)
- }
- func TestMode1InvalidMarkerDoesNotDowngrade(t *testing.T) {
- for _, version := range []any{"2", 999, nil} {
- a := mode1TestAccount(1)
- mode1Marker(a)["policy_version"] = version
- p, err := resolveMode1TLSProfile(a)
- require.Error(t, err)
- require.Nil(t, p)
- }
- }
- func TestMode1DifferentAccountsWithDuplicatedLegacyDeviceStillDiffer(t *testing.T) {
- a, b := mode1TestAccount(1), mode1TestAccount(2)
- a.Extra["openai_device_id"] = "legacy-shared"
- b.Extra["openai_device_id"] = "legacy-shared"
- require.Equal(t, a.GetOpenAIDeviceID(), b.GetOpenAIDeviceID())
- seedA, _ := codexFingerprintSeed(a.Extra)
- seedB, _ := codexFingerprintSeed(b.Extra)
- require.NotEqual(t, resolveConvergedInstallationID(a, seedA), resolveConvergedInstallationID(b, seedB))
- require.Equal(t, resolveConvergedInstallationID(a, seedA), resolveConvergedInstallationID(a, seedA))
- }
- func TestMode1PartialAndBulkUpdatesCannotDisablePolicy(t *testing.T) {
- a := mode1TestAccount(1)
- repo := &upstreamBillingProbeAccountRepo{accounts: map[int64]*Account{1: a}}
- admin := &adminServiceImpl{accountRepo: repo}
- err := admin.UpdateAccountExtra(context.Background(), 1, map[string]any{"anti_degrade": nil})
- require.ErrorContains(t, err, "专用")
- _, err = admin.BulkUpdateAccounts(context.Background(), &BulkUpdateAccountsInput{AccountIDs: []int64{1}, Extra: map[string]any{"anti_degrade": nil}})
- require.ErrorContains(t, err, "模式一")
- require.True(t, isMode1ProtectionEnabled(repo.accounts[1]))
- require.Empty(t, repo.bulkUpdates)
- }
- type mode1HTTPRecorder struct {
- normal, tls int
- profile *tlsfingerprint.Profile
- cap int
- err error
- }
- func (r *mode1HTTPRecorder) Do(_ *http.Request, _ string, _ int64, cap int) (*http.Response, error) {
- r.normal++
- r.cap = cap
- if r.err != nil {
- return nil, r.err
- }
- return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader("ok"))}, nil
- }
- func (r *mode1HTTPRecorder) DoWithTLS(_ *http.Request, _ string, _ int64, cap int, p *tlsfingerprint.Profile) (*http.Response, error) {
- r.tls++
- r.profile = p
- r.cap = cap
- if r.err != nil {
- return nil, r.err
- }
- return &http.Response{StatusCode: 200, Header: http.Header{}, Body: io.NopCloser(strings.NewReader("ok"))}, nil
- }
- func TestMode1HTTPAndAccountTestUseSameTLSWithoutFallback(t *testing.T) {
- ctx := context.Background()
- a := mode1TestAccount(1)
- a.Concurrency = 100
- u := &mode1HTTPRecorder{}
- gw := &OpenAIGatewayService{httpUpstream: u}
- req, _ := http.NewRequestWithContext(ctx, http.MethodPost, "https://localhost/responses", nil)
- r, err := gw.doOpenAIUpstream(req, "", a)
- require.NoError(t, err)
- r.Body.Close()
- require.Equal(t, 100, u.cap)
- require.NotNil(t, u.profile)
- profileKey := u.profile.CacheKey()
- tester := &AccountTestService{httpUpstream: u}
- r, err = tester.doOpenAIAccountTestUpstream(req, "", a, false)
- require.NoError(t, err)
- r.Body.Close()
- require.Equal(t, profileKey, u.profile.CacheKey())
- u.err = errors.New("TLS failed")
- _, err = gw.doOpenAIUpstream(req, "", a)
- require.Error(t, err)
- require.Zero(t, u.normal)
- gw.cfg = &config.Config{}
- u.err = nil
- r, err = gw.doOpenAIUpstream(req, "", a)
- require.NoError(t, err)
- r.Body.Close()
- require.Equal(t, 1, u.normal)
- require.Equal(t, 100, u.cap)
- }
- func TestMode1UsesExistingPluginRouting(t *testing.T) {
- manager := &PluginManager{}
- manager.route.Store(&pluginRoute{pluginID: 1, rolloutPercent: 100, unavailable: "test"})
- u := &mode1HTTPRecorder{}
- gw := &OpenAIGatewayService{httpUpstream: u, pluginManager: manager}
- req, _ := http.NewRequest(http.MethodPost, "https://localhost/responses", nil)
- _, err := gw.doOpenAIUpstream(req, "", mode1TestAccount(1))
- require.Error(t, err) // the configured plugin is actually unavailable
- require.NotContains(t, err.Error(), "未声明支持模式一")
- require.Zero(t, u.tls)
- require.Zero(t, u.normal)
- }
- func TestMode1IntegrityPreservesReasoningToolsAndBudget(t *testing.T) {
- original := []byte(`{"model":"test-model","reasoning":{"effort":"high"},"input":[{"type":"reasoning","encrypted_content":"opaque"},{"type":"function_call_output","call_id":"call-1","output":"result"}],"max_output_tokens":2000,"tools":[{"type":"function","name":"read"}]}`)
- require.NoError(t, validateMode1RequestIntegrity(original, original))
- for _, field := range []string{"reasoning", "input", "max_output_tokens", "tools", "model"} {
- var payload map[string]any
- require.NoError(t, json.Unmarshal(original, &payload))
- delete(payload, field)
- out, _ := json.Marshal(payload)
- require.ErrorContains(t, validateMode1RequestIntegrity(original, out), field)
- }
- }
- type mode1ForwardRecorder struct {
- httpUpstreamRecorder
- template *tlsfingerprint.Profile
- ordinary int
- }
- func (r *mode1ForwardRecorder) Do(req *http.Request, p string, id int64, n int) (*http.Response, error) {
- r.ordinary++
- return r.httpUpstreamRecorder.Do(req, p, id, n)
- }
- func (r *mode1ForwardRecorder) DoWithTLS(req *http.Request, p string, id int64, n int, profile *tlsfingerprint.Profile) (*http.Response, error) {
- r.template = profile
- return r.httpUpstreamRecorder.Do(req, p, id, n)
- }
- func TestMode1ForwardPreservesPayloadThroughNormalAndPassthrough(t *testing.T) {
- for _, passthrough := range []bool{false, true} {
- t.Run(fmt.Sprint(passthrough), func(t *testing.T) {
- gin.SetMode(gin.TestMode)
- c, _ := gin.CreateTestContext(httptest.NewRecorder())
- body := []byte(`{"model":"gpt-5.2","stream":true,"reasoning":{"effort":"high"},"input":[{"type":"message","role":"user","content":[{"type":"input_text","text":"Return OK"}]}]}`)
- c.Request = httptest.NewRequest("POST", "/v1/responses", bytes.NewReader(body))
- c.Request.Header.Set("User-Agent", "codex_cli_rs/0.144.1")
- if !passthrough {
- c.Request.Header.Set("User-Agent", "local-test/1.0")
- }
- c.Request.Header.Set("session-id", "test-session")
- c.Request.Header.Set("thread-id", "test-thread")
- u := &mode1ForwardRecorder{httpUpstreamRecorder: httpUpstreamRecorder{resp: &http.Response{StatusCode: 200, Header: http.Header{"Content-Type": []string{"text/event-stream"}}, Body: io.NopCloser(strings.NewReader("data: {\"type\":\"response.output_text.delta\",\"delta\":\"OK\"}\n\ndata: {\"type\":\"response.completed\",\"response\":{\"id\":\"test\",\"status\":\"completed\",\"output\":[{\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"OK\"}]}]}}\n\ndata: [DONE]\n\n"))}}}
- a := mode1TestAccount(1)
- a.Name = "mode1"
- a.Schedulable = true
- a.Credentials = map[string]any{"access_token": "local-dummy", "chatgpt_account_id": "dummy"}
- a.Extra["openai_passthrough"] = passthrough
- cfg := &config.Config{}
- cfg.Gateway.TLSFingerprint.Enabled = true
- gw := &OpenAIGatewayService{cfg: cfg, httpUpstream: u, toolCorrector: NewCodexToolCorrector()}
- _, err := gw.Forward(context.Background(), c, a, body)
- require.NoError(t, err)
- require.Zero(t, u.ordinary)
- require.NotNil(t, u.template)
- require.NoError(t, validateMode1RequestIntegrity(body, u.lastBody))
- require.NotEmpty(t, u.lastReq.Header.Get("x-codex-installation-id"))
- require.NotEmpty(t, u.lastReq.Header.Get("session-id"))
- })
- }
- }
|