298 lines
10 KiB
Go
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))
|
|
}
|