test(group): 补充分组列表可用账号数与总账号数统计正确性的集成测试

修复 #2579 报告的可用账号数等于总数问题:
上游已通过 loadAccountCounts / GetAccountCount 两处 SQL 中的
  COUNT(*) FILTER (WHERE status='active' AND schedulable=true)
正确区分可用账号,但缺少覆盖 active < total 场景的测试,
导致回归容易被忽略。

新增三个集成测试:
- TestListWithFilters_ActiveAccountCount_LessThanTotal
    含 active+schedulable、disabled、active+unschedulable 三类账号,
    断言 AccountCount=3、ActiveAccountCount=1,
    并验证 GetAccountCount 返回值与 ListWithFilters 字段一致。
- TestListWithFilters_RateLimitedAccountCount
    验证 rate_limit_reset_at 未过期的账号计入 ActiveAccountCount(仍可调度),
    同时单独出现在 RateLimitedAccountCount 中。
- TestListWithAccountCountSort_AttachesActiveCount
    通过 SortBy=account_count 触发 listWithAccountCountSort 路径,
    验证排序按 total 而非 active,且两个字段均被正确附加。

Fixes #2579
This commit is contained in:
wucm667 2026-05-20 11:33:29 +08:00
parent 91da815993
commit 5465003d07
2 changed files with 179 additions and 0 deletions

View File

@ -651,6 +651,124 @@ func (s *GroupRepoSuite) TestGetAccountCount_Empty() {
s.Require().Zero(count)
}
// TestListWithFilters_ActiveAccountCount_LessThanTotal 验证 ActiveAccountCount 正确区分可用与不可用账号。
// 当分组内存在 disabled 或 schedulable=false 的账号时ActiveAccountCount 必须小于 AccountCount
// 且与 GetAccountCount 返回的 active 值一致。
func (s *GroupRepoSuite) TestListWithFilters_ActiveAccountCount_LessThanTotal() {
g := &service.Group{
Name: "g-mixed-status",
Platform: service.PlatformAnthropic,
RateMultiplier: 1.0,
IsExclusive: false,
Status: service.StatusActive,
SubscriptionType: service.SubscriptionTypeStandard,
}
s.Require().NoError(s.repo.Create(s.ctx, g))
insertAccount := func(name, status string, schedulable bool) int64 {
var id int64
s.Require().NoError(scanSingleRow(
s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, status, schedulable) VALUES ($1, $2, $3, $4, $5) RETURNING id",
[]any{name, service.PlatformAnthropic, service.AccountTypeOAuth, status, schedulable},
&id,
))
return id
}
link := func(accountID int64, priority int) {
_, err := s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
accountID, g.ID, priority)
s.Require().NoError(err)
}
// account 1: active + schedulable → counts toward both total and active
link(insertAccount("acc-active-sched", service.StatusActive, true), 1)
// account 2: disabled → counts toward total only
link(insertAccount("acc-disabled", service.StatusDisabled, true), 2)
// account 3: active + not schedulable → counts toward total only
link(insertAccount("acc-unschedulable", service.StatusActive, false), 3)
// --- ListWithFilters path ---
isExclusive := false
groups, _, err := s.repo.ListWithFilters(s.ctx,
pagination.PaginationParams{Page: 1, PageSize: 100},
service.PlatformAnthropic, service.StatusActive, "", &isExclusive)
s.Require().NoError(err)
var found *service.Group
for i := range groups {
if groups[i].ID == g.ID {
found = &groups[i]
break
}
}
s.Require().NotNil(found, "created group must appear in ListWithFilters result")
s.Assert().Equal(int64(3), found.AccountCount, "AccountCount must count all 3 accounts")
s.Assert().Equal(int64(1), found.ActiveAccountCount, "ActiveAccountCount must count only the active+schedulable account")
// --- GetAccountCount must return identical values ---
total, active, err := s.repo.GetAccountCount(s.ctx, g.ID)
s.Require().NoError(err)
s.Assert().Equal(found.AccountCount, total, "GetAccountCount total must match ListWithFilters AccountCount")
s.Assert().Equal(found.ActiveAccountCount, active, "GetAccountCount active must match ListWithFilters ActiveAccountCount")
}
// TestListWithFilters_RateLimitedAccountCount 验证 RateLimitedAccountCount 正确统计临时受限账号。
// 受限账号rate_limit_reset_at 尚未过期)仍然计入 ActiveAccountCount
// 同时额外出现在 RateLimitedAccountCount 中。
func (s *GroupRepoSuite) TestListWithFilters_RateLimitedAccountCount() {
g := &service.Group{
Name: "g-rate-limited",
Platform: service.PlatformAnthropic,
RateMultiplier: 1.0,
IsExclusive: false,
Status: service.StatusActive,
SubscriptionType: service.SubscriptionTypeStandard,
}
s.Require().NoError(s.repo.Create(s.ctx, g))
var normalID int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type) VALUES ($1, $2, $3) RETURNING id",
[]any{"acc-normal", service.PlatformAnthropic, service.AccountTypeOAuth},
&normalID))
var rateLimitedID int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, rate_limit_reset_at) VALUES ($1, $2, $3, NOW() + INTERVAL '1 hour') RETURNING id",
[]any{"acc-rate-limited", service.PlatformAnthropic, service.AccountTypeOAuth},
&rateLimitedID))
_, err := s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
normalID, g.ID, 1)
s.Require().NoError(err)
_, err = s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
rateLimitedID, g.ID, 2)
s.Require().NoError(err)
isExclusive := false
groups, _, err := s.repo.ListWithFilters(s.ctx,
pagination.PaginationParams{Page: 1, PageSize: 100},
service.PlatformAnthropic, service.StatusActive, "", &isExclusive)
s.Require().NoError(err)
var found *service.Group
for i := range groups {
if groups[i].ID == g.ID {
found = &groups[i]
break
}
}
s.Require().NotNil(found, "created group must appear in ListWithFilters result")
s.Assert().Equal(int64(2), found.AccountCount, "AccountCount must be 2")
// rate-limited account is still active+schedulable, so it counts toward active
s.Assert().Equal(int64(2), found.ActiveAccountCount, "rate-limited account still counts as active")
s.Assert().Equal(int64(1), found.RateLimitedAccountCount, "RateLimitedAccountCount must be 1")
}
// --- DeleteAccountGroupsByGroupID ---
func (s *GroupRepoSuite) TestDeleteAccountGroupsByGroupID() {

View File

@ -7,6 +7,67 @@ import (
"github.com/Wei-Shaw/sub2api/internal/service"
)
// TestListWithAccountCountSort_AttachesActiveCount 验证通过 account_count 排序时,
// ActiveAccountCount 与 AccountCount 都被正确附加到返回结果中,
// 且排序基于 total 账号数而非 active 账号数。
func (s *GroupRepoSuite) TestListWithAccountCountSort_AttachesActiveCount() {
// Group A: 2 total, 1 active (1 disabled account)
gA := &service.Group{Name: "sort-count-a", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard}
// Group B: 1 total, 1 active
gB := &service.Group{Name: "sort-count-b", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard}
s.Require().NoError(s.repo.Create(s.ctx, gA))
s.Require().NoError(s.repo.Create(s.ctx, gB))
insertAccount := func(name, status string) int64 {
var id int64
s.Require().NoError(scanSingleRow(s.ctx, s.tx,
"INSERT INTO accounts (name, platform, type, status) VALUES ($1, $2, $3, $4) RETURNING id",
[]any{name, service.PlatformAnthropic, service.AccountTypeOAuth, status},
&id))
return id
}
link := func(accountID, groupID int64, priority int) {
_, err := s.tx.ExecContext(s.ctx,
"INSERT INTO account_groups (account_id, group_id, priority, created_at) VALUES ($1, $2, $3, NOW())",
accountID, groupID, priority)
s.Require().NoError(err)
}
// gA: 1 active + 1 disabled → total=2, active=1
link(insertAccount("sa-active", service.StatusActive), gA.ID, 1)
link(insertAccount("sa-disabled", service.StatusDisabled), gA.ID, 2)
// gB: 1 active → total=1, active=1
link(insertAccount("sb-active", service.StatusActive), gB.ID, 1)
groups, _, err := s.repo.ListWithFilters(s.ctx, pagination.PaginationParams{
Page: 1, PageSize: 100, SortBy: "account_count", SortOrder: "desc",
}, service.PlatformAnthropic, service.StatusActive, "", nil)
s.Require().NoError(err)
byID := make(map[int64]service.Group, len(groups))
for _, g := range groups {
byID[g.ID] = g
}
s.Require().Contains(byID, gA.ID, "gA must appear in results")
s.Require().Contains(byID, gB.ID, "gB must appear in results")
cA := byID[gA.ID]
s.Assert().Equal(int64(2), cA.AccountCount, "gA AccountCount must be 2")
s.Assert().Equal(int64(1), cA.ActiveAccountCount, "gA ActiveAccountCount must be 1")
cB := byID[gB.ID]
s.Assert().Equal(int64(1), cB.AccountCount, "gB AccountCount must be 1")
s.Assert().Equal(int64(1), cB.ActiveAccountCount, "gB ActiveAccountCount must be 1")
// Sort is by total (not active): gA (total=2) must rank higher than gB (total=1) in desc order
indexByID := make(map[int64]int, len(groups))
for i, g := range groups {
indexByID[g.ID] = i
}
s.Assert().Less(indexByID[gA.ID], indexByID[gB.ID], "gA (total=2) must rank above gB (total=1) with account_count desc")
}
func (s *GroupRepoSuite) TestList_DefaultSortBySortOrderAsc() {
g1 := &service.Group{Name: "g1", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 20}
g2 := &service.Group{Name: "g2", Platform: service.PlatformAnthropic, RateMultiplier: 1, Status: service.StatusActive, SubscriptionType: service.SubscriptionTypeStandard, SortOrder: 10}