267 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service_test
import (
"context"
"fmt"
"testing"
"time"
"google.golang.org/protobuf/proto"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/xerr"
roomservice "hyapp/services/room-service/internal/room/service"
"hyapp/services/room-service/internal/testutil/mysqltest"
)
const vipTestAppCode = "fami"
func TestJoinRoomCarriesEffectiveVIPAndQueriesWalletOnce(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
8202: tieredVIPResponse(8202, 4, "VIP4", "room_entry_notice"),
}}
directIM := newRecordingRoomDirectIMPublisher()
now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC)}
svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, directIM)
roomID := "fami-vip-join-room"
createVIPRoom(t, ctx, svc, roomID, 8201)
response, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: vipRoomMeta(roomID, 8202, "join"),
Role: "audience",
})
if err != nil {
t.Fatalf("join room failed: %v", err)
}
if response.GetEffectiveVip().GetProgramType() != "tiered_privilege_v1" || response.GetEffectiveVip().GetLevel() != 4 || response.GetEffectiveVip().GetName() != "VIP4" || !response.GetEffectiveVip().GetRoomEntryNoticeEnabled() {
t.Fatalf("join effective vip mismatch: %+v", response.GetEffectiveVip())
}
if len(wallet.vipRequests) != 1 || wallet.vipRequests[0].GetAppCode() != vipTestAppCode || wallet.vipRequests[0].GetUserId() != 8202 {
t.Fatalf("join must query exactly one scoped effective vip state: %+v", wallet.vipRequests)
}
waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomUserJoined": 1})
records := directIM.recordsByType("RoomUserJoined")
if len(records) != 1 {
t.Fatalf("joined direct IM record mismatch: %+v", records)
}
var joined roomeventsv1.RoomUserJoined
if err := proto.Unmarshal(records[0].GetBody(), &joined); err != nil {
t.Fatalf("decode joined event failed: %v", err)
}
if joined.GetVipProgramType() != "tiered_privilege_v1" || joined.GetEffectiveVipLevel() != 4 || joined.GetEffectiveVipName() != "VIP4" || !joined.GetRoomEntryNoticeEnabled() {
t.Fatalf("joined event vip mismatch: %+v", &joined)
}
}
func TestRoomEntryNoticeRequiresBenefitAndUserSetting(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
8502: tieredVIPResponseWithRoomNoticeSetting(8502, 8, "VIP8", true, true),
8503: tieredVIPResponseWithRoomNoticeSetting(8503, 8, "VIP8", true, false),
8504: tieredVIPResponseWithRoomNoticeSetting(8504, 8, "VIP8", false, true),
// nil user_settings 模拟滚动升级期间旧 wallet必须按默认开启兼容。
8505: tieredVIPResponse(8505, 8, "VIP8", "room_entry_notice"),
}}
directIM := newRecordingRoomDirectIMPublisher()
now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 8, 30, 0, 0, time.UTC)}
svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, directIM)
roomID := "fami-vip-setting-room"
createVIPRoom(t, ctx, svc, roomID, 8501)
wants := map[int64]bool{8502: true, 8503: false, 8504: false, 8505: true}
for userID, want := range wants {
response, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: vipRoomMeta(roomID, userID, fmt.Sprintf("settings-%d", userID)), Role: "audience",
})
if err != nil {
t.Fatalf("join user %d failed: %v", userID, err)
}
if got := response.GetEffectiveVip().GetRoomEntryNoticeEnabled(); got != want {
t.Fatalf("room snapshot setting mismatch user=%d got=%v want=%v snapshot=%+v", userID, got, want, response.GetEffectiveVip())
}
}
waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomUserJoined": len(wants)})
seen := make(map[int64]bool, len(wants))
for _, record := range directIM.recordsByType("RoomUserJoined") {
var joined roomeventsv1.RoomUserJoined
if err := proto.Unmarshal(record.GetBody(), &joined); err != nil {
t.Fatalf("decode joined event failed: %v", err)
}
seen[joined.GetUserId()] = joined.GetRoomEntryNoticeEnabled()
}
for userID, want := range wants {
if got, exists := seen[userID]; !exists || got != want {
t.Fatalf("joined event/IM setting mismatch user=%d exists=%v got=%v want=%v", userID, exists, got, want)
}
}
}
func TestTieredVIPCustomRoomBackgroundRequiresEffectiveBenefit(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
ownerID := int64(8301)
wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
ownerID: tieredVIPResponse(ownerID, 3, "VIP3"),
}}
now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 9, 0, 0, 0, time.UTC)}
svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())
roomID := "fami-vip-background-room"
createVIPRoom(t, ctx, svc, roomID, ownerID)
if _, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
Meta: vipRoomMeta(roomID, ownerID, "save-denied"),
RoomId: roomID,
ImageUrl: "https://cdn.example.com/fami-vip-denied.png",
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("tiered app without custom_room_background must be denied: %v", err)
}
wallet.vipResponses[ownerID] = tieredVIPResponse(ownerID, 3, "VIP3", "custom_room_background")
first := saveVIPRoomBackground(t, ctx, svc, roomID, ownerID, "first", "https://cdn.example.com/fami-vip-first.png")
second := saveVIPRoomBackground(t, ctx, svc, roomID, ownerID, "second", "https://cdn.example.com/fami-vip-second.png")
if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
Meta: vipRoomMeta(roomID, ownerID, "set-allowed"),
BackgroundId: first.GetBackgroundId(),
}); err != nil {
t.Fatalf("effective custom_room_background benefit must allow set: %v", err)
}
wallet.vipResponses[ownerID] = tieredVIPResponse(ownerID, 3, "VIP3")
if _, err := svc.SetRoomBackground(ctx, &roomv1.SetRoomBackgroundRequest{
Meta: vipRoomMeta(roomID, ownerID, "set-denied"),
BackgroundId: second.GetBackgroundId(),
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("expired custom_room_background benefit must block later set: %v", err)
}
}
func TestVIPModerationProtectionBlocksRoomManagersButNotSystemEvict(t *testing.T) {
ctx := context.Background()
repository := mysqltest.NewRepository(t)
targetID := int64(8402)
wallet := &rocketTestWallet{vipResponses: map[int64]*walletv1.GetMyVipResponse{
targetID: tieredVIPResponse(targetID, 9, "VIP9", "anti_kick", "anti_mute"),
}}
now := &fixedRoomRocketClock{now: time.Date(2026, 7, 10, 10, 0, 0, 0, time.UTC)}
svc := newRocketTestServiceWithDirectIM(t, repository, wallet, now, nil, newRecordingRoomDirectIMPublisher())
roomID := "fami-vip-moderation-room"
ownerID := int64(8401)
createVIPRoom(t, ctx, svc, roomID, ownerID)
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{Meta: vipRoomMeta(roomID, targetID, "target-join"), Role: "audience"}); err != nil {
t.Fatalf("join protected target failed: %v", err)
}
if _, err := svc.MuteUser(ctx, &roomv1.MuteUserRequest{
Meta: vipRoomMeta(roomID, ownerID, "mute"),
TargetUserId: targetID,
Muted: true,
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("anti_mute must block ordinary room owner: %v", err)
}
if _, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
Meta: vipRoomMeta(roomID, ownerID, "kick"),
TargetUserId: targetID,
}); !xerr.IsCode(err, xerr.PermissionDenied) {
t.Fatalf("anti_kick must block ordinary room owner: %v", err)
}
vipCallsBeforeSystemEvict := len(wallet.vipRequests)
evict, err := svc.SystemEvictUser(ctx, &roomv1.SystemEvictUserRequest{
Meta: vipRoomMeta(roomID, 9901, "system-evict"),
TargetUserId: targetID,
OperatorUserId: 9901,
Reason: "platform_moderation",
BanFromRoom: true,
})
if err != nil {
t.Fatalf("system evict must bypass anti_kick: %v", err)
}
if !evict.GetResult().GetApplied() || findRoomUser(evict.GetRoom(), targetID) != nil || !containsInt64(evict.GetRoom().GetBanUserIds(), targetID) {
t.Fatalf("system evict must remove and ban protected target: %+v", evict)
}
if len(wallet.vipRequests) != vipCallsBeforeSystemEvict {
t.Fatalf("SystemEvictUser must not query or enforce ordinary VIP moderation benefits: before=%d after=%d", vipCallsBeforeSystemEvict, len(wallet.vipRequests))
}
}
func tieredVIPResponse(userID int64, level int32, name string, benefitCodes ...string) *walletv1.GetMyVipResponse {
benefits := make([]*walletv1.VipBenefit, 0, len(benefitCodes))
for index, code := range benefitCodes {
benefits = append(benefits, &walletv1.VipBenefit{
BenefitCode: code,
Name: code,
Status: "active",
SortOrder: int32(index + 1),
})
}
return &walletv1.GetMyVipResponse{State: &walletv1.VipState{
ProgramConfig: &walletv1.VipProgramConfig{AppCode: vipTestAppCode, ProgramType: "tiered_privilege_v1", Status: "active", LevelCount: 9},
EffectiveVip: &walletv1.UserVip{
UserId: userID,
Level: level,
Name: name,
Active: level > 0,
ProgramType: "tiered_privilege_v1",
},
EffectiveBenefits: benefits,
}}
}
func tieredVIPResponseWithRoomNoticeSetting(userID int64, level int32, name string, hasBenefit bool, enabled bool) *walletv1.GetMyVipResponse {
codes := []string{}
if hasBenefit {
codes = append(codes, "room_entry_notice")
}
response := tieredVIPResponse(userID, level, name, codes...)
response.GetState().UserSettings = &walletv1.VipUserSettings{
AppCode: vipTestAppCode, UserId: userID,
RoomEntryNoticeEnabled: enabled, OnlineGlobalNoticeEnabled: true,
}
return response
}
func createVIPRoom(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, ownerID int64) {
t.Helper()
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: vipRoomMeta(roomID, ownerID, "create"),
SeatCount: 10,
Mode: "voice",
VisibleRegionId: 1001,
RoomName: roomID,
RoomAvatar: testRoomCoverURL,
RoomShortId: roomID,
}); err != nil {
t.Fatalf("create vip room failed: %v", err)
}
}
func saveVIPRoomBackground(t *testing.T, ctx context.Context, svc *roomservice.Service, roomID string, ownerID int64, suffix string, imageURL string) *roomv1.RoomBackgroundImage {
t.Helper()
response, err := svc.SaveRoomBackground(ctx, &roomv1.SaveRoomBackgroundRequest{
Meta: vipRoomMeta(roomID, ownerID, "save-"+suffix),
RoomId: roomID,
ImageUrl: imageURL,
})
if err != nil {
t.Fatalf("save vip room background failed: %v", err)
}
return response.GetBackground()
}
func vipRoomMeta(roomID string, actorUserID int64, suffix string) *roomv1.RequestMeta {
return &roomv1.RequestMeta{
RequestId: fmt.Sprintf("req-vip-%s", suffix),
CommandId: fmt.Sprintf("cmd-vip-%s", suffix),
ActorUserId: actorUserID,
RoomId: roomID,
AppCode: vipTestAppCode,
SentAtMs: time.Date(2026, 7, 10, 8, 0, 0, 0, time.UTC).UnixMilli(),
}
}