topfans/backend/services/activityService/service/activity_service_test.go
2026-06-24 18:10:12 +08:00

298 lines
10 KiB
Go

package service
import (
"context"
"errors"
"testing"
"github.com/stretchr/testify/assert"
"github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/pkg/models"
pb "github.com/topfans/backend/pkg/proto/activity"
"github.com/topfans/backend/services/activityService/client"
"github.com/topfans/backend/services/activityService/repository"
"go.uber.org/zap"
)
func init() {
if logger.Logger == nil {
logger.Logger = zap.NewNop()
}
}
// -------------------- Mocks --------------------
// mockActivityRepo 内存版 ActivityRepository;只实现本测试需要的方法
type mockActivityRepo struct {
top3Stats []*models.ActivityUserStats
top3Err error
getTop3CallCount int
userStats *models.ActivityUserStats // 缺省返回(单用户场景)
statsByUserID map[int64]*models.ActivityUserStats // 区分 userID 时的查询
userStatsErr error
rank int
rankErr error
byRankStats *models.ActivityUserStats
byRankErr error
}
func (m *mockActivityRepo) GetTop3(activityID, starID int64) ([]*models.ActivityUserStats, error) {
m.getTop3CallCount++
if m.top3Err != nil {
return nil, m.top3Err
}
return m.top3Stats, nil
}
func (m *mockActivityRepo) GetUserStatsForRanking(activityID, userID, starID int64) (*models.ActivityUserStats, error) {
if m.userStatsErr != nil {
return nil, m.userStatsErr
}
if m.statsByUserID != nil {
if s, ok := m.statsByUserID[userID]; ok {
return s, nil
}
return nil, nil // 未找到视为未参与
}
return m.userStats, nil
}
func (m *mockActivityRepo) GetUserRank(userID, activityID, starID int64) (int, error) {
return m.rank, m.rankErr
}
func (m *mockActivityRepo) GetUserStatsByRank(activityID, starID int64, offset int) (*models.ActivityUserStats, error) {
return m.byRankStats, m.byRankErr
}
// 其他未使用方法(no-op,保证实现接口)
func (m *mockActivityRepo) CreateActivity(*models.Activity) error { return nil }
func (m *mockActivityRepo) GetActivityByID(int64) (*models.Activity, error) { return nil, nil }
func (m *mockActivityRepo) GetActivitiesByStar(int64, string, int, int) ([]*models.Activity, int64, error) {
return nil, 0, nil
}
func (m *mockActivityRepo) UpdateActivityProgress(int64, int64) error { return nil }
func (m *mockActivityRepo) GetActivityItems(int64) ([]*models.ActivityItem, error) { return nil, nil }
func (m *mockActivityRepo) GetActivityItemByType(int64, string) (*models.ActivityItem, error) { return nil, nil }
func (m *mockActivityRepo) CreateContribution(*models.ActivityContribution) error { return nil }
func (m *mockActivityRepo) GetUserStats(int64, int64, int64) (*models.ActivityUserStats, error) {
return m.userStats, m.userStatsErr
}
func (m *mockActivityRepo) UpdateUserStats(*models.ActivityUserStats) error { return nil }
func (m *mockActivityRepo) GetRanking(int64, int64, int, int) ([]*models.ActivityUserStats, int64, error) {
return nil, 0, nil
}
func (m *mockActivityRepo) GetLatestContributions(int64, int64, int64, int) ([]*models.ActivityContribution, error) {
return nil, nil
}
// mockUserRPC 内存版 UserRPCClient
type mockUserRPC struct {
profiles map[int64]*client.FanProfile
err error
}
func (m *mockUserRPC) GetFanProfile(userID, starID int64) (*client.FanProfile, error) {
if m.err != nil {
return nil, m.err
}
if p, ok := m.profiles[userID]; ok {
return p, nil
}
return nil, errors.New("profile not found")
}
func (m *mockUserRPC) UpdateCrystalBalance(int64, int64, int64) (int64, error) { return 0, nil }
// -------------------- Tests --------------------
func newTestService(repo repository.ActivityRepository, urpc client.UserRPCClient) *activityService {
return &activityService{
activityRepo: repo,
userRPCClient: urpc,
// cache=nil → getTop3WithCache 跳过缓存,直接走 DB
}
}
func TestGetTopRanking_UnrankedUser(t *testing.T) {
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 1001, TotalContribution: 900},
{UserID: 1002, TotalContribution: 800},
},
userStats: nil, // 未参与
}
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
}}
svc := newTestService(repo, urpc)
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100,
StarId: 7,
UserId: 2001,
})
assert.NoError(t, err)
assert.NotNil(t, resp)
assert.Equal(t, uint32(0), resp.Base.Code)
assert.Len(t, resp.Top3, 2)
assert.NotNil(t, resp.MyInfo)
assert.Equal(t, int32(0), resp.MyInfo.Rank)
assert.Equal(t, "unranked", resp.MyInfo.Status)
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev)
assert.Equal(t, "https://cdn/2001.jpg", resp.MyInfo.AvatarUrl,
"未上榜用户也应该拿到自己的头像")
}
func TestGetTopRanking_Rank1(t *testing.T) {
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 2001, TotalContribution: 1500},
},
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 1500},
rank: 1,
}
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
}}
svc := newTestService(repo, urpc)
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, StarId: 7, UserId: 2001,
})
assert.NoError(t, err)
assert.Equal(t, int32(1), resp.MyInfo.Rank)
assert.Equal(t, "ranked", resp.MyInfo.Status)
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev, "rank=1 时 gap 必为 0")
}
func TestGetTopRanking_RankInTop3(t *testing.T) {
// 我是 rank=3;top3 = [1001(900), 1002(800), 2001(700)]
// gap = 1002.contribution(800) - 2001.contribution(700) = 100
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 1001, TotalContribution: 900},
{UserID: 1002, TotalContribution: 800},
{UserID: 2001, TotalContribution: 700},
},
statsByUserID: map[int64]*models.ActivityUserStats{
2001: {UserID: 2001, TotalContribution: 700},
1002: {UserID: 1002, TotalContribution: 800}, // prev(我上一名)
},
rank: 3,
}
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
1001: {UserID: 1001, AvatarUrl: "https://cdn/1001.jpg"},
1002: {UserID: 1002, AvatarUrl: "https://cdn/1002.jpg"},
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
}}
svc := newTestService(repo, urpc)
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, StarId: 7, UserId: 2001,
})
assert.NoError(t, err)
assert.Equal(t, int32(3), resp.MyInfo.Rank)
assert.Equal(t, "ranked", resp.MyInfo.Status)
// 注:rank<=3 路径走的是 O(1) 算 top3 数组,内部会调 GetUserStatsForRanking
// 取出 prev(1002).contribution = 800, gap = 800 - 700 = 100
assert.Equal(t, int64(100), resp.MyInfo.GapToPrev)
}
func TestGetTopRanking_RankBeyondTop3(t *testing.T) {
// rank=10; top3 数组长度=3,需走 OFFSET 查询第 9 名(偏移 8)
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 1001, TotalContribution: 900},
{UserID: 1002, TotalContribution: 800},
{UserID: 1003, TotalContribution: 700},
},
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 100},
rank: 10,
byRankStats: &models.ActivityUserStats{UserID: 1099, TotalContribution: 500},
// gap = 500 - 100 = 400
}
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
1001: {UserID: 1001, AvatarUrl: "https://cdn/1001.jpg"},
1002: {UserID: 1002, AvatarUrl: "https://cdn/1002.jpg"},
1003: {UserID: 1003, AvatarUrl: "https://cdn/1003.jpg"},
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
}}
svc := newTestService(repo, urpc)
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, StarId: 7, UserId: 2001,
})
assert.NoError(t, err)
assert.Equal(t, int32(10), resp.MyInfo.Rank)
assert.Equal(t, int64(400), resp.MyInfo.GapToPrev, "rank>3 走 OFFSET 查询")
}
func TestGetTopRanking_GapClampedToZero(t *testing.T) {
// 自己贡献被并发刷成比上一名还高(异常);gap 应被 clamp 到 0
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 2001, TotalContribution: 9999},
},
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 9999},
rank: 1, // 自己第一名,gap 自然 0
}
urpc := &mockUserRPC{profiles: map[int64]*client.FanProfile{
2001: {UserID: 2001, AvatarUrl: "https://cdn/2001.jpg"},
}}
svc := newTestService(repo, urpc)
resp, _ := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, StarId: 7, UserId: 2001,
})
assert.Equal(t, int64(0), resp.MyInfo.GapToPrev)
assert.GreaterOrEqual(t, resp.MyInfo.GapToPrev, int64(0),
"gap 永远非负")
}
func TestGetTopRanking_FanProfileFailure(t *testing.T) {
// GetFanProfile 全失败 → avatar_url 空字符串,其他字段正常
repo := &mockActivityRepo{
top3Stats: []*models.ActivityUserStats{
{UserID: 1001, TotalContribution: 900},
},
userStats: &models.ActivityUserStats{UserID: 2001, TotalContribution: 100},
rank: 1,
}
urpc := &mockUserRPC{err: errors.New("rpc down")}
svc := newTestService(repo, urpc)
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, StarId: 7, UserId: 2001,
})
assert.NoError(t, err)
assert.Equal(t, uint32(0), resp.Base.Code, "RPC 失败不应阻塞接口")
for _, it := range resp.Top3 {
assert.Equal(t, "", it.AvatarUrl)
}
assert.Equal(t, "", resp.MyInfo.AvatarUrl)
}
func TestGetTopRanking_InvalidActivityID(t *testing.T) {
svc := newTestService(&mockActivityRepo{}, &mockUserRPC{})
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 0, UserId: 1,
})
assert.NoError(t, err)
assert.NotEqual(t, uint32(0), resp.Base.Code, "activity_id<=0 应返回错误")
}
func TestGetTopRanking_MissingUserID(t *testing.T) {
svc := newTestService(&mockActivityRepo{}, &mockUserRPC{})
resp, err := svc.GetTopRanking(context.Background(), &pb.TopRankingRequest{
ActivityId: 100, UserId: 0,
})
assert.NoError(t, err)
assert.NotEqual(t, uint32(0), resp.Base.Code, "user_id=0 应返回未授权")
}
func TestStarIDOrAll(t *testing.T) {
assert.Equal(t, "all", starIDOrAll(0))
assert.Equal(t, "all", starIDOrAll(-1))
assert.Equal(t, "123", starIDOrAll(123))
}