2026-06-15 23:53:30 +08:00

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
}