262 lines
9.6 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 grpc
import (
"context"
"net"
"os"
"testing"
"time"
roomv1 "hyapp.local/api/proto/room/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/integration"
roomservice "hyapp/services/room-service/internal/room/service"
"hyapp/services/room-service/internal/router"
"hyapp/services/room-service/internal/testutil/mysqltest"
grpcpkg "google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials/insecure"
grpcstatus "google.golang.org/grpc/status"
)
// fakeWallet 只满足 room-service 构造依赖;本测试走参数校验失败分支,不触发扣费。
type fakeWallet struct{}
// DebitGift 返回固定结果,避免 transport 测试依赖 wallet-service 进程。
func (fakeWallet) DebitGift(context.Context, *walletv1.DebitGiftRequest) (*walletv1.DebitGiftResponse, error) {
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}, nil
}
// BatchDebitGift 返回固定结果,避免 transport 测试依赖 wallet-service 批量扣费实现。
func (fakeWallet) BatchDebitGift(context.Context, *walletv1.BatchDebitGiftRequest) (*walletv1.BatchDebitGiftResponse, error) {
return &walletv1.BatchDebitGiftResponse{Aggregate: &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-test"}}, nil
}
// DebitRobotGift 返回固定结果,避免 transport 测试依赖 wallet-service 机器人金币实现。
func (fakeWallet) DebitRobotGift(context.Context, *walletv1.DebitRobotGiftRequest) (*walletv1.DebitGiftResponse, error) {
return &walletv1.DebitGiftResponse{BillingReceiptId: "receipt-robot-test"}, nil
}
// GrantResourceGroup 返回固定发放结果,避免 transport 测试依赖 wallet-service 资源组实现。
func (fakeWallet) GrantResourceGroup(context.Context, *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error) {
return &walletv1.ResourceGrantResponse{Grant: &walletv1.ResourceGrant{GrantId: "grant-test"}}, nil
}
// GetMyVip 返回显式 legacy program避免 transport 测试把 VIP 配置缺失误判成房间协议错误。
func (fakeWallet) GetMyVip(_ context.Context, req *walletv1.GetMyVipRequest) (*walletv1.GetMyVipResponse, error) {
return &walletv1.GetMyVipResponse{State: &walletv1.VipState{
ProgramConfig: &walletv1.VipProgramConfig{AppCode: appcode.Default, ProgramType: "legacy_timed", Status: "active"},
EffectiveVip: &walletv1.UserVip{UserId: req.GetUserId()},
}}, nil
}
// newTransportTestServer 构造完整领域服务,确保测试覆盖真实 service -> transport 错误边界。
func newTransportTestServer(t *testing.T) *Server {
t.Helper()
svc := roomservice.New(roomservice.Config{
NodeID: "node-transport-test",
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, router.NewMemoryDirectory(), mysqltest.NewRepository(t), fakeWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
return NewServer(svc)
}
func TestCreateRoomMapsDomainErrorToGRPCReason(t *testing.T) {
server := newTransportTestServer(t)
_, err := server.CreateRoom(context.Background(), &roomv1.CreateRoomRequest{
// room_id 缺失属于客户端参数错误,不能在 gRPC 边界丢失 reason 后变成 INTERNAL_ERROR。
SeatCount: 10,
Mode: "voice",
})
if err == nil {
t.Fatalf("CreateRoom without room_id should fail")
}
if got := grpcstatus.Code(err); got != codes.InvalidArgument {
t.Fatalf("gRPC code mismatch: got %s", got)
}
if got := xerr.ReasonFromGRPC(err); got != xerr.InvalidArgument {
t.Fatalf("reason mismatch: got %s", got)
}
if message := grpcstatus.Convert(err).Message(); message != "room_id is required" {
t.Fatalf("message mismatch: got %q", message)
}
}
func TestOwnerForwardingUsesRedisRouteAndNodeRegistry(t *testing.T) {
redisAddr := os.Getenv("ROOM_SERVICE_REDIS_TEST_ADDR")
if redisAddr == "" {
t.Skip("set ROOM_SERVICE_REDIS_TEST_ADDR to run Redis-backed owner forwarding test")
}
ctx := context.Background()
redisClient, err := router.NewRedisClient(ctx, redisAddr, os.Getenv("ROOM_SERVICE_REDIS_TEST_PASSWORD"), 0)
if err != nil {
t.Fatalf("open redis directory failed: %v", err)
}
t.Cleanup(func() { _ = redisClient.Close() })
directory := router.NewRedisDirectory(redisClient)
repository := mysqltest.NewRepository(t)
addrA, stopA := startForwardingRoomServer(t, "node-forward-a", directory, repository)
defer stopA()
addrB, stopB := startForwardingRoomServer(t, "node-forward-b", directory, repository)
defer stopB()
if err := directory.RegisterNode(ctx, router.NodeRegistration{NodeID: "node-forward-a", GRPCAddr: addrA}, time.Minute); err != nil {
t.Fatalf("register node a failed: %v", err)
}
if err := directory.RegisterNode(ctx, router.NodeRegistration{NodeID: "node-forward-b", GRPCAddr: addrB}, time.Minute); err != nil {
t.Fatalf("register node b failed: %v", err)
}
t.Cleanup(func() {
_ = directory.UnregisterNode(context.Background(), "node-forward-a")
_ = directory.UnregisterNode(context.Background(), "node-forward-b")
})
connA, err := grpcpkg.Dial(addrA, grpcpkg.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("dial node a failed: %v", err)
}
defer connA.Close()
connB, err := grpcpkg.Dial(addrB, grpcpkg.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
t.Fatalf("dial node b failed: %v", err)
}
defer connB.Close()
roomID := "room-forward-test"
directory.ForceExpire(roomservice.RuntimeRoomKey(appcode.Default, roomID))
t.Cleanup(func() {
directory.ForceExpire(roomservice.RuntimeRoomKey(appcode.Default, roomID))
})
createResp, err := roomv1.NewRoomCommandServiceClient(connA).CreateRoom(ctx, &roomv1.CreateRoomRequest{
Meta: &roomv1.RequestMeta{
RequestId: "req-forward-create",
CommandId: "cmd-forward-create",
ActorUserId: 1001,
RoomId: roomID,
SentAtMs: time.Now().UTC().UnixMilli(),
AppCode: appcode.Default,
},
SeatCount: 10,
Mode: "voice",
RoomName: "Forward Room",
RoomAvatar: "https://cdn.example.com/room-cover.png",
RoomShortId: "forward-short",
})
if err != nil {
t.Fatalf("create room on node a failed: %v", err)
}
if createResp.GetRoom().GetRoomId() != roomID {
t.Fatalf("create room id mismatch: %+v", createResp.GetRoom())
}
lease, exists, err := directory.Lookup(ctx, roomservice.RuntimeRoomKey(appcode.Default, roomID))
if err != nil || !exists {
t.Fatalf("lookup owner lease failed: exists=%v err=%v", exists, err)
}
if lease.NodeID != "node-forward-a" {
t.Fatalf("room should be owned by node a before forwarding, got %+v", lease)
}
joinResp, err := roomv1.NewRoomCommandServiceClient(connB).JoinRoom(ctx, &roomv1.JoinRoomRequest{
Meta: &roomv1.RequestMeta{
RequestId: "req-forward-join",
CommandId: "cmd-forward-join",
ActorUserId: 1002,
RoomId: roomID,
SentAtMs: time.Now().UTC().UnixMilli(),
AppCode: appcode.Default,
},
Role: "audience",
})
if err != nil {
t.Fatalf("join room through node b should forward to owner: %v", err)
}
if findUser(joinResp.GetRoom(), 1002) == nil {
t.Fatalf("forwarded join did not update owner room snapshot: %+v", joinResp.GetRoom().GetOnlineUsers())
}
guardResp, err := roomv1.NewRoomGuardServiceClient(connB).CheckSpeakPermission(ctx, &roomv1.CheckSpeakPermissionRequest{
RoomId: roomID,
UserId: 1002,
AppCode: appcode.Default,
})
if err != nil {
t.Fatalf("guard through node b should forward to owner: %v", err)
}
if !guardResp.GetAllowed() {
t.Fatalf("forwarded guard should allow joined user, got %+v", guardResp)
}
snapshotResp, err := roomv1.NewRoomQueryServiceClient(connB).GetRoomSnapshot(ctx, &roomv1.GetRoomSnapshotRequest{
Meta: &roomv1.RequestMeta{
RequestId: "req-forward-snapshot",
ActorUserId: 1002,
RoomId: roomID,
AppCode: appcode.Default,
},
RoomId: roomID,
ViewerUserId: 1002,
})
if err != nil {
t.Fatalf("snapshot through node b should forward to owner: %v", err)
}
if snapshotResp.GetRoom().GetVersion() != joinResp.GetRoom().GetVersion() {
t.Fatalf("forwarded snapshot version mismatch: got %d want %d", snapshotResp.GetRoom().GetVersion(), joinResp.GetRoom().GetVersion())
}
}
func startForwardingRoomServer(t *testing.T, nodeID string, directory *router.RedisDirectory, repository roomservice.Repository) (string, func()) {
t.Helper()
svc := roomservice.New(roomservice.Config{
NodeID: nodeID,
LeaseTTL: 10 * time.Second,
RankLimit: 20,
SnapshotEveryN: 1,
}, directory, repository, fakeWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen forwarding test server failed: %v", err)
}
grpcServer := grpcpkg.NewServer()
roomServer := NewServer(svc, WithOwnerForwarding(nodeID, directory, directory, time.Second))
roomv1.RegisterRoomCommandServiceServer(grpcServer, roomServer)
roomv1.RegisterRoomGuardServiceServer(grpcServer, roomServer)
roomv1.RegisterRoomQueryServiceServer(grpcServer, roomServer)
errCh := make(chan error, 1)
go func() {
errCh <- grpcServer.Serve(listener)
}()
stop := func() {
roomServer.ownerForwarder.close()
grpcServer.GracefulStop()
select {
case err := <-errCh:
if err != nil {
t.Fatalf("forwarding test server stopped with error: %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("forwarding test server did not stop")
}
}
return listener.Addr().String(), stop
}
func findUser(snapshot *roomv1.RoomSnapshot, userID int64) *roomv1.RoomUser {
for _, user := range snapshot.GetOnlineUsers() {
if user.GetUserId() == userID {
return user
}
}
return nil
}