Files
sub2api/backend/internal/service/account_credential_shadow_skip_test.go
T
李建琦 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

97 lines
3.3 KiB
Go

//go:build unit
package service
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/imroc/req/v3"
"github.com/stretchr/testify/require"
)
// shadowSkipTestRepo 是满足 AccountRepository 接口的最小 stub(只实现 GetByID)。
// 其他方法通过嵌入 nil 接口值满足编译,若被误调则 panic,便于发现意外调用路径。
type shadowSkipTestRepo struct {
AccountRepository
account *Account
}
func (r *shadowSkipTestRepo) GetByID(_ context.Context, id int64) (*Account, error) {
if r.account == nil || r.account.ID != id {
return nil, ErrAccountNotFound
}
return r.account, nil
}
func newShadowTestGinCtx() *gin.Context {
gin.SetMode(gin.TestMode)
rec := httptest.NewRecorder()
c, _ := gin.CreateTestContext(rec)
c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/200/test", nil)
return c
}
// --- 1. CanRefresh 守卫 ---
// TestOpenAITokenRefresherSkipsShadow 验证影子账号不被后台 token 刷新器处理。
func TestOpenAITokenRefresherSkipsShadow(t *testing.T) {
pid := int64(100)
r := NewOpenAITokenRefresher(nil, nil)
// 影子账号:ParentAccountID 非 nil → CanRefresh 应返回 false
require.False(t, r.CanRefresh(&Account{ID: 200, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &pid}))
// 普通账号:有 refresh_token → CanRefresh 应返回 true
require.True(t, r.CanRefresh(&Account{ID: 100, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"refresh_token": "RT"}}))
}
// --- 2. TestAccountConnection 影子凭据解析 ---
// TestAccountTestServiceSkipsShadow 验证影子账号连接测试不再早拒,而是尝试解析母账号凭据。
func TestAccountTestServiceSkipsShadow(t *testing.T) {
pid := int64(100)
shadow := &Account{
ID: 200,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
ParentAccountID: &pid,
}
repo := &shadowSkipTestRepo{account: shadow}
svc := &AccountTestService{accountRepo: repo}
c := newShadowTestGinCtx()
err := svc.TestAccountConnection(c, 200, "", "", "")
require.Error(t, err)
require.Contains(t, err.Error(), "resolve spark shadow parent")
}
// --- 3. EnsureOpenAIPrivacy 守卫 ---
// TestEnsureOpenAIPrivacySkipsShadow 验证影子账号跳过隐私设置(不调用 privacyClientFactory)。
// 影子账号透传母账号凭据,但 Extra 通常为空,需给它一个 access_token 才能让
// 现有的 token=="" 提前返回路径失效,从而真实验证 IsCredentialShadow 守卫。
func TestEnsureOpenAIPrivacySkipsShadow(t *testing.T) {
pid := int64(100)
shadow := &Account{
ID: 200,
Platform: PlatformOpenAI,
Type: AccountTypeOAuth,
ParentAccountID: &pid,
// 提供 access_token:没有影子守卫时会进入 factory 调用
Credentials: map[string]any{"access_token": "shadow-passthrough-token"},
}
privacyCalled := false
svc := &adminServiceImpl{
privacyClientFactory: func(proxyURL string) (*req.Client, error) {
privacyCalled = true
return nil, errors.New("should not reach factory for shadow account")
},
}
got := svc.EnsureOpenAIPrivacy(context.Background(), shadow)
require.Equal(t, "", got)
require.False(t, privacyCalled, "privacyClientFactory 不应被影子账号触发")
}