245 lines
7.4 KiB
Go
245 lines
7.4 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"
|
|
"hyapp/pkg/appcode"
|
|
"hyapp/services/room-service/internal/integration"
|
|
roomoutbox "hyapp/services/room-service/internal/room/outbox"
|
|
roomservice "hyapp/services/room-service/internal/room/service"
|
|
"hyapp/services/room-service/internal/router"
|
|
"hyapp/services/room-service/internal/testutil/mysqltest"
|
|
)
|
|
|
|
type fakeRTCUserRemover struct {
|
|
calls []rtcRemoveCall
|
|
roomID string
|
|
userID int64
|
|
}
|
|
|
|
type rtcRemoveCall struct {
|
|
roomID string
|
|
userID int64
|
|
}
|
|
|
|
func (r *fakeRTCUserRemover) RemoveUserByStrRoomID(_ context.Context, roomID string, userID int64) error {
|
|
r.roomID = roomID
|
|
r.userID = userID
|
|
r.calls = append(r.calls, rtcRemoveCall{roomID: roomID, userID: userID})
|
|
return nil
|
|
}
|
|
|
|
func TestKickUserRemovesPresenceBanAndRTCConnection(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
rtc := &fakeRTCUserRemover{}
|
|
svc := roomservice.New(roomservice.Config{
|
|
NodeID: "node-kick-test",
|
|
LeaseTTL: 10 * time.Second,
|
|
RankLimit: 20,
|
|
SnapshotEveryN: 1,
|
|
RTCUserRemover: rtc,
|
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
|
|
|
roomID := "room-kick-flow"
|
|
ownerID := int64(7101)
|
|
viewerID := int64(7201)
|
|
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
|
SeatCount: 10,
|
|
Mode: "voice",
|
|
VisibleRegionId: 8101,
|
|
RoomName: "Kick Flow",
|
|
RoomShortId: "kick-flow",
|
|
}); err != nil {
|
|
t.Fatalf("create kick room fixture failed: %v", err)
|
|
}
|
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
|
Role: "audience",
|
|
}); err != nil {
|
|
t.Fatalf("join kick target failed: %v", err)
|
|
}
|
|
|
|
kickResp, err := svc.KickUser(ctx, &roomv1.KickUserRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
|
TargetUserId: viewerID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("kick user failed: %v", err)
|
|
}
|
|
if !kickResp.GetRtcKicked() || kickResp.GetRtcKickError() != "" || rtc.roomID != roomID || rtc.userID != viewerID {
|
|
t.Fatalf("kick must remove RTC user after Room Cell commit: resp=%+v rtc=%+v", kickResp, rtc)
|
|
}
|
|
if !containsInt64(kickResp.GetRoom().GetBanUserIds(), viewerID) || findRoomUser(kickResp.GetRoom(), viewerID) != nil {
|
|
t.Fatalf("kick response must ban and remove target presence: %+v", kickResp.GetRoom())
|
|
}
|
|
|
|
presenceResp, err := svc.VerifyRoomPresence(ctx, &roomv1.VerifyRoomPresenceRequest{
|
|
AppCode: appcode.Default,
|
|
RoomId: roomID,
|
|
UserId: viewerID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("verify room presence after kick failed: %v", err)
|
|
}
|
|
if presenceResp.GetPresent() || presenceResp.GetReason() != "user_banned" {
|
|
t.Fatalf("kicked user must not pass IM presence guard: %+v", presenceResp)
|
|
}
|
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
|
Role: "audience",
|
|
}); err == nil {
|
|
t.Fatalf("kicked user must not join before unban")
|
|
}
|
|
}
|
|
|
|
func TestCloseRoomByAdminEvictsCurrentUsersThroughOutboxAndRTC(t *testing.T) {
|
|
ctx := context.Background()
|
|
repository := mysqltest.NewRepository(t)
|
|
rtc := &fakeRTCUserRemover{}
|
|
svc := roomservice.New(roomservice.Config{
|
|
NodeID: "node-close-test",
|
|
LeaseTTL: 10 * time.Second,
|
|
RankLimit: 20,
|
|
SnapshotEveryN: 1,
|
|
RTCUserRemover: rtc,
|
|
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
|
|
|
roomID := "room-close-flow"
|
|
ownerID := int64(8101)
|
|
viewerID := int64(8201)
|
|
adminID := int64(1)
|
|
if _, err := svc.CreateRoom(ctx, &roomv1.CreateRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, ownerID),
|
|
SeatCount: 10,
|
|
Mode: "voice",
|
|
VisibleRegionId: 8101,
|
|
RoomName: "Close Flow",
|
|
RoomShortId: "close-flow",
|
|
}); err != nil {
|
|
t.Fatalf("create close room fixture failed: %v", err)
|
|
}
|
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
|
Role: "audience",
|
|
}); err != nil {
|
|
t.Fatalf("join close target failed: %v", err)
|
|
}
|
|
|
|
resp, err := svc.CloseRoom(ctx, &roomv1.CloseRoomRequest{
|
|
Meta: &roomv1.RequestMeta{
|
|
RequestId: "req-close-admin",
|
|
CommandId: "cmd-close-admin",
|
|
ActorUserId: adminID,
|
|
RoomId: roomID,
|
|
AppCode: appcode.Default,
|
|
SentAtMs: time.Now().UnixMilli(),
|
|
},
|
|
Reason: "admin_closed",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("admin close room failed: %v", err)
|
|
}
|
|
if resp.GetRoom().GetStatus() != "closed" || len(resp.GetRoom().GetOnlineUsers()) != 0 {
|
|
t.Fatalf("admin close response must close room and clear users: %+v", resp.GetRoom())
|
|
}
|
|
if !hasRTCRemoveCall(rtc.calls, roomID, ownerID) || !hasRTCRemoveCall(rtc.calls, roomID, viewerID) {
|
|
t.Fatalf("admin close must remove all current RTC users: %+v", rtc.calls)
|
|
}
|
|
|
|
records, err := repository.ListPendingOutbox(ctx, 20)
|
|
if err != nil {
|
|
t.Fatalf("list pending outbox failed: %v", err)
|
|
}
|
|
if !hasOutboxEvent(records, "RoomClosed", 0) ||
|
|
!hasOutboxEvent(records, "RoomUserKicked", ownerID) ||
|
|
!hasOutboxEvent(records, "RoomUserKicked", viewerID) {
|
|
t.Fatalf("admin close must write room closed and per-user kick events: %+v", outboxSummary(records))
|
|
}
|
|
|
|
reopenResp, err := svc.CloseRoom(ctx, &roomv1.CloseRoomRequest{
|
|
Meta: &roomv1.RequestMeta{
|
|
RequestId: "req-reopen-admin",
|
|
CommandId: "cmd-reopen-admin",
|
|
ActorUserId: adminID,
|
|
RoomId: roomID,
|
|
AppCode: appcode.Default,
|
|
SentAtMs: time.Now().UnixMilli(),
|
|
},
|
|
Reason: "admin_reopen",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("admin reopen room failed: %v", err)
|
|
}
|
|
if reopenResp.GetRoom().GetStatus() != "active" {
|
|
t.Fatalf("admin reopen response must make room active: %+v", reopenResp.GetRoom())
|
|
}
|
|
if _, err := svc.JoinRoom(ctx, &roomv1.JoinRoomRequest{
|
|
Meta: roomservice.NewRequestMeta(roomID, viewerID),
|
|
Role: "audience",
|
|
}); err != nil {
|
|
t.Fatalf("viewer must join after admin reopen: %v", err)
|
|
}
|
|
}
|
|
|
|
func findRoomUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
|
|
for _, user := range snapshot.GetOnlineUsers() {
|
|
if user.GetUserId() == userID {
|
|
return user
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func containsInt64(values []int64, target int64) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasRTCRemoveCall(calls []rtcRemoveCall, roomID string, userID int64) bool {
|
|
for _, call := range calls {
|
|
if call.roomID == roomID && call.userID == userID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func hasOutboxEvent(records []roomoutbox.Record, eventType string, targetUserID int64) bool {
|
|
for _, record := range records {
|
|
if record.EventType != eventType {
|
|
continue
|
|
}
|
|
if targetUserID == 0 {
|
|
return true
|
|
}
|
|
var body roomeventsv1.RoomUserKicked
|
|
if record.Envelope == nil || proto.Unmarshal(record.Envelope.GetBody(), &body) != nil {
|
|
continue
|
|
}
|
|
if body.GetTargetUserId() == targetUserID {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func outboxSummary(records []roomoutbox.Record) []string {
|
|
summary := make([]string, 0, len(records))
|
|
for _, record := range records {
|
|
summary = append(summary, fmt.Sprintf("%s:%s", record.EventType, record.EventID))
|
|
}
|
|
return summary
|
|
}
|