267 lines
11 KiB
Go
267 lines
11 KiB
Go
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(),
|
||
}
|
||
}
|