The debug report was only reachable in DEBUG mode, which is the one mode
a player hitting a real bug won't be running. Move it to a "🐛 Report a
bug" item in the game menu: a note field, a download, and a copy-as-text,
on any server.
That knowingly shows the reporter their opponents' hands and the shop
deck order. Credentials are still required, so a report only ever reaches
someone seated at that table. The buy-any-card panel stays DEBUG-only —
it changes the game; a report only reads it.
117 lines
3.6 KiB
Go
117 lines
3.6 KiB
Go
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)
|
|
}
|
|
}
|