266 lines
9.6 KiB
Go
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
|
|
}
|