2026-01-05 17:07:29 +08:00
|
|
|
|
//go:build unit
|
|
|
|
|
|
|
|
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/pkg/pagination"
|
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// groupRepoStubForAdmin 用于测试 AdminService 的 GroupRepository Stub
|
|
|
|
|
|
type groupRepoStubForAdmin struct {
|
|
|
|
|
|
created *Group // 记录 Create 调用的参数
|
|
|
|
|
|
updated *Group // 记录 Update 调用的参数
|
|
|
|
|
|
getByID *Group // GetByID 返回值
|
|
|
|
|
|
getErr error // GetByID 返回的错误
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) Create(_ context.Context, g *Group) error {
|
|
|
|
|
|
s.created = g
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) Update(_ context.Context, g *Group) error {
|
|
|
|
|
|
s.updated = g
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) GetByID(_ context.Context, _ int64) (*Group, error) {
|
|
|
|
|
|
if s.getErr != nil {
|
|
|
|
|
|
return nil, s.getErr
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.getByID, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-09 23:01:42 +08:00
|
|
|
|
func (s *groupRepoStubForAdmin) GetByIDLite(_ context.Context, _ int64) (*Group, error) {
|
|
|
|
|
|
if s.getErr != nil {
|
|
|
|
|
|
return nil, s.getErr
|
|
|
|
|
|
}
|
|
|
|
|
|
return s.getByID, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-05 17:07:29 +08:00
|
|
|
|
func (s *groupRepoStubForAdmin) Delete(_ context.Context, _ int64) error {
|
|
|
|
|
|
panic("unexpected Delete call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) DeleteCascade(_ context.Context, _ int64) ([]int64, error) {
|
|
|
|
|
|
panic("unexpected DeleteCascade call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) List(_ context.Context, _ pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
|
|
|
|
|
|
panic("unexpected List call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) {
|
|
|
|
|
|
panic("unexpected ListWithFilters call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) ListActive(_ context.Context) ([]Group, error) {
|
|
|
|
|
|
panic("unexpected ListActive call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) ListActiveByPlatform(_ context.Context, _ string) ([]Group, error) {
|
|
|
|
|
|
panic("unexpected ListActiveByPlatform call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) ExistsByName(_ context.Context, _ string) (bool, error) {
|
|
|
|
|
|
panic("unexpected ExistsByName call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) GetAccountCount(_ context.Context, _ int64) (int64, error) {
|
|
|
|
|
|
panic("unexpected GetAccountCount call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForAdmin) DeleteAccountGroupsByGroupID(_ context.Context, _ int64) (int64, error) {
|
|
|
|
|
|
panic("unexpected DeleteAccountGroupsByGroupID call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminService_CreateGroup_WithImagePricing 测试创建分组时 ImagePrice 字段正确传递
|
|
|
|
|
|
func TestAdminService_CreateGroup_WithImagePricing(t *testing.T) {
|
|
|
|
|
|
repo := &groupRepoStubForAdmin{}
|
|
|
|
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
|
|
|
|
|
|
|
|
price1K := 0.10
|
|
|
|
|
|
price2K := 0.15
|
|
|
|
|
|
price4K := 0.30
|
|
|
|
|
|
|
|
|
|
|
|
input := &CreateGroupInput{
|
|
|
|
|
|
Name: "test-group",
|
|
|
|
|
|
Description: "Test group",
|
|
|
|
|
|
Platform: PlatformAntigravity,
|
|
|
|
|
|
RateMultiplier: 1.0,
|
|
|
|
|
|
ImagePrice1K: &price1K,
|
|
|
|
|
|
ImagePrice2K: &price2K,
|
|
|
|
|
|
ImagePrice4K: &price4K,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
group, err := svc.CreateGroup(context.Background(), input)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotNil(t, group)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 repo 收到了正确的字段
|
|
|
|
|
|
require.NotNil(t, repo.created)
|
|
|
|
|
|
require.NotNil(t, repo.created.ImagePrice1K)
|
|
|
|
|
|
require.NotNil(t, repo.created.ImagePrice2K)
|
|
|
|
|
|
require.NotNil(t, repo.created.ImagePrice4K)
|
|
|
|
|
|
require.InDelta(t, 0.10, *repo.created.ImagePrice1K, 0.0001)
|
|
|
|
|
|
require.InDelta(t, 0.15, *repo.created.ImagePrice2K, 0.0001)
|
|
|
|
|
|
require.InDelta(t, 0.30, *repo.created.ImagePrice4K, 0.0001)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminService_CreateGroup_NilImagePricing 测试 ImagePrice 为 nil 时正常创建
|
|
|
|
|
|
func TestAdminService_CreateGroup_NilImagePricing(t *testing.T) {
|
|
|
|
|
|
repo := &groupRepoStubForAdmin{}
|
|
|
|
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
|
|
|
|
|
|
|
|
input := &CreateGroupInput{
|
|
|
|
|
|
Name: "test-group",
|
|
|
|
|
|
Description: "Test group",
|
|
|
|
|
|
Platform: PlatformAntigravity,
|
|
|
|
|
|
RateMultiplier: 1.0,
|
|
|
|
|
|
// ImagePrice 字段全部为 nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
group, err := svc.CreateGroup(context.Background(), input)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotNil(t, group)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 ImagePrice 字段为 nil
|
|
|
|
|
|
require.NotNil(t, repo.created)
|
|
|
|
|
|
require.Nil(t, repo.created.ImagePrice1K)
|
|
|
|
|
|
require.Nil(t, repo.created.ImagePrice2K)
|
|
|
|
|
|
require.Nil(t, repo.created.ImagePrice4K)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminService_UpdateGroup_WithImagePricing 测试更新分组时 ImagePrice 字段正确更新
|
|
|
|
|
|
func TestAdminService_UpdateGroup_WithImagePricing(t *testing.T) {
|
|
|
|
|
|
existingGroup := &Group{
|
|
|
|
|
|
ID: 1,
|
|
|
|
|
|
Name: "existing-group",
|
|
|
|
|
|
Platform: PlatformAntigravity,
|
|
|
|
|
|
Status: StatusActive,
|
|
|
|
|
|
}
|
|
|
|
|
|
repo := &groupRepoStubForAdmin{getByID: existingGroup}
|
|
|
|
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
|
|
|
|
|
|
|
|
price1K := 0.12
|
|
|
|
|
|
price2K := 0.18
|
|
|
|
|
|
price4K := 0.36
|
|
|
|
|
|
|
|
|
|
|
|
input := &UpdateGroupInput{
|
|
|
|
|
|
ImagePrice1K: &price1K,
|
|
|
|
|
|
ImagePrice2K: &price2K,
|
|
|
|
|
|
ImagePrice4K: &price4K,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
group, err := svc.UpdateGroup(context.Background(), 1, input)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotNil(t, group)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证 repo 收到了更新后的字段
|
|
|
|
|
|
require.NotNil(t, repo.updated)
|
|
|
|
|
|
require.NotNil(t, repo.updated.ImagePrice1K)
|
|
|
|
|
|
require.NotNil(t, repo.updated.ImagePrice2K)
|
|
|
|
|
|
require.NotNil(t, repo.updated.ImagePrice4K)
|
|
|
|
|
|
require.InDelta(t, 0.12, *repo.updated.ImagePrice1K, 0.0001)
|
|
|
|
|
|
require.InDelta(t, 0.18, *repo.updated.ImagePrice2K, 0.0001)
|
|
|
|
|
|
require.InDelta(t, 0.36, *repo.updated.ImagePrice4K, 0.0001)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestAdminService_UpdateGroup_PartialImagePricing 测试仅更新部分 ImagePrice 字段
|
|
|
|
|
|
func TestAdminService_UpdateGroup_PartialImagePricing(t *testing.T) {
|
|
|
|
|
|
oldPrice2K := 0.15
|
|
|
|
|
|
existingGroup := &Group{
|
|
|
|
|
|
ID: 1,
|
|
|
|
|
|
Name: "existing-group",
|
|
|
|
|
|
Platform: PlatformAntigravity,
|
|
|
|
|
|
Status: StatusActive,
|
|
|
|
|
|
ImagePrice2K: &oldPrice2K, // 已有 2K 价格
|
|
|
|
|
|
}
|
|
|
|
|
|
repo := &groupRepoStubForAdmin{getByID: existingGroup}
|
|
|
|
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
|
|
|
|
|
|
|
|
// 只更新 1K 价格
|
|
|
|
|
|
price1K := 0.10
|
|
|
|
|
|
input := &UpdateGroupInput{
|
|
|
|
|
|
ImagePrice1K: &price1K,
|
|
|
|
|
|
// ImagePrice2K 和 ImagePrice4K 为 nil,不更新
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
group, err := svc.UpdateGroup(context.Background(), 1, input)
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
require.NotNil(t, group)
|
|
|
|
|
|
|
|
|
|
|
|
// 验证:1K 被更新,2K 保持原值,4K 仍为 nil
|
|
|
|
|
|
require.NotNil(t, repo.updated)
|
|
|
|
|
|
require.NotNil(t, repo.updated.ImagePrice1K)
|
|
|
|
|
|
require.InDelta(t, 0.10, *repo.updated.ImagePrice1K, 0.0001)
|
|
|
|
|
|
require.NotNil(t, repo.updated.ImagePrice2K)
|
|
|
|
|
|
require.InDelta(t, 0.15, *repo.updated.ImagePrice2K, 0.0001) // 原值保持
|
|
|
|
|
|
require.Nil(t, repo.updated.ImagePrice4K)
|
|
|
|
|
|
}
|
2026-01-10 07:56:50 +08:00
|
|
|
|
|
|
|
|
|
|
func TestAdminService_ValidateFallbackGroup_DetectsCycle(t *testing.T) {
|
|
|
|
|
|
groupID := int64(1)
|
|
|
|
|
|
fallbackID := int64(2)
|
|
|
|
|
|
repo := &groupRepoStubForFallbackCycle{
|
|
|
|
|
|
groups: map[int64]*Group{
|
|
|
|
|
|
groupID: {
|
|
|
|
|
|
ID: groupID,
|
|
|
|
|
|
FallbackGroupID: &fallbackID,
|
|
|
|
|
|
},
|
|
|
|
|
|
fallbackID: {
|
|
|
|
|
|
ID: fallbackID,
|
|
|
|
|
|
FallbackGroupID: &groupID,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
svc := &adminServiceImpl{groupRepo: repo}
|
|
|
|
|
|
|
|
|
|
|
|
err := svc.validateFallbackGroup(context.Background(), groupID, fallbackID)
|
|
|
|
|
|
require.Error(t, err)
|
|
|
|
|
|
require.Contains(t, err.Error(), "fallback group cycle")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type groupRepoStubForFallbackCycle struct {
|
|
|
|
|
|
groups map[int64]*Group
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) Create(_ context.Context, _ *Group) error {
|
|
|
|
|
|
panic("unexpected Create call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) Update(_ context.Context, _ *Group) error {
|
|
|
|
|
|
panic("unexpected Update call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) GetByID(ctx context.Context, id int64) (*Group, error) {
|
|
|
|
|
|
return s.GetByIDLite(ctx, id)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) GetByIDLite(_ context.Context, id int64) (*Group, error) {
|
|
|
|
|
|
if g, ok := s.groups[id]; ok {
|
|
|
|
|
|
return g, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil, ErrGroupNotFound
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) Delete(_ context.Context, _ int64) error {
|
|
|
|
|
|
panic("unexpected Delete call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) DeleteCascade(_ context.Context, _ int64) ([]int64, error) {
|
|
|
|
|
|
panic("unexpected DeleteCascade call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) List(_ context.Context, _ pagination.PaginationParams) ([]Group, *pagination.PaginationResult, error) {
|
|
|
|
|
|
panic("unexpected List call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) ListWithFilters(_ context.Context, _ pagination.PaginationParams, _, _ string, _ *bool) ([]Group, *pagination.PaginationResult, error) {
|
|
|
|
|
|
panic("unexpected ListWithFilters call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) ListActive(_ context.Context) ([]Group, error) {
|
|
|
|
|
|
panic("unexpected ListActive call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) ListActiveByPlatform(_ context.Context, _ string) ([]Group, error) {
|
|
|
|
|
|
panic("unexpected ListActiveByPlatform call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) ExistsByName(_ context.Context, _ string) (bool, error) {
|
|
|
|
|
|
panic("unexpected ExistsByName call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) GetAccountCount(_ context.Context, _ int64) (int64, error) {
|
|
|
|
|
|
panic("unexpected GetAccountCount call")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (s *groupRepoStubForFallbackCycle) DeleteAccountGroupsByGroupID(_ context.Context, _ int64) (int64, error) {
|
|
|
|
|
|
panic("unexpected DeleteAccountGroupsByGroupID call")
|
|
|
|
|
|
}
|