package engineclient import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "galaxy/model/rest" "github.com/google/uuid" ) func newTestClient(t *testing.T, srv *httptest.Server) *Client { t.Helper() cli, err := NewClientWithHTTP(Config{CallTimeout: 2 * time.Second, ProbeTimeout: 1 * time.Second}, srv.Client()) if err != nil { t.Fatalf("NewClientWithHTTP: %v", err) } return cli } func TestClientInitSuccess(t *testing.T) { wantID := uuid.New() srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathAdminInit { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.Method != http.MethodPost { t.Fatalf("unexpected method: %s", r.Method) } w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(rest.StateResponse{ID: wantID, Turn: 1, Players: []rest.PlayerState{{ID: uuid.New(), RaceName: "alpha"}}}) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) got, err := cli.Init(context.Background(), srv.URL, rest.InitRequest{Races: []rest.InitRace{{RaceName: "alpha"}}}) if err != nil { t.Fatalf("Init returned error: %v", err) } if got.ID != wantID { t.Fatalf("ID = %s, want %s", got.ID, wantID) } if got.Turn != 1 { t.Fatalf("Turn = %d, want 1", got.Turn) } } func TestClientInitValidationError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, `{"reason":"races empty"}`, http.StatusBadRequest) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) _, err := cli.Init(context.Background(), srv.URL, rest.InitRequest{Races: []rest.InitRace{{RaceName: "x"}}}) if !errors.Is(err, ErrEngineValidation) { t.Fatalf("expected ErrEngineValidation, got %v", err) } } func TestClientInitUnreachableOn5xx(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "boom", http.StatusInternalServerError) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) _, err := cli.Init(context.Background(), srv.URL, rest.InitRequest{Races: []rest.InitRace{{RaceName: "x"}}}) if !errors.Is(err, ErrEngineUnreachable) { t.Fatalf("expected ErrEngineUnreachable, got %v", err) } } func TestClientInitProtocolViolation(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte("not-json")) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) _, err := cli.Init(context.Background(), srv.URL, rest.InitRequest{Races: []rest.InitRace{{RaceName: "x"}}}) if !errors.Is(err, ErrEngineProtocolViolation) { t.Fatalf("expected ErrEngineProtocolViolation, got %v", err) } } func TestClientStatusOK(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathAdminStatus || r.Method != http.MethodGet { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } _ = json.NewEncoder(w).Encode(rest.StateResponse{Turn: 5}) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) got, err := cli.Status(context.Background(), srv.URL) if err != nil { t.Fatalf("Status: %v", err) } if got.Turn != 5 { t.Fatalf("Turn = %d, want 5", got.Turn) } } func TestClientTurnOK(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathAdminTurn || r.Method != http.MethodPut { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } _ = json.NewEncoder(w).Encode(rest.StateResponse{Turn: 6, Finished: true}) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) got, err := cli.Turn(context.Background(), srv.URL) if err != nil { t.Fatalf("Turn: %v", err) } if !got.Finished { t.Fatalf("expected finished=true") } } func TestClientBanishRace(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathAdminRaceBanish || r.Method != http.MethodPost { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } var got rest.BanishRequest _ = json.NewDecoder(r.Body).Decode(&got) if got.RaceName != "loser" { t.Fatalf("got race name %q", got.RaceName) } w.WriteHeader(http.StatusNoContent) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) if err := cli.BanishRace(context.Background(), srv.URL, "loser"); err != nil { t.Fatalf("BanishRace: %v", err) } } func TestClientCommandsForwardsBody(t *testing.T) { want := json.RawMessage(`{"actor":"alpha","cmd":[{"@type":"raceQuit"}]}`) gotResp := json.RawMessage(`{"applied":true}`) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathPlayerCommand || r.Method != http.MethodPut { t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path) } _, _ = w.Write(gotResp) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) resp, err := cli.ExecuteCommands(context.Background(), srv.URL, want) if err != nil { t.Fatalf("ExecuteCommands: %v", err) } if string(resp) != string(gotResp) { t.Fatalf("response = %s, want %s", string(resp), string(gotResp)) } } func TestClientReportsForwardsQuery(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathPlayerReport { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.URL.Query().Get("player") != "alpha" { t.Fatalf("player = %q", r.URL.Query().Get("player")) } if r.URL.Query().Get("turn") != "3" { t.Fatalf("turn = %q", r.URL.Query().Get("turn")) } _, _ = w.Write([]byte(`{"turn":3}`)) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) body, err := cli.GetReport(context.Background(), srv.URL, "alpha", 3) if err != nil { t.Fatalf("GetReport: %v", err) } if !strings.Contains(string(body), `"turn":3`) { t.Fatalf("body = %s", body) } } func TestClientGetOrderForwardsQuery(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathPlayerOrder { t.Fatalf("unexpected path: %s", r.URL.Path) } if r.Method != http.MethodGet { t.Fatalf("unexpected method: %s", r.Method) } if r.URL.Query().Get("player") != "alpha" { t.Fatalf("player = %q", r.URL.Query().Get("player")) } if r.URL.Query().Get("turn") != "3" { t.Fatalf("turn = %q", r.URL.Query().Get("turn")) } _, _ = w.Write([]byte(`{"game_id":"abc","updatedAt":99,"cmd":[]}`)) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) body, status, err := cli.GetOrder(context.Background(), srv.URL, "alpha", 3) if err != nil { t.Fatalf("GetOrder: %v", err) } if status != http.StatusOK { t.Fatalf("status = %d", status) } if !strings.Contains(string(body), `"updatedAt":99`) { t.Fatalf("body = %s", body) } } func TestClientGetOrderNoContent(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusNoContent) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) body, status, err := cli.GetOrder(context.Background(), srv.URL, "alpha", 3) if err != nil { t.Fatalf("GetOrder: %v", err) } if status != http.StatusNoContent { t.Fatalf("status = %d", status) } if body != nil { t.Fatalf("expected nil body on 204, got %s", body) } } func TestClientGetOrderRejectsBadInput(t *testing.T) { cli := newTestClient(t, httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("server must not be hit on bad input") }))) if _, _, err := cli.GetOrder(context.Background(), "http://example.com", "", 0); err == nil { t.Fatal("expected error on empty race name") } if _, _, err := cli.GetOrder(context.Background(), "http://example.com", "alpha", -1); err == nil { t.Fatal("expected error on negative turn") } } func TestClientHealthzSuccess(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != pathHealthz { t.Fatalf("unexpected path: %s", r.URL.Path) } _, _ = w.Write([]byte(`{"status":"ok"}`)) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) if err := cli.Healthz(context.Background(), srv.URL); err != nil { t.Fatalf("Healthz: %v", err) } } func TestClientHealthzFailure(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Error(w, "down", http.StatusServiceUnavailable) })) t.Cleanup(srv.Close) cli := newTestClient(t, srv) if err := cli.Healthz(context.Background(), srv.URL); !errors.Is(err, ErrEngineUnreachable) { t.Fatalf("expected ErrEngineUnreachable, got %v", err) } } func TestClientRejectsInvalidBaseURL(t *testing.T) { cli, err := NewClientWithHTTP(Config{CallTimeout: time.Second, ProbeTimeout: time.Second}, http.DefaultClient) if err != nil { t.Fatalf("NewClientWithHTTP: %v", err) } if _, err := cli.Status(context.Background(), ""); err == nil { t.Fatalf("expected error on empty base URL") } if _, err := cli.Status(context.Background(), "ftp://example.test"); err == nil { t.Fatalf("expected error on non-http base URL") } }