package http import ( "bytes" "context" "encoding/json" "errors" "net/http" "net/http/httptest" "testing" jwt "github.com/golang-jwt/jwt/v5" roomv1 "hyapp/api/proto/room/v1" userv1 "hyapp/api/proto/user/v1" "hyapp/pkg/xerr" "hyapp/services/gateway-service/internal/auth" ) // fakeRoomClient 只承接 gateway transport 测试需要观察的 gRPC 入参。 // 测试目标是 HTTP envelope 和 RequestMeta 传递,不验证 room-service 业务行为。 type fakeRoomClient struct { lastCreate *roomv1.CreateRoomRequest createErr error } func (f *fakeRoomClient) CreateRoom(_ context.Context, req *roomv1.CreateRoomRequest) (*roomv1.CreateRoomResponse, error) { f.lastCreate = req if f.createErr != nil { return nil, f.createErr } return &roomv1.CreateRoomResponse{ Result: &roomv1.CommandResult{Applied: true, RoomVersion: 1}, }, nil } func (f *fakeRoomClient) JoinRoom(context.Context, *roomv1.JoinRoomRequest) (*roomv1.JoinRoomResponse, error) { return nil, nil } func (f *fakeRoomClient) LeaveRoom(context.Context, *roomv1.LeaveRoomRequest) (*roomv1.LeaveRoomResponse, error) { return nil, nil } func (f *fakeRoomClient) MicUp(context.Context, *roomv1.MicUpRequest) (*roomv1.MicUpResponse, error) { return nil, nil } func (f *fakeRoomClient) MicDown(context.Context, *roomv1.MicDownRequest) (*roomv1.MicDownResponse, error) { return nil, nil } func (f *fakeRoomClient) ChangeMicSeat(context.Context, *roomv1.ChangeMicSeatRequest) (*roomv1.ChangeMicSeatResponse, error) { return nil, nil } func (f *fakeRoomClient) MuteUser(context.Context, *roomv1.MuteUserRequest) (*roomv1.MuteUserResponse, error) { return nil, nil } func (f *fakeRoomClient) KickUser(context.Context, *roomv1.KickUserRequest) (*roomv1.KickUserResponse, error) { return nil, nil } func (f *fakeRoomClient) SendGift(context.Context, *roomv1.SendGiftRequest) (*roomv1.SendGiftResponse, error) { return nil, nil } type fakeUserAuthClient struct { lastSetPassword *userv1.SetPasswordRequest loginErr error } func (f *fakeUserAuthClient) LoginPassword(context.Context, *userv1.LoginPasswordRequest) (*userv1.AuthResponse, error) { if f.loginErr != nil { return nil, f.loginErr } return &userv1.AuthResponse{Token: &userv1.AuthToken{UserId: 10001}}, nil } func (f *fakeUserAuthClient) LoginThirdParty(context.Context, *userv1.LoginThirdPartyRequest) (*userv1.AuthResponse, error) { return &userv1.AuthResponse{ Token: &userv1.AuthToken{ UserId: 10001, DisplayUserId: "100001", SessionId: "sess-1", AccessToken: "access-1", RefreshToken: "refresh-1", ExpiresInSec: 1800, TokenType: "Bearer", }, IsNewUser: true, }, nil } func (f *fakeUserAuthClient) SetPassword(_ context.Context, req *userv1.SetPasswordRequest) (*userv1.SetPasswordResponse, error) { f.lastSetPassword = req return &userv1.SetPasswordResponse{PasswordSet: true}, nil } func (f *fakeUserAuthClient) RefreshToken(context.Context, *userv1.RefreshTokenRequest) (*userv1.RefreshTokenResponse, error) { return nil, nil } func (f *fakeUserAuthClient) Logout(context.Context, *userv1.LogoutRequest) (*userv1.LogoutResponse, error) { return nil, nil } func TestRoutesWriteUnifiedEnvelopeAndReuseRequestID(t *testing.T) { client := &fakeRoomClient{} router := NewHandler(client).Routes(auth.NewVerifier("secret")) body := []byte(`{"room_id":"room-1","owner_user_id":42,"host_user_id":42,"seat_count":8,"mode":"voice"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("X-Request-ID", "req-test") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if recorder.Header().Get("X-Request-ID") != "req-test" { t.Fatalf("response request header mismatch: got %q", recorder.Header().Get("X-Request-ID")) } var response responseEnvelope if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } if response.Code != codeOK || response.Message != "ok" || response.RequestID != "req-test" { t.Fatalf("unexpected envelope: %+v", response) } if client.lastCreate == nil || client.lastCreate.GetMeta().GetRequestId() != "req-test" { t.Fatalf("request_id was not propagated to room-service meta: %+v", client.lastCreate) } if client.lastCreate.GetMeta().GetActorUserId() != 42 { t.Fatalf("actor user mismatch: got %d", client.lastCreate.GetMeta().GetActorUserId()) } } func TestThirdPartyLoginUsesPublicEnvelopeAndPropagatesRequestID(t *testing.T) { userClient := &fakeUserAuthClient{} router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret")) body := []byte(`{"provider":"wechat","credential":"openid-1","device_id":"ios"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/third-party/login", bytes.NewReader(body)) request.Header.Set("X-Request-ID", "req-auth-third") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } var response responseEnvelope if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } if response.Code != codeOK || response.RequestID != "req-auth-third" { t.Fatalf("unexpected envelope: %+v", response) } } func TestSetPasswordRequiresTokenAndPropagatesActorUserID(t *testing.T) { userClient := &fakeUserAuthClient{} router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret")) body := []byte(`{"password":"secret-pass"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/set", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("X-Request-ID", "req-set-password") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String()) } if userClient.lastSetPassword == nil || userClient.lastSetPassword.GetUserId() != 42 { t.Fatalf("authenticated user_id was not propagated: %+v", userClient.lastSetPassword) } if userClient.lastSetPassword.GetMeta().GetRequestId() != "req-set-password" { t.Fatalf("request_id was not propagated: %+v", userClient.lastSetPassword.GetMeta()) } } func TestAuthLoginMapsGRPCReason(t *testing.T) { userClient := &fakeUserAuthClient{loginErr: xerr.ToGRPCError(xerr.New(xerr.AuthFailed, "authentication failed"))} router := NewHandler(&fakeRoomClient{}, userClient).Routes(auth.NewVerifier("secret")) body := []byte(`{"display_user_id":"100001","password":"bad","device_id":"ios"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/auth/password/login", bytes.NewReader(body)) request.Header.Set("X-Request-ID", "req-auth-failed") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) assertEnvelope(t, recorder, http.StatusUnauthorized, string(xerr.AuthFailed), "req-auth-failed") } func TestRoutesWriteUnauthorizedEnvelope(t *testing.T) { router := NewHandler(&fakeRoomClient{}).Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader([]byte(`{}`))) request.Header.Set("X-Request-ID", "req-auth") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) assertEnvelope(t, recorder, http.StatusUnauthorized, codeUnauthorized, "req-auth") } func TestRoutesWriteInvalidJSONEnvelope(t *testing.T) { router := NewHandler(&fakeRoomClient{}).Routes(auth.NewVerifier("secret")) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader([]byte(`{`))) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("X-Request-ID", "req-json") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) assertEnvelope(t, recorder, http.StatusBadRequest, codeInvalidJSON, "req-json") } func TestRoutesWriteUpstreamErrorEnvelope(t *testing.T) { router := NewHandler(&fakeRoomClient{createErr: errors.New("room unavailable")}).Routes(auth.NewVerifier("secret")) body := []byte(`{"room_id":"room-1","owner_user_id":42,"host_user_id":42,"seat_count":8,"mode":"voice"}`) request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/create", bytes.NewReader(body)) request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42)) request.Header.Set("X-Request-ID", "req-upstream") recorder := httptest.NewRecorder() router.ServeHTTP(recorder, request) assertEnvelope(t, recorder, http.StatusBadGateway, codeUpstreamError, "req-upstream") } func assertEnvelope(t *testing.T, recorder *httptest.ResponseRecorder, statusCode int, code string, requestID string) { t.Helper() if recorder.Code != statusCode { t.Fatalf("status mismatch: got %d want %d body=%s", recorder.Code, statusCode, recorder.Body.String()) } var response responseEnvelope if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatalf("decode response failed: %v", err) } if response.Code != code || response.RequestID != requestID { t.Fatalf("unexpected envelope: %+v", response) } } func signGatewayToken(t *testing.T, secret string, userID int64) string { t.Helper() token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "user_id": float64(userID), }) signed, err := token.SignedString([]byte(secret)) if err != nil { t.Fatalf("sign token failed: %v", err) } return signed }