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
126 lines
4.5 KiB
Go
126 lines
4.5 KiB
Go
package service
|
||
|
||
// ForAccount 直传入口(P2-6)的校验回归测试:
|
||
// QueryUsageForAccount / QueryBalanceForAccount 接受已加载的 *Account,
|
||
// 但必须复用与 ID 入口相同的加载后校验——直传不能绕过平台/模式检查,
|
||
// 且校验在 singleflight 之前完成(无效账号不得发起任何上游请求)。
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
func codingAccount(platform string) *Account {
|
||
return &Account{
|
||
ID: 1, Platform: platform, Type: AccountTypeAPIKey, Status: StatusActive,
|
||
Credentials: map[string]any{"account_mode": AccountModeCoding, "api_key": "sk-test"},
|
||
}
|
||
}
|
||
|
||
func paygAccount(platform string) *Account {
|
||
return &Account{
|
||
ID: 2, Platform: platform, Type: AccountTypeAPIKey, Status: StatusActive,
|
||
Credentials: map[string]any{"account_mode": AccountModePayG, "api_key": "sk-test"},
|
||
}
|
||
}
|
||
|
||
func requireReason(t *testing.T, err error, reason string) {
|
||
t.Helper()
|
||
require.Error(t, err)
|
||
var appErr *infraerrors.ApplicationError
|
||
require.ErrorAs(t, err, &appErr)
|
||
require.Equal(t, reason, appErr.Reason)
|
||
}
|
||
|
||
func TestValidateCodingPlanAccount_Matrix(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
account *Account
|
||
wantReason string
|
||
}{
|
||
{name: "nil", account: nil, wantReason: "CN_QUOTA_ACCOUNT_NOT_FOUND"},
|
||
{name: "non cn provider", account: &Account{ID: 3, Platform: PlatformAnthropic}, wantReason: "CN_QUOTA_INVALID_PLATFORM"},
|
||
{name: "payg has no quota endpoint", account: paygAccount(PlatformKimi), wantReason: "CN_QUOTA_NOT_CODING_PLAN"},
|
||
{name: "kimi coding ok", account: codingAccount(PlatformKimi)},
|
||
{name: "zhipu coding ok", account: codingAccount(PlatformZhipu)},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
err := validateCodingPlanAccount(tc.account)
|
||
if tc.wantReason == "" {
|
||
require.NoError(t, err)
|
||
return
|
||
}
|
||
requireReason(t, err, tc.wantReason)
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestValidatePayGAccount_Matrix(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
account *Account
|
||
wantReason string
|
||
}{
|
||
{name: "nil", account: nil, wantReason: "CN_BALANCE_ACCOUNT_NOT_FOUND"},
|
||
{name: "non cn provider", account: &Account{ID: 3, Platform: PlatformAnthropic}, wantReason: "CN_BALANCE_INVALID_PLATFORM"},
|
||
{name: "coding has no balance endpoint", account: codingAccount(PlatformKimi), wantReason: "CN_BALANCE_CODING_PLAN"},
|
||
{name: "kimi payg ok", account: paygAccount(PlatformKimi)},
|
||
{name: "deepseek payg ok", account: paygAccount(PlatformDeepseek)},
|
||
}
|
||
for _, tc := range cases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
err := validatePayGAccount(tc.account)
|
||
if tc.wantReason == "" {
|
||
require.NoError(t, err)
|
||
return
|
||
}
|
||
requireReason(t, err, tc.wantReason)
|
||
})
|
||
}
|
||
}
|
||
|
||
// 直传入口的校验在 singleflight/上游请求之前:无效账号必须零出站请求。
|
||
func TestCNProviderQuotaService_QueryUsageForAccount_RejectsInvalidAccount(t *testing.T) {
|
||
repo := &fakeCNProbeAccountRepo{}
|
||
upstream := &recordingHTTPUpstream{}
|
||
svc := NewCNProviderQuotaService(repo, nil, upstream, nil)
|
||
|
||
_, err := svc.QueryUsageForAccount(context.Background(), paygAccount(PlatformKimi))
|
||
requireReason(t, err, "CN_QUOTA_NOT_CODING_PLAN")
|
||
require.Zero(t, upstream.calls)
|
||
|
||
_, err = svc.QueryUsageForAccount(context.Background(), nil)
|
||
requireReason(t, err, "CN_QUOTA_ACCOUNT_NOT_FOUND")
|
||
require.Zero(t, upstream.calls)
|
||
}
|
||
|
||
func TestCNProviderBalanceService_QueryBalanceForAccount_RejectsInvalidAccount(t *testing.T) {
|
||
repo := &fakeCNProbeAccountRepo{}
|
||
upstream := &recordingHTTPUpstream{}
|
||
svc := NewCNProviderBalanceService(repo, nil, upstream, nil)
|
||
|
||
_, err := svc.QueryBalanceForAccount(context.Background(), codingAccount(PlatformKimi))
|
||
requireReason(t, err, "CN_BALANCE_CODING_PLAN")
|
||
require.Zero(t, upstream.calls)
|
||
|
||
_, err = svc.QueryBalanceForAccount(context.Background(), &Account{ID: 9, Platform: PlatformAnthropic})
|
||
requireReason(t, err, "CN_BALANCE_INVALID_PLATFORM")
|
||
require.Zero(t, upstream.calls)
|
||
}
|
||
|
||
// ID 入口与 ForAccount 入口对同一账号的行为一致(loadCodingPlanAccount 的
|
||
// 加载后校验 = validateCodingPlanAccount;余额侧对称)。
|
||
func TestCNProviderServices_IDEntryAppliesSameValidation(t *testing.T) {
|
||
repo := &fakeCNProbeAccountRepo{account: paygAccount(PlatformKimi)}
|
||
upstream := &recordingHTTPUpstream{}
|
||
svc := NewCNProviderQuotaService(repo, nil, upstream, nil)
|
||
|
||
_, err := svc.QueryUsage(context.Background(), 2)
|
||
requireReason(t, err, "CN_QUOTA_NOT_CODING_PLAN")
|
||
require.Zero(t, upstream.calls)
|
||
}
|