Put the bug report behind an always-available button.

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.
This commit is contained in:
Greyson Parrelli
2026-08-10 13:42:31 -04:00
parent 8ccde03023
commit 6ae4d41976
9 changed files with 167 additions and 123 deletions
+16 -15
View File
@@ -12,9 +12,10 @@ import (
"github.com/greyson/super-auto-pets-board-game/internal/store"
)
// reportServer stands up a server holding one started two-player game, and
// returns it with the credentials for seat 0.
func reportServer(t *testing.T, debug bool) (*httptest.Server, *game.Game, *game.Player) {
// 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 {
@@ -31,8 +32,8 @@ func reportServer(t *testing.T, debug bool) (*httptest.Server, *game.Game, *game
if err := st.Save(g); err != nil {
t.Fatal(err)
}
srv := New(st, "", debug)
srv.rooms[g.ID] = &room{game: g, conns: map[*client]struct{}{}, debug: debug}
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
@@ -52,10 +53,10 @@ func get(t *testing.T, url string) (int, string) {
return res.StatusCode, string(body)
}
// In DEBUG mode a seated player can pull the report, in both forms, and the
// JSON one restores to the game the server is actually holding.
// 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, true)
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+"&note=shop+row+looked+wrong")
@@ -90,16 +91,16 @@ func TestDebugReportEndpoint(t *testing.T) {
}
}
// A report exposes both players' decks and the shop deck order, so it must not
// be reachable without DEBUG, and never on someone else's credentials.
func TestDebugReportIsGated(t *testing.T) {
ts, g, p1 := reportServer(t, false)
// 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.StatusNotFound {
t.Fatalf("without DEBUG the endpoint should 404, got %d", status)
if status != http.StatusOK {
t.Fatalf("a player should get their report without DEBUG, got %d", status)
}
ts, g, p1 = reportServer(t, true)
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)
+8 -8
View File
@@ -59,15 +59,15 @@ func (s *Server) Handler() http.Handler {
// `format=text` renders it for reading instead of as JSON, and `note=` records
// what looked wrong.
//
// A report holds hidden information — the opponents' decks, the order of the
// shop decks — so a seated player must not be able to pull one mid-game. It is
// DEBUG-only for that reason; on a live server, `go run ./cmd/report` reads the
// same report straight out of the database instead.
// This backs the in-game "Report a bug" button, so it is deliberately not
// DEBUG-gated: a reproducible bug report is worth more than the hidden
// information a report gives away. It does hand a seated player their
// opponents' hands and the order of the shop decks, which a determined one
// could read mid-game — the trade accepted here is that a player who wants to
// cheat gains little and a player who hits a bug can actually report it.
// Credentials are still required, so a report only ever goes to someone at that
// table.
func (s *Server) handleDebugReport(w http.ResponseWriter, req *http.Request) {
if !s.debug {
httpError(w, http.StatusNotFound, "debug reports are only served in DEBUG mode")
return
}
q := req.URL.Query()
r, err := s.getRoom(q.Get("game"))
if err != nil {