1316 lines
42 KiB
Go
1316 lines
42 KiB
Go
package voiceroomrocket
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/config"
|
|
"chatapp3-golang/internal/integration"
|
|
"chatapp3-golang/internal/model"
|
|
|
|
"github.com/alicebob/miniredis/v2"
|
|
"github.com/redis/go-redis/v9"
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestProcessGiftPayloadDropsOverflowAndLaunchesOnce(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
now := time.Now()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
|
seedRewardConfig(t, db, 1, rewardSceneTop1, "GOLD", 10)
|
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, "GOLD", 5)
|
|
|
|
firstPayload := `{
|
|
"trackId":"track-1",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":99,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(now) + `
|
|
}`
|
|
first, err := service.ProcessGiftPayload(ctx, firstPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(first) error = %v", err)
|
|
}
|
|
if !first.Processed || first.AcceptedEnergy != 99 || first.Launched {
|
|
t.Fatalf("first response = %+v", first)
|
|
}
|
|
|
|
secondPayload := `{
|
|
"trackId":"track-2",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1002",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"12","giftCandy":101,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(now.Add(time.Second)) + `
|
|
}`
|
|
second, err := service.ProcessGiftPayload(ctx, secondPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(second) error = %v", err)
|
|
}
|
|
if !second.Processed || second.AcceptedEnergy != 1 || !second.Launched {
|
|
t.Fatalf("second response = %+v", second)
|
|
}
|
|
|
|
var status model.VoiceRoomRocketStatus
|
|
if err := db.Where("sys_origin = ? AND room_id = ?", "LIKEI", int64(9001)).First(&status).Error; err != nil {
|
|
t.Fatalf("load status: %v", err)
|
|
}
|
|
if status.RoundNo != 1 || status.CurrentLevel != 2 || status.CurrentEnergy != 0 || status.NeedEnergy != 200 {
|
|
t.Fatalf("status = %+v, want round 1 level 2 energy 0 need 200", status)
|
|
}
|
|
|
|
var eventLog model.VoiceRoomRocketGiftEventLog
|
|
if err := db.Where("track_id = ?", "track-2").First(&eventLog).Error; err != nil {
|
|
t.Fatalf("load event log: %v", err)
|
|
}
|
|
if eventLog.RawEnergy != 101 || eventLog.AcceptedEnergy != 1 {
|
|
t.Fatalf("eventLog energy raw=%d accepted=%d, want 101/1", eventLog.RawEnergy, eventLog.AcceptedEnergy)
|
|
}
|
|
|
|
var launch model.VoiceRoomRocketLaunch
|
|
if err := db.Where("room_id = ? AND round_no = ? AND level = ?", int64(9001), 1, 1).First(&launch).Error; err != nil {
|
|
t.Fatalf("load launch: %v", err)
|
|
}
|
|
if launch.Top1UserID == nil || *launch.Top1UserID != 1001 || launch.IgniteUserID == nil || *launch.IgniteUserID != 1002 {
|
|
t.Fatalf("launch users top1=%v ignite=%v, want 1001/1002", launch.Top1UserID, launch.IgniteUserID)
|
|
}
|
|
|
|
var rewardCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketRewardRecord{}).Where("launch_id = ?", launch.ID).Count(&rewardCount).Error; err != nil {
|
|
t.Fatalf("count reward records: %v", err)
|
|
}
|
|
if rewardCount != 2 {
|
|
t.Fatalf("reward count = %d, want 2", rewardCount)
|
|
}
|
|
|
|
duplicate, err := service.ProcessGiftPayload(ctx, secondPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(duplicate) error = %v", err)
|
|
}
|
|
if duplicate.Reason != "duplicate_event" {
|
|
t.Fatalf("duplicate response = %+v, want duplicate_event", duplicate)
|
|
}
|
|
var launchCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketLaunch{}).Count(&launchCount).Error; err != nil {
|
|
t.Fatalf("count launches: %v", err)
|
|
}
|
|
if launchCount != 1 {
|
|
t.Fatalf("launch count = %d, want 1", launchCount)
|
|
}
|
|
}
|
|
|
|
func TestPageRoomRewardRecordsReturnsAllLaunchWinnersWithProfiles(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
gateway := newFakeRocketGateway()
|
|
gateway.profiles = map[int64]integration.UserProfile{
|
|
1001: {ID: integration.Int64Value(1001), Account: "1001026", UserNickname: "room1001026", UserAvatar: "https://avatar.example/1001.png"},
|
|
1002: {ID: integration.Int64Value(1002), Account: "1001027", UserNickname: "room1001027", UserAvatar: "https://avatar.example/1002.png"},
|
|
}
|
|
service.userProfiles = gateway
|
|
seedRocketConfig(t, db)
|
|
|
|
now := time.Now()
|
|
launch := model.VoiceRoomRocketLaunch{
|
|
ID: 1,
|
|
LaunchNo: "VRR-test-launch-1",
|
|
SysOrigin: "LIKEI",
|
|
RoomID: 9001,
|
|
DayKey: "2026-05-15",
|
|
RoundNo: 1,
|
|
Level: 3,
|
|
LaunchTime: now,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := db.Create(&launch).Error; err != nil {
|
|
t.Fatalf("create launch: %v", err)
|
|
}
|
|
rewardItemID := int64(2001)
|
|
expireDays := 15
|
|
rows := []model.VoiceRoomRocketRewardRecord{
|
|
{
|
|
ID: 11,
|
|
LaunchID: launch.ID,
|
|
LaunchNo: launch.LaunchNo,
|
|
SysOrigin: "LIKEI",
|
|
RoomID: launch.RoomID,
|
|
DayKey: launch.DayKey,
|
|
RoundNo: launch.RoundNo,
|
|
Level: launch.Level,
|
|
UserID: 1001,
|
|
RewardScene: rewardSceneTop1,
|
|
RewardType: "GOLD",
|
|
RewardName: "Yumi",
|
|
RewardAmount: 5000,
|
|
GrantEventID: "grant-event-11",
|
|
GrantStatus: rewardStatusSuccess,
|
|
PopupStatus: popupStatusUnread,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
},
|
|
{
|
|
ID: 12,
|
|
LaunchID: launch.ID,
|
|
LaunchNo: launch.LaunchNo,
|
|
SysOrigin: "LIKEI",
|
|
RoomID: launch.RoomID,
|
|
DayKey: launch.DayKey,
|
|
RoundNo: launch.RoundNo,
|
|
Level: launch.Level,
|
|
UserID: 1002,
|
|
RewardScene: rewardSceneInRoom,
|
|
RewardType: "AVATAR_FRAME",
|
|
RewardItemID: &rewardItemID,
|
|
RewardName: "Frame",
|
|
RewardCover: "https://reward.example/frame.png",
|
|
RewardAmount: 1,
|
|
ExpireDays: &expireDays,
|
|
GrantEventID: "grant-event-12",
|
|
GrantStatus: rewardStatusSuccess,
|
|
PopupStatus: popupStatusUnread,
|
|
CreateTime: now.Add(time.Second),
|
|
UpdateTime: now.Add(time.Second),
|
|
},
|
|
}
|
|
if err := db.Create(&rows).Error; err != nil {
|
|
t.Fatalf("create reward records: %v", err)
|
|
}
|
|
|
|
resp, err := service.PageRoomRewardRecords(ctx, AuthUser{UserID: 9999, SysOrigin: "LIKEI"}, launch.RoomID, launch.LaunchNo, 1, 20)
|
|
if err != nil {
|
|
t.Fatalf("PageRoomRewardRecords() error = %v", err)
|
|
}
|
|
if len(resp.Records) != 2 {
|
|
t.Fatalf("records len = %d, want 2: %+v", len(resp.Records), resp.Records)
|
|
}
|
|
users := map[int64]RewardRecordView{}
|
|
for _, record := range resp.Records {
|
|
users[record.UserID] = record
|
|
}
|
|
if got := users[1001]; got.Account != "1001026" || got.UserNickname != "room1001026" || got.UserAvatar == "" {
|
|
t.Fatalf("user 1001 profile = %+v", got)
|
|
}
|
|
if got := users[1002]; got.Account != "1001027" || got.UserNickname != "room1001027" || got.UserAvatar == "" {
|
|
t.Fatalf("user 1002 profile = %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeGiftEventPayloadEnvelope(t *testing.T) {
|
|
payload := `{"tag":"give_gift_v3","body":"{\"trackId\":\"t1\",\"sendUserId\":\"10\",\"roomId\":\"20\",\"giftValue\":{\"actualAmount\":\"30\"}}"}`
|
|
event, err := decodeGiftEventPayload(payload, "give_gift_v3")
|
|
if err != nil {
|
|
t.Fatalf("decodeGiftEventPayload() error = %v", err)
|
|
}
|
|
if event.TrackID.String() != "t1" || event.SendUserID.Int64() != 10 || event.RoomID.Int64() != 20 || calculateRawEnergy(event) != 30 {
|
|
t.Fatalf("event = %+v", event)
|
|
}
|
|
}
|
|
|
|
func TestBackpackGiftIncreasesRocketEnergy(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{1000, 2000, 3000, 4000, 5000, 6000})
|
|
|
|
payloads := []struct {
|
|
raw string
|
|
energy int64
|
|
}{
|
|
{
|
|
energy: 30,
|
|
raw: `{
|
|
"trackId":"track-root-bag-gift",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"bagGift":true,
|
|
"giftConfig":{"id":"11","giftCandy":30,"type":"GOLD","giftTab":"NORMAL"},
|
|
"giftValue":{"actualAmount":30},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`,
|
|
},
|
|
{
|
|
energy: 40,
|
|
raw: `{
|
|
"trackId":"track-value-bag-gift",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":40,"type":"GOLD","giftTab":"NORMAL"},
|
|
"giftValue":{"actualAmount":40,"bag":true},
|
|
"createTime":` + strconvFormatMillis(time.Now().Add(time.Second)) + `
|
|
}`,
|
|
},
|
|
}
|
|
for _, payload := range payloads {
|
|
processed, err := service.ProcessGiftPayload(ctx, payload.raw)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(backpack) error = %v", err)
|
|
}
|
|
if !processed.Processed || processed.AcceptedEnergy != payload.energy {
|
|
t.Fatalf("processed = %+v, want accepted energy %d", processed, payload.energy)
|
|
}
|
|
}
|
|
|
|
var status model.VoiceRoomRocketStatus
|
|
if err := db.Where("sys_origin = ? AND room_id = ?", "LIKEI", 9001).
|
|
First(&status).Error; err != nil {
|
|
t.Fatalf("find status: %v", err)
|
|
}
|
|
if status.CurrentEnergy != 70 {
|
|
t.Fatalf("current energy = %d, want 70", status.CurrentEnergy)
|
|
}
|
|
|
|
var logs []model.VoiceRoomRocketGiftEventLog
|
|
if err := db.Where("track_id IN ?", []string{"track-root-bag-gift", "track-value-bag-gift"}).
|
|
Find(&logs).Error; err != nil {
|
|
t.Fatalf("find logs: %v", err)
|
|
}
|
|
if len(logs) != 2 {
|
|
t.Fatalf("logs = %d, want 2", len(logs))
|
|
}
|
|
for _, log := range logs {
|
|
if log.Status != eventStatusSuccess || log.AcceptedEnergy <= 0 {
|
|
t.Fatalf("log = %+v, want success with accepted energy", log)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGiftEnergyRulesAllowBackpackAndConfiguredGift(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{1000, 2000, 3000, 4000, 5000, 6000})
|
|
|
|
backpackPayload := `{
|
|
"trackId":"track-backpack",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":99,"type":"GOLD","giftTab":"BACKPACK"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
backpack, err := service.ProcessGiftPayload(ctx, backpackPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(backpack) error = %v", err)
|
|
}
|
|
if !backpack.Processed || backpack.AcceptedEnergy != 99 {
|
|
t.Fatalf("backpack = %+v, want 99 energy", backpack)
|
|
}
|
|
|
|
if err := db.Model(&model.VoiceRoomRocketConfig{}).
|
|
Where("sys_origin = ?", "LIKEI").
|
|
Update("energy_rule_json", `{"giftRules":[{"giftId":"22","energy":7,"enabled":true}]}`).Error; err != nil {
|
|
t.Fatalf("update energy rule: %v", err)
|
|
}
|
|
customPayload := `{
|
|
"trackId":"track-custom",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":2,
|
|
"giftConfig":{"id":"22","giftCandy":99,"type":"MAGIC","giftTab":"MAGIC"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
accepted, err := service.ProcessGiftPayload(ctx, customPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(custom) error = %v", err)
|
|
}
|
|
if !accepted.Processed || accepted.AcceptedEnergy != 14 {
|
|
t.Fatalf("accepted = %+v, want 14 energy", accepted)
|
|
}
|
|
}
|
|
|
|
func TestGiftTabEnergyRuleDiscountsLuckyGiftOnly(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{1000, 2000, 3000, 4000, 5000, 6000})
|
|
|
|
if err := db.Model(&model.VoiceRoomRocketConfig{}).
|
|
Where("sys_origin = ?", "LIKEI").
|
|
Update("energy_rule_json", `{"giftTabRules":[{"giftTab":"LUCKY_GIFT","ratio":0.1}]}`).Error; err != nil {
|
|
t.Fatalf("update energy rule: %v", err)
|
|
}
|
|
|
|
luckyPayload := `{
|
|
"trackId":"track-lucky-ratio",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"LUCKY_GIFT"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
lucky, err := service.ProcessGiftPayload(ctx, luckyPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(lucky) error = %v", err)
|
|
}
|
|
if !lucky.Processed || lucky.AcceptedEnergy != 10 {
|
|
t.Fatalf("lucky = %+v, want 10 energy", lucky)
|
|
}
|
|
|
|
ordinaryPayload := `{
|
|
"trackId":"track-ordinary-ratio",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"12","giftCandy":100,"type":"GOLD","giftTab":"ORDINARY"},
|
|
"createTime":` + strconvFormatMillis(time.Now().Add(time.Second)) + `
|
|
}`
|
|
ordinary, err := service.ProcessGiftPayload(ctx, ordinaryPayload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload(ordinary) error = %v", err)
|
|
}
|
|
if !ordinary.Processed || ordinary.AcceptedEnergy != 100 {
|
|
t.Fatalf("ordinary = %+v, want 100 energy", ordinary)
|
|
}
|
|
}
|
|
|
|
func TestInRoomRewardWorkerSnapshotsSelectsAndGrants(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
|
seedRewardConfigWithCopies(t, db, 1, rewardSceneInRoom, rewardTypeGold, 1, 3)
|
|
|
|
roomID := int64(9001)
|
|
for _, userID := range []int64{1001, 1002, 1003, 1004} {
|
|
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
|
|
t.Fatalf("sadd online: %v", err)
|
|
}
|
|
if err := rdb.Set(ctx, userRoomKey("LIKEI", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set user room: %v", err)
|
|
}
|
|
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(time.Now().UnixMilli(), 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set seen: %v", err)
|
|
}
|
|
}
|
|
|
|
payload := `{
|
|
"trackId":"track-launch-in-room",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
|
}
|
|
if !resp.Processed || resp.Launched || resp.LaunchNo != "" {
|
|
t.Fatalf("launch should be delayed, response = %+v", resp)
|
|
}
|
|
|
|
launched, err := service.ProcessDueLaunches(ctx, time.Now().Add(11*time.Second), 10)
|
|
if err != nil {
|
|
t.Fatalf("ProcessDueLaunches() error = %v", err)
|
|
}
|
|
if launched != 1 {
|
|
t.Fatalf("due launches = %d, want 1", launched)
|
|
}
|
|
|
|
processed, err := service.ProcessDueInRoomRewards(ctx, time.Now().Add(30*time.Second), 10)
|
|
if err != nil {
|
|
t.Fatalf("ProcessDueInRoomRewards() error = %v", err)
|
|
}
|
|
if processed != 1 {
|
|
t.Fatalf("processed due = %d, want 1", processed)
|
|
}
|
|
|
|
var snapshotCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Count(&snapshotCount).Error; err != nil {
|
|
t.Fatalf("count snapshot: %v", err)
|
|
}
|
|
if snapshotCount != 4 {
|
|
t.Fatalf("snapshot count = %d, want 4", snapshotCount)
|
|
}
|
|
var selectedCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Where("selected = ?", true).Count(&selectedCount).Error; err != nil {
|
|
t.Fatalf("count selected: %v", err)
|
|
}
|
|
if selectedCount != 3 {
|
|
t.Fatalf("selected count = %d, want 3", selectedCount)
|
|
}
|
|
|
|
granted, err := service.ProcessPendingRewards(ctx, 20)
|
|
if err != nil {
|
|
t.Fatalf("ProcessPendingRewards() error = %v", err)
|
|
}
|
|
if granted != 5 {
|
|
t.Fatalf("granted = %d, want 5", granted)
|
|
}
|
|
var pending int64
|
|
if err := db.Model(&model.VoiceRoomRocketRewardRecord{}).Where("grant_status <> ?", rewardStatusSuccess).Count(&pending).Error; err != nil {
|
|
t.Fatalf("count pending: %v", err)
|
|
}
|
|
if pending != 0 {
|
|
t.Fatalf("pending rewards = %d, want 0", pending)
|
|
}
|
|
if len(gateway.goldEvents) != 5 {
|
|
t.Fatalf("gold grant count = %d, want 5", len(gateway.goldEvents))
|
|
}
|
|
}
|
|
|
|
func TestInRoomRewardWorkerSkipsUsersNoLongerInRoom(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
now := time.Now()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
|
seedRewardConfigWithCopies(t, db, 1, rewardSceneInRoom, rewardTypeGold, 1, 4)
|
|
|
|
roomID := int64(9001)
|
|
for _, userID := range []int64{1001, 1002} {
|
|
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
|
|
t.Fatalf("sadd active online: %v", err)
|
|
}
|
|
if err := rdb.Set(ctx, userRoomKey("LIKEI", userID), strconv.FormatInt(roomID, 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set active room: %v", err)
|
|
}
|
|
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(now.UnixMilli(), 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set active seen: %v", err)
|
|
}
|
|
}
|
|
staleUsers := []int64{1003, 1004}
|
|
for _, userID := range staleUsers {
|
|
if err := rdb.SAdd(ctx, onlineRoomKey("LIKEI", roomID), userID).Err(); err != nil {
|
|
t.Fatalf("sadd stale online: %v", err)
|
|
}
|
|
if err := rdb.Set(ctx, userSeenKey("LIKEI", userID), strconv.FormatInt(now.UnixMilli(), 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set stale seen: %v", err)
|
|
}
|
|
}
|
|
if err := rdb.Set(ctx, userRoomKey("LIKEI", staleUsers[1]), strconv.FormatInt(roomID+1, 10), time.Minute).Err(); err != nil {
|
|
t.Fatalf("set stale room: %v", err)
|
|
}
|
|
|
|
payload := `{
|
|
"trackId":"track-launch-in-room-stale",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(now) + `
|
|
}`
|
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
|
}
|
|
if !resp.Processed {
|
|
t.Fatalf("response = %+v, want processed", resp)
|
|
}
|
|
if _, err := service.ProcessDueLaunches(ctx, now.Add(11*time.Second), 10); err != nil {
|
|
t.Fatalf("ProcessDueLaunches() error = %v", err)
|
|
}
|
|
if _, err := service.ProcessDueInRoomRewards(ctx, now.Add(30*time.Second), 10); err != nil {
|
|
t.Fatalf("ProcessDueInRoomRewards() error = %v", err)
|
|
}
|
|
|
|
var snapshotCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Count(&snapshotCount).Error; err != nil {
|
|
t.Fatalf("count snapshot: %v", err)
|
|
}
|
|
if snapshotCount != 2 {
|
|
t.Fatalf("snapshot count = %d, want 2 active users only", snapshotCount)
|
|
}
|
|
for _, userID := range staleUsers {
|
|
var userSnapshotCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketAudienceSnapshot{}).Where("user_id = ?", userID).Count(&userSnapshotCount).Error; err != nil {
|
|
t.Fatalf("count stale snapshot: %v", err)
|
|
}
|
|
if userSnapshotCount != 0 {
|
|
t.Fatalf("stale user %d snapshot count = %d, want 0", userID, userSnapshotCount)
|
|
}
|
|
var rewardCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketRewardRecord{}).Where("user_id = ? AND reward_scene = ?", userID, rewardSceneInRoom).Count(&rewardCount).Error; err != nil {
|
|
t.Fatalf("count stale reward: %v", err)
|
|
}
|
|
if rewardCount != 0 {
|
|
t.Fatalf("stale user %d in-room reward count = %d, want 0", userID, rewardCount)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProcessPendingRewardsGrantsBadgeThroughBadgeGateway(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db := newTestService(t)
|
|
service.rewardGateway = gateway
|
|
|
|
ctx := context.Background()
|
|
badgeID := int64(3001)
|
|
expireDays := 14
|
|
record := model.VoiceRoomRocketRewardRecord{
|
|
ID: 8801,
|
|
LaunchID: 9901,
|
|
LaunchNo: "launch-badge",
|
|
SysOrigin: "LIKEI",
|
|
RoomID: 9001,
|
|
DayKey: "20260515",
|
|
RoundNo: 1,
|
|
Level: 1,
|
|
UserID: 1001,
|
|
RewardScene: rewardSceneTop1,
|
|
RewardConfigID: 7701,
|
|
RewardType: rewardTypeBadge,
|
|
RewardItemID: &badgeID,
|
|
RewardName: "activity badge",
|
|
RewardAmount: 1,
|
|
ExpireDays: &expireDays,
|
|
GrantEventID: "VOICE_ROOM_ROCKET:launch-badge:TOP1:1001:7701",
|
|
GrantStatus: rewardStatusPending,
|
|
PopupStatus: popupStatusUnread,
|
|
CreateTime: time.Now(),
|
|
UpdateTime: time.Now(),
|
|
}
|
|
if err := db.Create(&record).Error; err != nil {
|
|
t.Fatalf("seed reward record: %v", err)
|
|
}
|
|
|
|
granted, err := service.ProcessPendingRewards(ctx, 10)
|
|
if err != nil {
|
|
t.Fatalf("ProcessPendingRewards() error = %v", err)
|
|
}
|
|
if granted != 1 {
|
|
t.Fatalf("granted = %d, want 1", granted)
|
|
}
|
|
if len(gateway.badges) != 1 {
|
|
t.Fatalf("badge grants = %d, want 1", len(gateway.badges))
|
|
}
|
|
if gateway.badges[0] != (fakeBadgeGrant{userID: 1001, badgeID: badgeID, days: expireDays}) {
|
|
t.Fatalf("badge grant = %+v, want user 1001 badge %d days %d", gateway.badges[0], badgeID, expireDays)
|
|
}
|
|
if len(gateway.props) != 0 {
|
|
t.Fatalf("props grants = %d, want 0", len(gateway.props))
|
|
}
|
|
|
|
var latest model.VoiceRoomRocketRewardRecord
|
|
if err := db.Where("id = ?", record.ID).First(&latest).Error; err != nil {
|
|
t.Fatalf("load reward record: %v", err)
|
|
}
|
|
if latest.GrantStatus != rewardStatusSuccess {
|
|
t.Fatalf("grant status = %s, want %s", latest.GrantStatus, rewardStatusSuccess)
|
|
}
|
|
}
|
|
|
|
func TestProcessGiftPayloadDelaysLaunchWithRedis(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
|
|
|
payload := `{
|
|
"trackId":"track-launch-delay",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":100,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
|
}
|
|
if !resp.Processed || resp.Launched {
|
|
t.Fatalf("response = %+v, want processed delayed launch", resp)
|
|
}
|
|
|
|
var status model.VoiceRoomRocketStatus
|
|
if err := db.Where("sys_origin = ? AND room_id = ?", "LIKEI", int64(9001)).First(&status).Error; err != nil {
|
|
t.Fatalf("load status: %v", err)
|
|
}
|
|
if status.Status != statusLaunching || status.CurrentEnergy != 100 {
|
|
t.Fatalf("status = %+v, want launching full energy", status)
|
|
}
|
|
var launchCount int64
|
|
if err := db.Model(&model.VoiceRoomRocketLaunch{}).Count(&launchCount).Error; err != nil {
|
|
t.Fatalf("count launch before due: %v", err)
|
|
}
|
|
if launchCount != 0 {
|
|
t.Fatalf("launch count before due = %d, want 0", launchCount)
|
|
}
|
|
|
|
processed, err := service.ProcessDueLaunches(ctx, time.Now().Add(9*time.Second), 10)
|
|
if err != nil {
|
|
t.Fatalf("ProcessDueLaunches(before due) error = %v", err)
|
|
}
|
|
if processed != 0 {
|
|
t.Fatalf("launches before due = %d, want 0", processed)
|
|
}
|
|
processed, err = service.ProcessDueLaunches(ctx, time.Now().Add(11*time.Second), 10)
|
|
if err != nil {
|
|
t.Fatalf("ProcessDueLaunches(after due) error = %v", err)
|
|
}
|
|
if processed != 1 {
|
|
t.Fatalf("launches after due = %d, want 1", processed)
|
|
}
|
|
if err := db.Model(&model.VoiceRoomRocketLaunch{}).Count(&launchCount).Error; err != nil {
|
|
t.Fatalf("count launch after due: %v", err)
|
|
}
|
|
if launchCount != 1 {
|
|
t.Fatalf("launch count after due = %d, want 1", launchCount)
|
|
}
|
|
}
|
|
|
|
func TestPickInRoomRewardCanBeEmpty(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
launch := model.VoiceRoomRocketLaunch{LaunchNo: "VRR-empty"}
|
|
rewards := []model.VoiceRoomRocketRewardConfig{
|
|
{
|
|
RewardScene: rewardSceneInRoom,
|
|
RewardType: rewardTypeNone,
|
|
RewardName: "轮空",
|
|
Weight: 1,
|
|
},
|
|
}
|
|
got := service.pickInRoomRewardsForUser(launch, rewards, 1001)
|
|
if len(got) != 0 {
|
|
t.Fatalf("picked rewards = %+v, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestGrantGoldRewardUsesWalletRocketRewardOrigin(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
gateway := newFakeRocketGateway()
|
|
service.wallet = gateway
|
|
record := model.VoiceRoomRocketRewardRecord{
|
|
UserID: 1001,
|
|
SysOrigin: "LIKEI",
|
|
Level: 1,
|
|
RewardScene: rewardSceneInRoom,
|
|
RewardType: rewardTypeGold,
|
|
RewardAmount: 100,
|
|
GrantEventID: "VOICE_ROOM_ROCKET:launch-1:IN_ROOM:1001:1",
|
|
}
|
|
|
|
if err := service.grantGoldReward(context.Background(), record); err != nil {
|
|
t.Fatalf("grantGoldReward() error = %v", err)
|
|
}
|
|
walletEventID := walletGoldRewardEventID(record.GrantEventID)
|
|
cmd, ok := gateway.goldEvents[walletEventID]
|
|
if !ok {
|
|
t.Fatalf("gold wallet event was not created")
|
|
}
|
|
if len(cmd.EventID) > 50 {
|
|
t.Fatalf("wallet event id length = %d, want <= 50: %s", len(cmd.EventID), cmd.EventID)
|
|
}
|
|
if cmd.Origin != walletOriginRocketReward {
|
|
t.Fatalf("wallet origin = %q, want %q", cmd.Origin, walletOriginRocketReward)
|
|
}
|
|
if cmd.CustomizeOrigin != "" {
|
|
t.Fatalf("customize origin = %q, want empty", cmd.CustomizeOrigin)
|
|
}
|
|
if cmd.CustomizeOriginDesc != walletOriginRocketRewardDesc {
|
|
t.Fatalf("wallet origin desc = %q, want %q", cmd.CustomizeOriginDesc, walletOriginRocketRewardDesc)
|
|
}
|
|
}
|
|
|
|
func TestRewardGrantEventIDFitsWalletAssetRecord(t *testing.T) {
|
|
eventID := rewardGrantEventID("VOICE_ROOM_ROCKET_LAUNCH:20260515:9001:ROUND:1:LEVEL:6:EXTRA-LONG", rewardSceneInRoom, 1001001001001, 9009009009001)
|
|
if len(eventID) > 50 {
|
|
t.Fatalf("grant event id length = %d, want <= 50: %s", len(eventID), eventID)
|
|
}
|
|
if eventID != rewardGrantEventID("VOICE_ROOM_ROCKET_LAUNCH:20260515:9001:ROUND:1:LEVEL:6:EXTRA-LONG", rewardSceneInRoom, 1001001001001, 9009009009001) {
|
|
t.Fatalf("grant event id must be deterministic")
|
|
}
|
|
}
|
|
|
|
func TestInRoomRewardCopiesLimitSelection(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
service.cfg.VoiceRoomRocket.InRoomRewardUserLimit = 10
|
|
launch := model.VoiceRoomRocketLaunch{LaunchNo: "VRR-copies"}
|
|
audience := []roomAudienceUser{
|
|
{UserID: 1001},
|
|
{UserID: 1002},
|
|
{UserID: 1003},
|
|
{UserID: 1004},
|
|
}
|
|
rewards := []model.VoiceRoomRocketRewardConfig{
|
|
{
|
|
ID: 1,
|
|
RewardScene: rewardSceneInRoom,
|
|
RewardType: rewardTypeGold,
|
|
RewardName: "金币",
|
|
RewardAmount: 100,
|
|
RewardCopies: 2,
|
|
Weight: 1,
|
|
},
|
|
}
|
|
|
|
selected := service.selectInRoomRewardUsers(launch, audience, rewards)
|
|
if len(selected) != 2 {
|
|
t.Fatalf("selected users = %+v, want 2 users", selected)
|
|
}
|
|
picked := service.pickInRoomRewardsForUsers(launch, rewards, selected)
|
|
if len(picked) != 2 {
|
|
t.Fatalf("picked rewards = %+v, want 2 users with rewards", picked)
|
|
}
|
|
}
|
|
|
|
func TestNotifyLaunchPublishesIMAndRedisStream(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db, rdb, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
seedRocketConfig(t, db)
|
|
now := time.Now()
|
|
if err := db.Create(&model.VoiceRoomRocketLevelConfig{
|
|
ID: 991,
|
|
SysOrigin: "LIKEI",
|
|
Level: 1,
|
|
NeedEnergy: 100,
|
|
ShakeThresholdPercent: "95.0000",
|
|
PreviewRocketURL: "https://cdn.example.com/preview-rocket.png",
|
|
RocketIconURL: "https://cdn.example.com/rocket-icon.png",
|
|
RocketAnimationURL: "https://cdn.example.com/rocket-launch.svga",
|
|
Enabled: true,
|
|
Sort: 1,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}).Error; err != nil {
|
|
t.Fatalf("seed level config: %v", err)
|
|
}
|
|
fakeIM := &fakeRocketIM{}
|
|
service.SetIMGateway(fakeIM)
|
|
|
|
ignite := int64(1001)
|
|
launch := model.VoiceRoomRocketLaunch{
|
|
LaunchNo: "VRR-test",
|
|
SysOrigin: "LIKEI",
|
|
RoomID: 9001,
|
|
RoomAccount: "room-9001",
|
|
RoundNo: 1,
|
|
Level: 1,
|
|
IgniteUserID: &ignite,
|
|
}
|
|
if err := service.notifyLaunch(context.Background(), launch); err != nil {
|
|
t.Fatalf("notifyLaunch() error = %v", err)
|
|
}
|
|
if len(fakeIM.messages) != 1 || fakeIM.messages[0].groupID != "room-9001" {
|
|
t.Fatalf("im messages = %+v", fakeIM.messages)
|
|
}
|
|
body, ok := fakeIM.messages[0].body.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("im body = %#v", fakeIM.messages[0].body)
|
|
}
|
|
data, ok := body["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("im data = %#v", body["data"])
|
|
}
|
|
if data["previewRocketUrl"] != "https://cdn.example.com/preview-rocket.png" {
|
|
t.Fatalf("previewRocketUrl = %#v", data["previewRocketUrl"])
|
|
}
|
|
if data["rocketIconUrl"] != "https://cdn.example.com/rocket-icon.png" {
|
|
t.Fatalf("rocketIconUrl = %#v", data["rocketIconUrl"])
|
|
}
|
|
if data["rocketAnimationUrl"] != "https://cdn.example.com/rocket-launch.svga" {
|
|
t.Fatalf("rocketAnimationUrl = %#v", data["rocketAnimationUrl"])
|
|
}
|
|
count, err := rdb.XLen(context.Background(), "voice_room:rocket:broadcast").Result()
|
|
if err != nil {
|
|
t.Fatalf("xlen broadcast: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("broadcast stream count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestNotifyRewardRecordPublishesIMAndRedisStream(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
gateway.profiles = map[int64]integration.UserProfile{
|
|
1001: {
|
|
ID: integration.Int64Value(1001),
|
|
Account: "223456",
|
|
UserAvatar: "https://cdn.example.com/avatar-1001.png",
|
|
UserNickname: "Winner",
|
|
},
|
|
}
|
|
service, _, rdb, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
fakeIM := &fakeRocketIM{}
|
|
service.SetIMGateway(fakeIM)
|
|
|
|
record := model.VoiceRoomRocketRewardRecord{
|
|
ID: 9901,
|
|
LaunchNo: "VRR-reward",
|
|
RoomID: 9001,
|
|
UserID: 1001,
|
|
Level: 2,
|
|
RewardScene: rewardSceneTop1,
|
|
RewardType: rewardTypeGold,
|
|
RewardName: "gold",
|
|
RewardAmount: 100,
|
|
}
|
|
if err := service.notifyRewardRecord(context.Background(), record); err != nil {
|
|
t.Fatalf("notifyRewardRecord() error = %v", err)
|
|
}
|
|
if len(fakeIM.messages) != 1 || fakeIM.messages[0].groupID != "9001" {
|
|
t.Fatalf("im messages = %+v", fakeIM.messages)
|
|
}
|
|
body, ok := fakeIM.messages[0].body.(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("im body = %#v", fakeIM.messages[0].body)
|
|
}
|
|
if body["type"] != imTypeRewardUser {
|
|
t.Fatalf("im type = %#v, want %s", body["type"], imTypeRewardUser)
|
|
}
|
|
data, ok := body["data"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("im data = %#v", body["data"])
|
|
}
|
|
if data["userId"] != "1001" || data["rewardRecordId"] != "9901" {
|
|
t.Fatalf("im data = %#v", data)
|
|
}
|
|
if data["userNickname"] != "Winner" || data["account"] != "223456" || data["userAvatar"] != "https://cdn.example.com/avatar-1001.png" {
|
|
t.Fatalf("im user profile = %#v", data)
|
|
}
|
|
count, err := rdb.XLen(context.Background(), "voice_room:rocket:broadcast").Result()
|
|
if err != nil {
|
|
t.Fatalf("xlen broadcast: %v", err)
|
|
}
|
|
if count != 1 {
|
|
t.Fatalf("broadcast stream count = %d, want 1", count)
|
|
}
|
|
}
|
|
|
|
func TestPublishRewardMessageTrimsBroadcastStreamByConfiguredMaxLen(t *testing.T) {
|
|
service, _, rdb, mr := newTestServiceWithRedis(t, newFakeRocketGateway())
|
|
defer mr.Close()
|
|
service.cfg.VoiceRoomRocket.BroadcastStreamMaxLen = 2
|
|
|
|
for idx := 1; idx <= 3; idx++ {
|
|
service.publishRewardMessage(context.Background(), 9001, "launch-"+strconv.Itoa(idx), map[string]any{
|
|
"type": imTypeRewardPopup,
|
|
"data": map[string]any{
|
|
"roomId": "9001",
|
|
},
|
|
})
|
|
}
|
|
|
|
count, err := rdb.XLen(context.Background(), "voice_room:rocket:broadcast").Result()
|
|
if err != nil {
|
|
t.Fatalf("xlen broadcast: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("broadcast stream count = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
func TestStatusUpdatePublishesRegionIM(t *testing.T) {
|
|
gateway := newFakeRocketGateway()
|
|
service, db, _, mr := newTestServiceWithRedis(t, gateway)
|
|
defer mr.Close()
|
|
service.regionResolver = fakeRocketRegionResolver{regions: map[string]string{"LIKEI|SA": "AR"}}
|
|
regionIM := &fakeRocketRegionBroadcaster{}
|
|
service.SetRegionBroadcaster(regionIM)
|
|
|
|
ctx := context.Background()
|
|
seedRocketConfig(t, db)
|
|
seedLevelConfigs(t, db, []int64{100, 200, 300, 400, 500, 600})
|
|
|
|
payload := `{
|
|
"trackId":"track-region-status",
|
|
"sysOrigin":"LIKEI",
|
|
"sendUserId":"1001",
|
|
"roomId":"9001",
|
|
"quantity":1,
|
|
"giftConfig":{"id":"11","giftCandy":10,"type":"GOLD","giftTab":"NORMAL"},
|
|
"createTime":` + strconvFormatMillis(time.Now()) + `
|
|
}`
|
|
resp, err := service.ProcessGiftPayload(ctx, payload)
|
|
if err != nil {
|
|
t.Fatalf("ProcessGiftPayload() error = %v", err)
|
|
}
|
|
if !resp.Processed {
|
|
t.Fatalf("response = %+v, want processed", resp)
|
|
}
|
|
if len(regionIM.messages) != 1 {
|
|
t.Fatalf("region messages = %+v, want one", regionIM.messages)
|
|
}
|
|
if regionIM.messages[0].regionCode != "AR" || regionIM.messages[0].messageType != imTypeStatusUpdate {
|
|
t.Fatalf("region message = %+v", regionIM.messages[0])
|
|
}
|
|
}
|
|
|
|
func TestDayKeyUsesUTCPlus3BusinessDay(t *testing.T) {
|
|
location := resolveLocation("Asia/Riyadh")
|
|
before := time.Date(2026, 5, 13, 20, 59, 59, 0, time.UTC)
|
|
after := time.Date(2026, 5, 13, 21, 0, 0, 0, time.UTC)
|
|
if got := dayKey(before, location); got != "20260513" {
|
|
t.Fatalf("dayKey(before) = %s, want 20260513", got)
|
|
}
|
|
if got := dayKey(after, location); got != "20260514" {
|
|
t.Fatalf("dayKey(after) = %s, want 20260514", got)
|
|
}
|
|
}
|
|
|
|
func TestSaveAdminConfigForcesRiyadhTimezoneAndReturnsEnergyRule(t *testing.T) {
|
|
service, _ := newTestService(t)
|
|
ctx := context.Background()
|
|
|
|
resp, err := service.SaveAdminConfig(ctx, SaveConfigRequest{
|
|
SysOrigin: "LIKEI",
|
|
Enabled: true,
|
|
Timezone: "UTC",
|
|
MaxLevel: 6,
|
|
RankingTopLimit: 50,
|
|
RewardRecordKeepDays: 35,
|
|
BroadcastDurationSeconds: 7,
|
|
InRoomRewardDelaySeconds: 10,
|
|
EnergyRuleJSON: json.RawMessage(`{"allowGiftIds":[11]}`),
|
|
RuleText: json.RawMessage(`{"title":"rocket"}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveAdminConfig() error = %v", err)
|
|
}
|
|
if resp.Timezone != defaultTimezone {
|
|
t.Fatalf("timezone = %s, want %s", resp.Timezone, defaultTimezone)
|
|
}
|
|
if string(resp.EnergyRuleJSON) != `{"allowGiftIds":[11]}` {
|
|
t.Fatalf("energyRuleJson = %s", string(resp.EnergyRuleJSON))
|
|
}
|
|
}
|
|
|
|
func TestSaveRewardConfigsReplacesAndClearsSelectedLevel(t *testing.T) {
|
|
service, db := newTestService(t)
|
|
ctx := context.Background()
|
|
seedRewardConfig(t, db, 1, rewardSceneTop1, rewardTypeGold, 10)
|
|
seedRewardConfig(t, db, 1, rewardSceneIgnite, rewardTypeGold, 5)
|
|
seedRewardConfig(t, db, 2, rewardSceneTop1, rewardTypeGold, 20)
|
|
|
|
saved, err := service.SaveRewardConfigs(ctx, SaveRewardConfigsRequest{
|
|
SysOrigin: "LIKEI",
|
|
Level: 1,
|
|
Rewards: []RewardConfigPayload{
|
|
{
|
|
Level: 1,
|
|
RewardScene: rewardSceneInRoom,
|
|
RewardType: rewardTypeGold,
|
|
RewardName: "new gold",
|
|
RewardAmount: 99,
|
|
RewardCopies: 10,
|
|
Enabled: true,
|
|
Sort: 1,
|
|
},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveRewardConfigs(replace) error = %v", err)
|
|
}
|
|
if len(saved) != 1 || saved[0].RewardAmount != 99 || saved[0].RewardCopies != 10 {
|
|
t.Fatalf("saved rewards = %+v, want one amount 99 and 10 copies", saved)
|
|
}
|
|
|
|
var level1Count int64
|
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
|
Where("sys_origin = ? AND level = ?", "LIKEI", 1).
|
|
Count(&level1Count).Error; err != nil {
|
|
t.Fatalf("count level 1 rewards: %v", err)
|
|
}
|
|
if level1Count != 1 {
|
|
t.Fatalf("level 1 reward count = %d, want 1", level1Count)
|
|
}
|
|
var level2Count int64
|
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
|
Where("sys_origin = ? AND level = ?", "LIKEI", 2).
|
|
Count(&level2Count).Error; err != nil {
|
|
t.Fatalf("count level 2 rewards: %v", err)
|
|
}
|
|
if level2Count != 1 {
|
|
t.Fatalf("level 2 reward count = %d, want 1", level2Count)
|
|
}
|
|
|
|
saved, err = service.SaveRewardConfigs(ctx, SaveRewardConfigsRequest{
|
|
SysOrigin: "LIKEI",
|
|
Level: 1,
|
|
Rewards: []RewardConfigPayload{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("SaveRewardConfigs(clear) error = %v", err)
|
|
}
|
|
if len(saved) != 0 {
|
|
t.Fatalf("saved rewards after clear = %+v, want empty", saved)
|
|
}
|
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
|
Where("sys_origin = ? AND level = ?", "LIKEI", 1).
|
|
Count(&level1Count).Error; err != nil {
|
|
t.Fatalf("count cleared level 1 rewards: %v", err)
|
|
}
|
|
if level1Count != 0 {
|
|
t.Fatalf("level 1 reward count after clear = %d, want 0", level1Count)
|
|
}
|
|
if err := db.Model(&model.VoiceRoomRocketRewardConfig{}).
|
|
Where("sys_origin = ? AND level = ?", "LIKEI", 2).
|
|
Count(&level2Count).Error; err != nil {
|
|
t.Fatalf("count retained level 2 rewards: %v", err)
|
|
}
|
|
if level2Count != 1 {
|
|
t.Fatalf("level 2 reward count after clear = %d, want 1", level2Count)
|
|
}
|
|
}
|
|
|
|
func newTestService(t *testing.T) (*Service, *gorm.DB) {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=shared"), &gorm.Config{})
|
|
if err != nil {
|
|
t.Fatalf("open sqlite: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&model.VoiceRoomRocketConfig{},
|
|
&model.VoiceRoomRocketLevelConfig{},
|
|
&model.VoiceRoomRocketRewardConfig{},
|
|
&model.VoiceRoomRocketStatus{},
|
|
&model.VoiceRoomRocketLevelRound{},
|
|
&model.VoiceRoomRocketContribution{},
|
|
&model.VoiceRoomRocketGiftEventLog{},
|
|
&model.VoiceRoomRocketLaunch{},
|
|
&model.VoiceRoomRocketRewardRecord{},
|
|
&model.VoiceRoomRocketAudienceSnapshot{},
|
|
); err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
service := NewService(config.Config{
|
|
VoiceRoomRocket: config.VoiceRoomRocketConfig{
|
|
DefaultSysOrigin: "LIKEI",
|
|
Timezone: "Asia/Riyadh",
|
|
},
|
|
}, db, nil, nil)
|
|
return service, db
|
|
}
|
|
|
|
func newTestServiceWithRedis(t *testing.T, gateway *fakeRocketGateway) (*Service, *gorm.DB, *redis.Client, *miniredis.Miniredis) {
|
|
t.Helper()
|
|
service, db := newTestService(t)
|
|
mr := miniredis.RunT(t)
|
|
rdb := redis.NewClient(&redis.Options{Addr: mr.Addr()})
|
|
t.Cleanup(func() { _ = rdb.Close() })
|
|
service.repo.Redis = rdb
|
|
service.gateways = gateway
|
|
service.roomProfiles = gateway
|
|
service.userProfiles = gateway
|
|
service.wallet = gateway
|
|
service.rewardGateway = gateway
|
|
service.cfg.VoiceRoomRocket.BroadcastStreamKey = "voice_room:rocket:broadcast"
|
|
service.cfg.VoiceRoomRocket.BroadcastStreamMaxLen = 100
|
|
service.cfg.VoiceRoomRocket.InRoomRewardUserLimit = 3
|
|
service.cfg.VoiceRoomRocket.InRoomRewardDueZSetKey = "voice_room:rocket:in_room_due"
|
|
return service, db, rdb, mr
|
|
}
|
|
|
|
type fakeRocketGateway struct {
|
|
goldEvents map[string]integration.GoldReceiptCommand
|
|
props []integration.GivePropsBackpackRequest
|
|
badges []fakeBadgeGrant
|
|
profiles map[int64]integration.UserProfile
|
|
}
|
|
|
|
type fakeBadgeGrant struct {
|
|
userID int64
|
|
badgeID int64
|
|
days int
|
|
}
|
|
|
|
type fakeRocketIM struct {
|
|
messages []fakeRocketIMMessage
|
|
}
|
|
|
|
type fakeRocketIMMessage struct {
|
|
groupID string
|
|
body any
|
|
}
|
|
|
|
type fakeRocketRegionResolver struct {
|
|
regions map[string]string
|
|
}
|
|
|
|
type fakeRocketRegionBroadcaster struct {
|
|
messages []fakeRocketRegionMessage
|
|
}
|
|
|
|
type fakeRocketRegionMessage struct {
|
|
sysOrigin string
|
|
regionCode string
|
|
messageType string
|
|
body any
|
|
}
|
|
|
|
func newFakeRocketGateway() *fakeRocketGateway {
|
|
return &fakeRocketGateway{goldEvents: map[string]integration.GoldReceiptCommand{}}
|
|
}
|
|
|
|
func (g *fakeRocketGateway) MapRoomProfiles(ctx context.Context, roomIDs []int64) (map[int64]integration.RoomProfile, error) {
|
|
result := make(map[int64]integration.RoomProfile, len(roomIDs))
|
|
for _, roomID := range roomIDs {
|
|
result[roomID] = integration.RoomProfile{ID: integration.Int64Value(roomID), RoomAccount: strconv.FormatInt(roomID, 10), RoomName: "room"}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) GetUserProfile(ctx context.Context, userID int64) (integration.UserProfile, error) {
|
|
if profile, ok := g.profiles[userID]; ok {
|
|
if int64(profile.ID) == 0 {
|
|
profile.ID = integration.Int64Value(userID)
|
|
}
|
|
return profile, nil
|
|
}
|
|
return integration.UserProfile{ID: integration.Int64Value(userID), Account: strconv.FormatInt(userID, 10), UserNickname: "user", CountryCode: "SA"}, nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) ChangeGoldBalance(ctx context.Context, cmd integration.GoldReceiptCommand) error {
|
|
g.goldEvents[cmd.EventID] = cmd
|
|
return nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) ExistsGoldEvent(ctx context.Context, eventID string) (bool, error) {
|
|
_, ok := g.goldEvents[eventID]
|
|
return ok, nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) GivePropsBackpack(ctx context.Context, req integration.GivePropsBackpackRequest) error {
|
|
g.props = append(g.props, req)
|
|
return nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) GetNobleVIPAbility(ctx context.Context, sourceID int64) (integration.PropsNobleVIPAbility, error) {
|
|
return integration.PropsNobleVIPAbility{}, nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) SwitchUseProps(ctx context.Context, userID int64, propsID int64) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) ActivateTemporaryBadge(ctx context.Context, userID int64, badgeID int64, days int) error {
|
|
g.badges = append(g.badges, fakeBadgeGrant{userID: userID, badgeID: badgeID, days: days})
|
|
return nil
|
|
}
|
|
|
|
func (g *fakeRocketGateway) RemoveUserProfileCacheAll(ctx context.Context, userID int64) error {
|
|
return nil
|
|
}
|
|
|
|
func (g *fakeRocketIM) SendCustomGroupMessage(ctx context.Context, groupID string, body any) error {
|
|
g.messages = append(g.messages, fakeRocketIMMessage{groupID: groupID, body: body})
|
|
return nil
|
|
}
|
|
|
|
func (r fakeRocketRegionResolver) ResolveRegionCodeByCountryCode(ctx context.Context, sysOrigin string, countryCode string) (string, error) {
|
|
return r.regions[sysOrigin+"|"+countryCode], nil
|
|
}
|
|
|
|
func (b *fakeRocketRegionBroadcaster) SendRegionCustomMessage(ctx context.Context, sysOrigin string, regionCode string, body any) (string, error) {
|
|
messageType := ""
|
|
if payload, ok := body.(map[string]any); ok {
|
|
messageType, _ = payload["type"].(string)
|
|
}
|
|
b.messages = append(b.messages, fakeRocketRegionMessage{
|
|
sysOrigin: sysOrigin,
|
|
regionCode: regionCode,
|
|
messageType: messageType,
|
|
body: body,
|
|
})
|
|
return "@region-" + strings.ToLower(regionCode), nil
|
|
}
|
|
|
|
func seedRocketConfig(t *testing.T, db *gorm.DB) {
|
|
t.Helper()
|
|
now := time.Now()
|
|
row := model.VoiceRoomRocketConfig{
|
|
ID: 1,
|
|
SysOrigin: "LIKEI",
|
|
Enabled: true,
|
|
Timezone: "Asia/Riyadh",
|
|
MaxLevel: 6,
|
|
RankingTopLimit: 100,
|
|
RewardRecordKeepDays: 35,
|
|
BroadcastDurationSeconds: 7,
|
|
InRoomRewardDelaySeconds: 10,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed config: %v", err)
|
|
}
|
|
}
|
|
|
|
func seedLevelConfigs(t *testing.T, db *gorm.DB, energy []int64) {
|
|
t.Helper()
|
|
now := time.Now()
|
|
for index, need := range energy {
|
|
row := model.VoiceRoomRocketLevelConfig{
|
|
ID: int64(100 + index),
|
|
SysOrigin: "LIKEI",
|
|
Level: index + 1,
|
|
NeedEnergy: need,
|
|
ShakeThresholdPercent: "95.0000",
|
|
Enabled: true,
|
|
Sort: index + 1,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed level: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func seedRewardConfig(t *testing.T, db *gorm.DB, level int, scene string, rewardType string, amount int64) {
|
|
seedRewardConfigWithCopies(t, db, level, scene, rewardType, amount, 1)
|
|
}
|
|
|
|
func seedRewardConfigWithCopies(t *testing.T, db *gorm.DB, level int, scene string, rewardType string, amount int64, copies int) {
|
|
t.Helper()
|
|
now := time.Now()
|
|
row := model.VoiceRoomRocketRewardConfig{
|
|
ID: int64(1000 + len(scene) + level + int(amount)),
|
|
SysOrigin: "LIKEI",
|
|
Level: level,
|
|
RewardScene: scene,
|
|
RewardType: rewardType,
|
|
RewardName: scene + " reward",
|
|
RewardAmount: amount,
|
|
RewardCopies: copies,
|
|
Enabled: true,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatalf("seed reward: %v", err)
|
|
}
|
|
}
|
|
|
|
func strconvFormatMillis(t time.Time) string {
|
|
return strconv.FormatInt(t.UnixMilli(), 10)
|
|
}
|