254 lines
9.1 KiB
Go
254 lines
9.1 KiB
Go
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
|
|
}
|
|
|
|
// 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
|
|
}
|