2026-04-25 13:21:39 +08:00

266 lines
9.6 KiB
Go

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
}