cn_provider_foraccount_test.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. package service
  2. // ForAccount 直传入口(P2-6)的校验回归测试:
  3. // QueryUsageForAccount / QueryBalanceForAccount 接受已加载的 *Account,
  4. // 但必须复用与 ID 入口相同的加载后校验——直传不能绕过平台/模式检查,
  5. // 且校验在 singleflight 之前完成(无效账号不得发起任何上游请求)。
  6. import (
  7. "context"
  8. "testing"
  9. infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
  10. "github.com/stretchr/testify/require"
  11. )
  12. func codingAccount(platform string) *Account {
  13. return &Account{
  14. ID: 1, Platform: platform, Type: AccountTypeAPIKey, Status: StatusActive,
  15. Credentials: map[string]any{"account_mode": AccountModeCoding, "api_key": "sk-test"},
  16. }
  17. }
  18. func paygAccount(platform string) *Account {
  19. return &Account{
  20. ID: 2, Platform: platform, Type: AccountTypeAPIKey, Status: StatusActive,
  21. Credentials: map[string]any{"account_mode": AccountModePayG, "api_key": "sk-test"},
  22. }
  23. }
  24. func requireReason(t *testing.T, err error, reason string) {
  25. t.Helper()
  26. require.Error(t, err)
  27. var appErr *infraerrors.ApplicationError
  28. require.ErrorAs(t, err, &appErr)
  29. require.Equal(t, reason, appErr.Reason)
  30. }
  31. func TestValidateCodingPlanAccount_Matrix(t *testing.T) {
  32. cases := []struct {
  33. name string
  34. account *Account
  35. wantReason string
  36. }{
  37. {name: "nil", account: nil, wantReason: "CN_QUOTA_ACCOUNT_NOT_FOUND"},
  38. {name: "non cn provider", account: &Account{ID: 3, Platform: PlatformAnthropic}, wantReason: "CN_QUOTA_INVALID_PLATFORM"},
  39. {name: "payg has no quota endpoint", account: paygAccount(PlatformKimi), wantReason: "CN_QUOTA_NOT_CODING_PLAN"},
  40. {name: "kimi coding ok", account: codingAccount(PlatformKimi)},
  41. {name: "zhipu coding ok", account: codingAccount(PlatformZhipu)},
  42. {name: "minimax coding ok", account: codingAccount(PlatformMiniMax)},
  43. }
  44. for _, tc := range cases {
  45. t.Run(tc.name, func(t *testing.T) {
  46. err := validateCodingPlanAccount(tc.account)
  47. if tc.wantReason == "" {
  48. require.NoError(t, err)
  49. return
  50. }
  51. requireReason(t, err, tc.wantReason)
  52. })
  53. }
  54. }
  55. func TestValidatePayGAccount_Matrix(t *testing.T) {
  56. cases := []struct {
  57. name string
  58. account *Account
  59. wantReason string
  60. }{
  61. {name: "nil", account: nil, wantReason: "CN_BALANCE_ACCOUNT_NOT_FOUND"},
  62. {name: "non cn provider", account: &Account{ID: 3, Platform: PlatformAnthropic}, wantReason: "CN_BALANCE_INVALID_PLATFORM"},
  63. {name: "coding has no balance endpoint", account: codingAccount(PlatformKimi), wantReason: "CN_BALANCE_CODING_PLAN"},
  64. {name: "kimi payg ok", account: paygAccount(PlatformKimi)},
  65. {name: "deepseek payg ok", account: paygAccount(PlatformDeepseek)},
  66. }
  67. for _, tc := range cases {
  68. t.Run(tc.name, func(t *testing.T) {
  69. err := validatePayGAccount(tc.account)
  70. if tc.wantReason == "" {
  71. require.NoError(t, err)
  72. return
  73. }
  74. requireReason(t, err, tc.wantReason)
  75. })
  76. }
  77. }
  78. // 直传入口的校验在 singleflight/上游请求之前:无效账号必须零出站请求。
  79. func TestCNProviderQuotaService_QueryUsageForAccount_RejectsInvalidAccount(t *testing.T) {
  80. repo := &fakeCNProbeAccountRepo{}
  81. upstream := &recordingHTTPUpstream{}
  82. svc := NewCNProviderQuotaService(repo, nil, upstream, nil)
  83. _, err := svc.QueryUsageForAccount(context.Background(), paygAccount(PlatformKimi))
  84. requireReason(t, err, "CN_QUOTA_NOT_CODING_PLAN")
  85. require.Zero(t, upstream.calls)
  86. _, err = svc.QueryUsageForAccount(context.Background(), nil)
  87. requireReason(t, err, "CN_QUOTA_ACCOUNT_NOT_FOUND")
  88. require.Zero(t, upstream.calls)
  89. }
  90. func TestCNProviderBalanceService_QueryBalanceForAccount_RejectsInvalidAccount(t *testing.T) {
  91. repo := &fakeCNProbeAccountRepo{}
  92. upstream := &recordingHTTPUpstream{}
  93. svc := NewCNProviderBalanceService(repo, nil, upstream, nil)
  94. _, err := svc.QueryBalanceForAccount(context.Background(), codingAccount(PlatformKimi))
  95. requireReason(t, err, "CN_BALANCE_CODING_PLAN")
  96. require.Zero(t, upstream.calls)
  97. _, err = svc.QueryBalanceForAccount(context.Background(), &Account{ID: 9, Platform: PlatformAnthropic})
  98. requireReason(t, err, "CN_BALANCE_INVALID_PLATFORM")
  99. require.Zero(t, upstream.calls)
  100. }
  101. // ID 入口与 ForAccount 入口对同一账号的行为一致(loadCodingPlanAccount 的
  102. // 加载后校验 = validateCodingPlanAccount;余额侧对称)。
  103. func TestCNProviderServices_IDEntryAppliesSameValidation(t *testing.T) {
  104. repo := &fakeCNProbeAccountRepo{account: paygAccount(PlatformKimi)}
  105. upstream := &recordingHTTPUpstream{}
  106. svc := NewCNProviderQuotaService(repo, nil, upstream, nil)
  107. _, err := svc.QueryUsage(context.Background(), 2)
  108. requireReason(t, err, "CN_QUOTA_NOT_CODING_PLAN")
  109. require.Zero(t, upstream.calls)
  110. }