package server import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/greyson/super-auto-pets-board-game/internal/game" "github.com/greyson/super-auto-pets-board-game/internal/store" ) // reportServer stands up an ordinary server — DEBUG off, the way it runs in // production — holding one started two-player game, and returns it with the // credentials for seat 0. func reportServer(t *testing.T) (*httptest.Server, *game.Game, *game.Player) { t.Helper() st, err := store.Open(t.TempDir()) if err != nil { t.Fatal(err) } t.Cleanup(func() { st.Close() }) g := game.New() p1, _ := g.AddPlayer("Alice") g.AddPlayer("Bob") if err := g.StartGame(); err != nil { t.Fatal(err) } if err := st.Save(g); err != nil { t.Fatal(err) } srv := New(st, "", false) srv.rooms[g.ID] = &room{game: g, conns: map[*client]struct{}{}} ts := httptest.NewServer(srv.Handler()) t.Cleanup(ts.Close) return ts, g, p1 } func get(t *testing.T, url string) (int, string) { t.Helper() res, err := http.Get(url) if err != nil { t.Fatal(err) } defer res.Body.Close() body, err := io.ReadAll(res.Body) if err != nil { t.Fatal(err) } return res.StatusCode, string(body) } // A seated player can pull the report in both forms, and the JSON one restores // to the game the server is actually holding. func TestDebugReportEndpoint(t *testing.T) { ts, g, p1 := reportServer(t) url := ts.URL + "/api/debug/report?game=" + g.ID + "&player=" + p1.ID + "&token=" + p1.Token status, body := get(t, url+"¬e=shop+row+looked+wrong") if status != http.StatusOK { t.Fatalf("json report: status %d, body %s", status, body) } var rep game.DebugReport if err := json.Unmarshal([]byte(body), &rep); err != nil { t.Fatalf("the report isn't valid JSON: %v", err) } if rep.Note != "shop row looked wrong" { t.Fatalf("the note didn't make it into the report: %q", rep.Note) } if rep.CapturedAt == "" { t.Fatal("the server should stamp the report with a capture time") } restored, err := rep.Game() if err != nil { t.Fatal(err) } if restored.Code != g.Code || len(restored.Players) != len(g.Players) { t.Fatalf("restored game %s with %d players, want %s with %d", restored.Code, len(restored.Players), g.Code, len(g.Players)) } status, text := get(t, url+"&format=text") if status != http.StatusOK { t.Fatalf("text report: status %d", status) } if !strings.Contains(text, g.Code) || !strings.Contains(text, "=== Players ===") { t.Fatalf("the text report doesn't look like a report:\n%s", text) } } // The in-game bug report button has to work on a normal server, so the endpoint // is not DEBUG-gated — but it is still a seated player's own artifact, and never // reachable on someone else's credentials. func TestDebugReportNeedsCredentialsNotDebugMode(t *testing.T) { ts, g, p1 := reportServer(t) status, _ := get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token="+p1.Token) if status != http.StatusOK { t.Fatalf("a player should get their report without DEBUG, got %d", status) } status, _ = get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player="+p1.ID+"&token=wrong") if status != http.StatusForbidden { t.Fatalf("a bad token should 403, got %d", status) } status, _ = get(t, ts.URL+"/api/debug/report?game="+g.ID+"&player=nobody&token="+p1.Token) if status != http.StatusForbidden { t.Fatalf("an unknown player should 403, got %d", status) } status, _ = get(t, ts.URL+"/api/debug/report?game=nosuchgame&player="+p1.ID+"&token="+p1.Token) if status != http.StatusNotFound { t.Fatalf("an unknown game should 404, got %d", status) } }