package gmclient_test import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "time" "galaxy/lobby/internal/adapters/gmclient" "galaxy/lobby/internal/domain/common" "galaxy/lobby/internal/ports" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func validRequest() ports.RegisterGameRequest { return ports.RegisterGameRequest{ GameID: common.GameID("game-1"), ContainerID: "container-1", EngineEndpoint: "engine.local:9000", TargetEngineVersion: "v1.2.3", TurnSchedule: "0 18 * * *", } } func TestNewClientValidatesConfig(t *testing.T) { _, err := gmclient.NewClient(gmclient.Config{Timeout: time.Second}) require.Error(t, err) _, err = gmclient.NewClient(gmclient.Config{BaseURL: "http://gm.local"}) require.Error(t, err) } func TestRegisterGameSendsExpectedRequest(t *testing.T) { var observed struct { method string path string contentType string body []byte } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { observed.method = r.Method observed.path = r.URL.Path observed.contentType = r.Header.Get("Content-Type") observed.body, _ = io.ReadAll(r.Body) w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: time.Second}) require.NoError(t, err) require.NoError(t, client.RegisterGame(context.Background(), validRequest())) assert.Equal(t, http.MethodPost, observed.method) assert.Equal(t, "/api/v1/internal/games/game-1/register-runtime", observed.path) assert.Equal(t, "application/json", observed.contentType) var decoded map[string]string require.NoError(t, json.Unmarshal(observed.body, &decoded)) assert.Equal(t, "container-1", decoded["container_id"]) assert.Equal(t, "engine.local:9000", decoded["engine_endpoint"]) assert.Equal(t, "v1.2.3", decoded["target_engine_version"]) assert.Equal(t, "0 18 * * *", decoded["turn_schedule"]) } func TestRegisterGameWrapsServerErrorWithUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: time.Second}) require.NoError(t, err) err = client.RegisterGame(context.Background(), validRequest()) require.Error(t, err) assert.ErrorIs(t, err, ports.ErrGMUnavailable) } func TestRegisterGameWrapsTimeoutWithUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case <-r.Context().Done(): case <-time.After(200 * time.Millisecond): } w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: 10 * time.Millisecond}) require.NoError(t, err) err = client.RegisterGame(context.Background(), validRequest()) require.Error(t, err) assert.ErrorIs(t, err, ports.ErrGMUnavailable) } func TestPingHitsExpectedEndpoint(t *testing.T) { var observed struct { method string path string accept string } server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { observed.method = r.Method observed.path = r.URL.Path observed.accept = r.Header.Get("Accept") w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: time.Second}) require.NoError(t, err) require.NoError(t, client.Ping(context.Background())) assert.Equal(t, http.MethodGet, observed.method) assert.Equal(t, "/api/v1/internal/healthz", observed.path) assert.Equal(t, "application/json", observed.accept) } func TestPingWrapsServerErrorWithUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: time.Second}) require.NoError(t, err) err = client.Ping(context.Background()) require.Error(t, err) assert.ErrorIs(t, err, ports.ErrGMUnavailable) } func TestPingWrapsTimeoutWithUnavailable(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { select { case <-r.Context().Done(): case <-time.After(200 * time.Millisecond): } w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: 10 * time.Millisecond}) require.NoError(t, err) err = client.Ping(context.Background()) require.Error(t, err) assert.ErrorIs(t, err, ports.ErrGMUnavailable) } func TestRegisterGameValidatesRequest(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })) t.Cleanup(server.Close) client, err := gmclient.NewClient(gmclient.Config{BaseURL: server.URL, Timeout: time.Second}) require.NoError(t, err) bad := validRequest() bad.ContainerID = "" err = client.RegisterGame(context.Background(), bad) require.Error(t, err) bad = validRequest() bad.GameID = common.GameID("bogus") err = client.RegisterGame(context.Background(), bad) require.Error(t, err) }