package web import ( "context" "encoding/json" "fmt" "io/fs" "net" "net/http" "os" "testing" "time" "pete/internal/games/trivia" "pete/internal/opentdb" "pete/internal/storage" ) // TestDevCasino is not a test. It is the casino, running, on a port, with one // signed-in player who has chips — so the table can be driven in a real browser, // which is the only honest way to review an animation. // // Skipped unless you ask for it: // // PETE_DEV_CASINO=:7788 go test ./internal/web -run TestDevCasino -timeout 0 // // It prints the session cookie to plant. The routes are wired here rather than // taken from New(), because New() decides whether the casino exists at the // moment it builds the mux, and the test rig only signs the player in afterwards. func TestDevCasino(t *testing.T) { addr := os.Getenv("PETE_DEV_CASINO") if addr == "" { t.Skip("set PETE_DEV_CASINO=:port to run the casino for a browser") } s := newCasino(t) fund(t, 5000) fundUser(t, bobPlayer, 5000) seedTriviaBank(t) // The full table runtime, so the turn clock and the reaper are live under the // browser exactly as in production. s.StartTableClock(context.Background()) cookie := devCookie(s, "reala", "Reala") // A second player, so a shared table can be reviewed — hold'em is multiplayer // now, and one browser cannot see two people at the felt. Plant this cookie in // a second browser profile (or a private window) to sit down as Bob. bobCookie := devCookie(s, "bob", "Bob") staticSub, err := fs.Sub(staticFS, "static") if err != nil { t.Fatal(err) } mux := http.NewServeMux() mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticSub)))) s.casinoRoutes(mux) ln, err := net.Listen("tcp", addr) if err != nil { t.Fatal(err) } // Written to a file, not printed: `go test` buffers stdout, and the browser // driver needs the cookie while the server is still running. The second cookie // rides alongside it, newline-separated, for the second browser. if out := os.Getenv("PETE_DEV_COOKIE_FILE"); out != "" { if err := os.WriteFile(out, []byte(cookie+"\n"+bobCookie), 0o600); err != nil { t.Fatal(err) } } fmt.Printf("\nCASINO http://localhost%s/games\nCOOKIE %s=%s\nBOB %s=%s\n\n", addr, sessionCookie, cookie, sessionCookie, bobCookie) srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second} t.Cleanup(func() { _ = srv.Close() }) _ = srv.Serve(ln) } // devCookie mints a signed session for a player the rig has funded, so the felt // can be driven as them. func devCookie(s *Server, username, name string) string { payload, _ := json.Marshal(SessionUser{ Sub: "sub-" + username, Username: username, Name: name, Exp: time.Now().Add(24 * time.Hour).Unix(), }) return s.auth.sign(payload) } // seedTriviaBank puts enough questions in the bank to deal a ladder of each // difficulty. // // The rig does not run StartTriviaBank — a dev casino that spends its first two // minutes dripping four hundred questions per difficulty out of a free API is a // dev casino you cannot use. But a fresh database has an empty bank, and an // empty bank means every start 503s, so the rig would be unable to show you the // one game it exists to show you. // // One real batch per difficulty, through the real client: fifty questions is // four ladders' worth, and it means what the browser renders came out of OpenTDB // and through the same decode-and-store path production uses, entities and all. func seedTriviaBank(t *testing.T) { t.Helper() ctx := context.Background() client := opentdb.New() for i, tier := range trivia.Tiers { have, err := storage.CountTrivia(tier.Difficulty) if err != nil { t.Fatal(err) } if have >= trivia.Rungs { continue } if i > 0 { time.Sleep(opentdb.Politeness) // the API asks; asking faster earns nothing } qs, err := client.Fetch(ctx, tier.Difficulty, opentdb.Batch) if err != nil { t.Fatalf("seeding the trivia bank (%s): %v", tier.Difficulty, err) } added, err := storage.AddTriviaQuestions(tier.Difficulty, qs) if err != nil { t.Fatal(err) } fmt.Printf("BANK %-6s %d questions\n", tier.Difficulty, added) } }