package lobbyclient import ( "context" "errors" "net/http" "net/http/httptest" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "galaxy/rtmanager/internal/ports" ) func newTestClient(t *testing.T, baseURL string, timeout time.Duration) *Client { t.Helper() client, err := NewClient(Config{BaseURL: baseURL, RequestTimeout: timeout}) require.NoError(t, err) t.Cleanup(func() { _ = client.Close() }) return client } func TestNewClientValidatesConfig(t *testing.T) { cases := map[string]Config{ "empty base url": {BaseURL: "", RequestTimeout: time.Second}, "non-absolute base url": {BaseURL: "lobby:8095", RequestTimeout: time.Second}, "non-positive timeout": {BaseURL: "http://lobby:8095", RequestTimeout: 0}, } for name, cfg := range cases { t.Run(name, func(t *testing.T) { _, err := NewClient(cfg) require.Error(t, err) }) } } func TestGetGameSuccess(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodGet, r.Method) require.Equal(t, "/api/v1/internal/games/game-1", r.URL.Path) require.Equal(t, "application/json", r.Header.Get("Accept")) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{ "game_id": "game-1", "game_name": "Sample", "status": "running", "target_engine_version": "1.4.2", "current_turn": 0, "runtime_status": "running" }`)) })) defer server.Close() client := newTestClient(t, server.URL, time.Second) got, err := client.GetGame(context.Background(), "game-1") require.NoError(t, err) assert.Equal(t, "game-1", got.GameID) assert.Equal(t, "running", got.Status) assert.Equal(t, "1.4.2", got.TargetEngineVersion) } func TestGetGameNotFound(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusNotFound) _, _ = w.Write([]byte(`{"error":{"code":"not_found","message":"no such game"}}`)) })) defer server.Close() client := newTestClient(t, server.URL, time.Second) _, err := client.GetGame(context.Background(), "missing") require.Error(t, err) assert.True(t, errors.Is(err, ports.ErrLobbyGameNotFound)) assert.False(t, errors.Is(err, ports.ErrLobbyUnavailable)) } func TestGetGameInternalErrorMapsToUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":{"code":"internal_error","message":"boom"}}`)) })) defer server.Close() client := newTestClient(t, server.URL, time.Second) _, err := client.GetGame(context.Background(), "x") require.Error(t, err) assert.True(t, errors.Is(err, ports.ErrLobbyUnavailable)) assert.Contains(t, err.Error(), "500") assert.Contains(t, err.Error(), "internal_error") } func TestGetGameTimeoutMapsToUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { time.Sleep(150 * time.Millisecond) _, _ = w.Write([]byte(`{}`)) })) defer server.Close() client := newTestClient(t, server.URL, 50*time.Millisecond) _, err := client.GetGame(context.Background(), "x") require.Error(t, err) assert.True(t, errors.Is(err, ports.ErrLobbyUnavailable)) } func TestGetGameSuccessMissingGameIDIsUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"status":"running"}`)) })) defer server.Close() client := newTestClient(t, server.URL, time.Second) _, err := client.GetGame(context.Background(), "x") require.Error(t, err) assert.True(t, errors.Is(err, ports.ErrLobbyUnavailable)) assert.Contains(t, err.Error(), "missing game_id") } func TestGetGameRejectsBadInput(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("must not contact lobby on bad input") })) defer server.Close() client := newTestClient(t, server.URL, time.Second) t.Run("empty game id", func(t *testing.T) { _, err := client.GetGame(context.Background(), " ") require.Error(t, err) assert.Contains(t, err.Error(), "game id") }) t.Run("canceled context", func(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := client.GetGame(ctx, "x") require.Error(t, err) assert.True(t, errors.Is(err, context.Canceled)) }) } func TestCloseReleasesConnections(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte(`{"game_id":"x","status":"running","target_engine_version":"1.0.0"}`)) })) defer server.Close() client := newTestClient(t, server.URL, time.Second) _, err := client.GetGame(context.Background(), "x") require.NoError(t, err) assert.NoError(t, client.Close()) assert.NoError(t, client.Close()) // idempotent }