400 lines
12 KiB
Go
400 lines
12 KiB
Go
package userservice
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"galaxy/gateway/internal/downstream"
|
|
usermodel "galaxy/model/user"
|
|
"galaxy/transcoder"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNewHTTPClient(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
baseURL string
|
|
wantURL string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "absolute URL is normalized",
|
|
baseURL: " http://127.0.0.1:8081/ ",
|
|
wantURL: "http://127.0.0.1:8081",
|
|
},
|
|
{
|
|
name: "empty base URL is rejected",
|
|
baseURL: " ",
|
|
wantErr: "base URL must not be empty",
|
|
},
|
|
{
|
|
name: "relative base URL is rejected",
|
|
baseURL: "/relative",
|
|
wantErr: "base URL must be absolute",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tt := tt
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client, err := NewHTTPClient(tt.baseURL)
|
|
if tt.wantErr != "" {
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), tt.wantErr)
|
|
return
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
assert.Equal(t, tt.wantURL, client.baseURL)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHTTPClientExecuteGetMyAccountSuccess(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
wantResponse := sampleAccountResponse()
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
require.Equal(t, http.MethodGet, request.Method)
|
|
require.Equal(t, "/api/v1/internal/users/user-123/account", request.URL.Path)
|
|
require.NoError(t, json.NewEncoder(writer).Encode(wantResponse))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
|
require.NoError(t, err)
|
|
|
|
result, err := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeGetMyAccount,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, getMyAccountResultCodeOK, result.ResultCode)
|
|
|
|
decoded, err := transcoder.PayloadToAccountResponse(result.PayloadBytes)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, wantResponse, decoded)
|
|
}
|
|
|
|
func TestHTTPClientExecuteUpdateMyProfileProjectsConflict(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
require.Equal(t, http.MethodPost, request.Method)
|
|
require.Equal(t, "/api/v1/internal/users/user-123/profile", request.URL.Path)
|
|
|
|
body, err := io.ReadAll(request.Body)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, `{"race_name":"Nova Prime"}`, string(body))
|
|
|
|
writer.WriteHeader(http.StatusConflict)
|
|
require.NoError(t, json.NewEncoder(writer).Encode(&usermodel.ErrorResponse{
|
|
Error: usermodel.ErrorBody{
|
|
Code: "conflict",
|
|
Message: "request conflicts with current state",
|
|
},
|
|
}))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.UpdateMyProfileRequestToPayload(&usermodel.UpdateMyProfileRequest{RaceName: "Nova Prime"})
|
|
require.NoError(t, err)
|
|
|
|
result, err := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeUpdateMyProfile,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "conflict", result.ResultCode)
|
|
|
|
decoded, err := transcoder.PayloadToErrorResponse(result.PayloadBytes)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, &usermodel.ErrorResponse{
|
|
Error: usermodel.ErrorBody{
|
|
Code: "conflict",
|
|
Message: "request conflicts with current state",
|
|
},
|
|
}, decoded)
|
|
}
|
|
|
|
func TestHTTPClientExecuteUpdateMySettingsProjectsInvalidRequest(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
require.Equal(t, http.MethodPost, request.Method)
|
|
require.Equal(t, "/api/v1/internal/users/user-123/settings", request.URL.Path)
|
|
|
|
body, err := io.ReadAll(request.Body)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, `{"preferred_language":"bad","time_zone":"Mars/Base"}`, string(body))
|
|
|
|
writer.WriteHeader(http.StatusBadRequest)
|
|
require.NoError(t, json.NewEncoder(writer).Encode(&usermodel.ErrorResponse{
|
|
Error: usermodel.ErrorBody{
|
|
Code: "invalid_request",
|
|
Message: "request is invalid",
|
|
},
|
|
}))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.UpdateMySettingsRequestToPayload(&usermodel.UpdateMySettingsRequest{
|
|
PreferredLanguage: "bad",
|
|
TimeZone: "Mars/Base",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeUpdateMySettings,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "invalid_request", result.ResultCode)
|
|
|
|
decoded, err := transcoder.PayloadToErrorResponse(result.PayloadBytes)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "invalid_request", decoded.Error.Code)
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandProjectsSubjectNotFound(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusNotFound)
|
|
require.NoError(t, json.NewEncoder(writer).Encode(&usermodel.ErrorResponse{
|
|
Error: usermodel.ErrorBody{
|
|
Code: "subject_not_found",
|
|
Message: "subject not found",
|
|
},
|
|
}))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
|
require.NoError(t, err)
|
|
|
|
result, err := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-missing",
|
|
MessageType: usermodel.MessageTypeGetMyAccount,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "subject_not_found", result.ResultCode)
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandMaps503ToUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
writer.WriteHeader(http.StatusServiceUnavailable)
|
|
require.NoError(t, json.NewEncoder(writer).Encode(&usermodel.ErrorResponse{
|
|
Error: usermodel.ErrorBody{
|
|
Code: "service_unavailable",
|
|
Message: "service is unavailable",
|
|
},
|
|
}))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeGetMyAccount,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, downstream.ErrDownstreamUnavailable)
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandUsesCallerContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
<-request.Context().Done()
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 25*time.Millisecond)
|
|
defer cancel()
|
|
|
|
_, err = client.ExecuteCommand(ctx, downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeGetMyAccount,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, context.DeadlineExceeded)
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandRejectsMalformedSuccessPayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
_, _ = writer.Write([]byte(`{"account":{"user_id":"user-123","unexpected":true}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
payload, err := transcoder.GetMyAccountRequestToPayload(&usermodel.GetMyAccountRequest{})
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: usermodel.MessageTypeGetMyAccount,
|
|
PayloadBytes: payload,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "decode success response")
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandRejectsUnsupportedMessageType(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.NotFoundHandler())
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
|
|
_, err := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: "user.unsupported",
|
|
PayloadBytes: []byte("payload"),
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unsupported message type")
|
|
}
|
|
|
|
func TestNewRoutesReserveUserMessageTypesWhenUnconfigured(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
routes, closeFn, err := NewRoutes("")
|
|
require.NoError(t, err)
|
|
require.NoError(t, closeFn())
|
|
|
|
router := downstream.NewStaticRouter(routes)
|
|
for _, messageType := range []string{
|
|
usermodel.MessageTypeGetMyAccount,
|
|
usermodel.MessageTypeUpdateMyProfile,
|
|
usermodel.MessageTypeUpdateMySettings,
|
|
} {
|
|
client, routeErr := router.Route(messageType)
|
|
require.NoError(t, routeErr)
|
|
|
|
_, execErr := client.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{
|
|
UserID: "user-123",
|
|
MessageType: messageType,
|
|
})
|
|
require.Error(t, execErr)
|
|
assert.ErrorIs(t, execErr, downstream.ErrDownstreamUnavailable)
|
|
}
|
|
}
|
|
|
|
func TestUnavailableClientReturnsDownstreamUnavailable(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := unavailableClient{}.ExecuteCommand(context.Background(), downstream.AuthenticatedCommand{})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, downstream.ErrDownstreamUnavailable)
|
|
}
|
|
|
|
func newTestHTTPClient(t *testing.T, server *httptest.Server) *HTTPClient {
|
|
t.Helper()
|
|
|
|
client, err := newHTTPClient(server.URL, server.Client())
|
|
require.NoError(t, err)
|
|
return client
|
|
}
|
|
|
|
func sampleAccountResponse() *usermodel.AccountResponse {
|
|
now := time.Date(2026, time.April, 9, 10, 0, 0, 0, time.UTC)
|
|
expiresAt := now.Add(30 * 24 * time.Hour)
|
|
|
|
return &usermodel.AccountResponse{
|
|
Account: usermodel.Account{
|
|
UserID: "user-123",
|
|
Email: "pilot@example.com",
|
|
RaceName: "Pilot Nova",
|
|
PreferredLanguage: "en",
|
|
TimeZone: "Europe/Kaliningrad",
|
|
DeclaredCountry: "DE",
|
|
Entitlement: usermodel.EntitlementSnapshot{
|
|
PlanCode: "free",
|
|
IsPaid: false,
|
|
Source: "auth_registration",
|
|
Actor: usermodel.ActorRef{Type: "service", ID: "user-service"},
|
|
ReasonCode: "initial_free_entitlement",
|
|
StartsAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
ActiveSanctions: []usermodel.ActiveSanction{
|
|
{
|
|
SanctionCode: "profile_update_block",
|
|
Scope: "lobby",
|
|
ReasonCode: "manual_block",
|
|
Actor: usermodel.ActorRef{Type: "admin", ID: "admin-1"},
|
|
AppliedAt: now,
|
|
ExpiresAt: &expiresAt,
|
|
},
|
|
},
|
|
ActiveLimits: []usermodel.ActiveLimit{
|
|
{
|
|
LimitCode: "max_owned_private_games",
|
|
Value: 3,
|
|
ReasonCode: "manual_override",
|
|
Actor: usermodel.ActorRef{Type: "admin", ID: "admin-1"},
|
|
AppliedAt: now,
|
|
},
|
|
},
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestDecodeUserServiceErrorNormalizesBlankFields(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
response, err := decodeUserServiceError(http.StatusBadRequest, []byte(`{"error":{"code":" ","message":" "}}`))
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "invalid_request", response.Error.Code)
|
|
assert.Equal(t, "request is invalid", response.Error.Message)
|
|
}
|
|
|
|
func TestHTTPClientExecuteCommandRejectsNilContext(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
server := httptest.NewServer(http.NotFoundHandler())
|
|
defer server.Close()
|
|
|
|
client := newTestHTTPClient(t, server)
|
|
|
|
_, err := client.ExecuteCommand(nil, downstream.AuthenticatedCommand{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "nil context")
|
|
}
|