account_credential_shadow_skip_test.go 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. //go:build unit
  2. package service
  3. import (
  4. "context"
  5. "errors"
  6. "net/http"
  7. "net/http/httptest"
  8. "testing"
  9. "github.com/gin-gonic/gin"
  10. "github.com/imroc/req/v3"
  11. "github.com/stretchr/testify/require"
  12. )
  13. // shadowSkipTestRepo 是满足 AccountRepository 接口的最小 stub(只实现 GetByID)。
  14. // 其他方法通过嵌入 nil 接口值满足编译,若被误调则 panic,便于发现意外调用路径。
  15. type shadowSkipTestRepo struct {
  16. AccountRepository
  17. account *Account
  18. }
  19. func (r *shadowSkipTestRepo) GetByID(_ context.Context, id int64) (*Account, error) {
  20. if r.account == nil || r.account.ID != id {
  21. return nil, ErrAccountNotFound
  22. }
  23. return r.account, nil
  24. }
  25. func newShadowTestGinCtx() *gin.Context {
  26. gin.SetMode(gin.TestMode)
  27. rec := httptest.NewRecorder()
  28. c, _ := gin.CreateTestContext(rec)
  29. c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/200/test", nil)
  30. return c
  31. }
  32. // --- 1. CanRefresh 守卫 ---
  33. // TestOpenAITokenRefresherSkipsShadow 验证影子账号不被后台 token 刷新器处理。
  34. func TestOpenAITokenRefresherSkipsShadow(t *testing.T) {
  35. pid := int64(100)
  36. r := NewOpenAITokenRefresher(nil, nil)
  37. // 影子账号:ParentAccountID 非 nil → CanRefresh 应返回 false
  38. require.False(t, r.CanRefresh(&Account{ID: 200, Platform: PlatformOpenAI, Type: AccountTypeOAuth, ParentAccountID: &pid}))
  39. // 普通账号:有 refresh_token → CanRefresh 应返回 true
  40. require.True(t, r.CanRefresh(&Account{ID: 100, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Credentials: map[string]any{"refresh_token": "RT"}}))
  41. }
  42. // --- 2. TestAccountConnection 影子凭据解析 ---
  43. // TestAccountTestServiceSkipsShadow 验证影子账号连接测试不再早拒,而是尝试解析母账号凭据。
  44. func TestAccountTestServiceSkipsShadow(t *testing.T) {
  45. pid := int64(100)
  46. shadow := &Account{
  47. ID: 200,
  48. Platform: PlatformOpenAI,
  49. Type: AccountTypeOAuth,
  50. ParentAccountID: &pid,
  51. }
  52. repo := &shadowSkipTestRepo{account: shadow}
  53. svc := &AccountTestService{accountRepo: repo}
  54. c := newShadowTestGinCtx()
  55. err := svc.TestAccountConnection(c, 200, "", "", "")
  56. require.Error(t, err)
  57. require.Contains(t, err.Error(), "resolve spark shadow parent")
  58. }
  59. // --- 3. EnsureOpenAIPrivacy 守卫 ---
  60. // TestEnsureOpenAIPrivacySkipsShadow 验证影子账号跳过隐私设置(不调用 privacyClientFactory)。
  61. // 影子账号透传母账号凭据,但 Extra 通常为空,需给它一个 access_token 才能让
  62. // 现有的 token=="" 提前返回路径失效,从而真实验证 IsCredentialShadow 守卫。
  63. func TestEnsureOpenAIPrivacySkipsShadow(t *testing.T) {
  64. pid := int64(100)
  65. shadow := &Account{
  66. ID: 200,
  67. Platform: PlatformOpenAI,
  68. Type: AccountTypeOAuth,
  69. ParentAccountID: &pid,
  70. // 提供 access_token:没有影子守卫时会进入 factory 调用
  71. Credentials: map[string]any{"access_token": "shadow-passthrough-token"},
  72. }
  73. privacyCalled := false
  74. svc := &adminServiceImpl{
  75. privacyClientFactory: func(proxyURL string) (*req.Client, error) {
  76. privacyCalled = true
  77. return nil, errors.New("should not reach factory for shadow account")
  78. },
  79. }
  80. got := svc.EnsureOpenAIPrivacy(context.Background(), shadow)
  81. require.Equal(t, "", got)
  82. require.False(t, privacyCalled, "privacyClientFactory 不应被影子账号触发")
  83. }