Files
sub2api/backend/internal/service/crs_sync_long_context_billing_test.go
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

170 lines
9.8 KiB
Go

//go:build unit
package service
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/Wei-Shaw/sub2api/internal/config"
"github.com/stretchr/testify/require"
)
type crsLongContextAccountRepo struct {
AccountRepository
accounts map[string]*Account
nextID int64
}
type crsOpenAILongContextSource struct {
collection string
credentials map[string]any
extra map[string]any
}
func newCRSLongContextAccountRepo(existing ...*Account) *crsLongContextAccountRepo {
repo := &crsLongContextAccountRepo{accounts: make(map[string]*Account)}
for _, account := range existing {
if account == nil {
continue
}
crsID, _ := account.Extra["crs_account_id"].(string)
repo.accounts[crsID] = account
if account.ID > repo.nextID {
repo.nextID = account.ID
}
}
return repo
}
func (r *crsLongContextAccountRepo) Create(_ context.Context, account *Account) error {
r.nextID++
account.ID = r.nextID
crsID, _ := account.Extra["crs_account_id"].(string)
r.accounts[crsID] = account
return nil
}
func (r *crsLongContextAccountRepo) Update(_ context.Context, account *Account) error {
crsID, _ := account.Extra["crs_account_id"].(string)
r.accounts[crsID] = account
return nil
}
func (r *crsLongContextAccountRepo) GetByCRSAccountID(_ context.Context, crsID string) (*Account, error) {
return r.accounts[crsID], nil
}
func (r *crsLongContextAccountRepo) ListShadowsByParent(_ context.Context, _ int64) ([]*Account, error) {
return nil, nil
}
func TestCRSSyncOpenAILongContextBilling(t *testing.T) {
tests := []struct {
name string
collection string
credentials map[string]any
sourceExtra map[string]any
existingExtra map[string]any
wantAction string
wantEnabled bool
}{
{name: "OAuth create defaults missing value disabled", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, wantAction: "created"},
{name: "OAuth create preserves source true", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "created", wantEnabled: true},
{name: "OAuth create preserves source false", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "created"},
{name: "OAuth update defaults missing value disabled", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, existingExtra: map[string]any{"existing": true}, wantAction: "updated"},
{name: "OAuth update preserves existing true when source omits value", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "updated", wantEnabled: true},
{name: "OAuth update preserves existing false when source omits value", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "updated"},
{name: "OAuth update preserves source true over existing false", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: true}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "updated", wantEnabled: true},
{name: "OAuth update preserves source false over existing true", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: false}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "updated"},
{name: "OAuth rejects malformed source value", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, wantAction: "failed"},
{name: "OAuth rejects malformed existing value", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, wantAction: "failed"},
{name: "OAuth update rejects malformed source value", collection: "openaiOAuthAccounts", credentials: map[string]any{"access_token": "oauth-token"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "failed"},
{name: "API key create defaults missing value disabled", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, wantAction: "created"},
{name: "API key create preserves source true", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "created", wantEnabled: true},
{name: "API key create preserves source false", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "created"},
{name: "API key update defaults missing value disabled", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, existingExtra: map[string]any{"existing": true}, wantAction: "updated"},
{name: "API key update preserves existing true when source omits value", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "updated", wantEnabled: true},
{name: "API key update preserves existing false when source omits value", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "updated"},
{name: "API key update preserves source true over existing false", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: true}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: false}, wantAction: "updated", wantEnabled: true},
{name: "API key update preserves source false over existing true", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: false}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "updated"},
{name: "API key rejects malformed source value", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, wantAction: "failed"},
{name: "API key rejects malformed existing value", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, wantAction: "failed"},
{name: "API key update rejects malformed source value", collection: "openaiResponsesAccounts", credentials: map[string]any{"api_key": "sk-test"}, sourceExtra: map[string]any{openAILongContextBillingEnabledKey: "false"}, existingExtra: map[string]any{openAILongContextBillingEnabledKey: true}, wantAction: "failed"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
const crsID = "crs-openai-1"
var existing *Account
if tt.existingExtra != nil {
existingExtra := mergeMap(tt.existingExtra, map[string]any{"crs_account_id": crsID})
accountType := AccountTypeOAuth
if tt.collection == "openaiResponsesAccounts" {
accountType = AccountTypeAPIKey
}
existing = &Account{ID: 41, Platform: PlatformOpenAI, Type: accountType, Extra: existingExtra}
}
repo := newCRSLongContextAccountRepo(existing)
result := runCRSOpenAILongContextSync(t, repo, crsOpenAILongContextSource{
collection: tt.collection,
credentials: tt.credentials,
extra: tt.sourceExtra,
})
require.Len(t, result.Items, 1)
require.Equal(t, tt.wantAction, result.Items[0].Action)
if tt.wantAction == "failed" {
require.Contains(t, result.Items[0].Error, "openai_long_context_billing_enabled must be a boolean")
return
}
stored, ok := repo.accounts[crsID].Extra[openAILongContextBillingEnabledKey]
require.True(t, ok)
require.Equal(t, tt.wantEnabled, stored)
})
}
}
func runCRSOpenAILongContextSync(t *testing.T, repo AccountRepository, source crsOpenAILongContextSource) *SyncFromCRSResult {
t.Helper()
account := map[string]any{
"kind": "openai",
"id": "crs-openai-1",
"name": "OpenAI CRS",
"isActive": true,
"schedulable": true,
"credentials": source.credentials,
}
if source.extra != nil {
account["extra"] = source.extra
}
server := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Type", "application/json")
if request.URL.Path == "/web/auth/login" {
_, _ = response.Write([]byte(`{"success":true,"token":"admin-token"}`))
return
}
require.Equal(t, "/admin/sync/export-accounts", request.URL.Path)
require.NoError(t, json.NewEncoder(response).Encode(map[string]any{
"success": true,
"data": map[string]any{source.collection: []any{account}},
}))
}))
t.Cleanup(server.Close)
cfg := &config.Config{}
cfg.Security.URLAllowlist.AllowInsecureHTTP = true
service := NewCRSSyncService(repo, nil, nil, nil, nil, cfg)
result, err := service.SyncFromCRS(context.Background(), SyncFromCRSInput{
BaseURL: server.URL,
Username: "admin",
Password: "password",
})
require.NoError(t, err)
return result
}