Files
Pete/internal/web/games_og_test.go
prosolis 7ca1f7a030 games: the door you can see from outside, and the picture on it
We never had Open Graph on the casino, and adding meta tags would not have
fixed it. Every route was behind requirePlayer, so a link pasted into a chat
window got a 302 to sign-in and unfurled as whatever the auth screen said:
"parodia.dev", no image, no description. Tags on a page a stranger cannot
fetch are tags nobody reads. So the casino now has a front door — a real page,
served to anybody, that says what the place is and offers a way in. You still
can't play from it, and every table still bounces you to sign-in.

The share card is drawn in Go rather than checked in as a picture, because the
casino has two names on a clock and the card keeps the joke: paste the link in
daylight and you get Casinopolis on green felt, paste it after six and the neon
is on and it says Casino Night Zone. Same roomAt() rule as everywhere else,
except the clock that decides is the server's — an unfurl bot has no evening of
its own. Both cards are drawn once, at first ask, and kept.

Two things worth keeping from building it. color.RGBA is alpha-premultiplied,
and the lamp over the table wrote raw channels next to a low alpha, which is
not a dim glow but an invalid colour: image/draw ran it past 255 and wrapped
the hue, and the first card came out with a blue dome over a green stripe. If
a colour here ever comes out impossible, look for a missing premultiply. And
og:image has to be an absolute URL that actually resolves, which is two
different addresses depending on how you arrived: /og.png on the games host
(hostRouter puts the /games back on) and /games/og.png anywhere else. The dev
rig advertised the first while serving only the second. The test now reads the
URL off the page and goes and fetches it, on both hosts, because an og:image
that 404s is worth exactly as much as no og:image.

Fredoka is vendored (OFL) — the page can reach for a font over the network and
a server drawing a PNG cannot.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
2026-07-14 13:30:52 -07:00

167 lines
5.0 KiB
Go

package web
import (
"bytes"
"image/png"
"net/http"
"net/http/httptest"
"net/url"
"regexp"
"strings"
"testing"
)
// The casino had no share card for as long as it existed, and the reason is worth
// keeping in a test rather than in a memory: every route was behind sign-in, so a
// link pasted into a chat window unfurled as whatever the *auth screen* said. Meta
// tags on a page nobody unauthenticated can fetch are meta tags nobody reads.
//
// So the two things these tests hold down are: a stranger gets a real page at the
// front door, and a stranger gets the picture. Everything else stays shut.
func TestTheDoorIsOpenToStrangers(t *testing.T) {
s := newCasino(t)
mux := http.NewServeMux()
s.casinoRoutes(mux)
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", "/games", nil))
if w.Code != http.StatusOK {
t.Fatalf("GET /games as a stranger = %d, want 200: the front door has to be a page an unfurl bot can read", w.Code)
}
body := w.Body.String()
for _, want := range []string{
`property="og:image"`,
`property="og:title"`,
`property="og:description"`,
`name="twitter:card"`,
"og.png",
"Sign in",
} {
if !strings.Contains(body, want) {
t.Errorf("the door is missing %q", want)
}
}
if strings.Contains(body, "unknown page") {
t.Fatal("games_door is not in the games template set in server.go")
}
// A door you can play from is not a door.
if strings.Contains(body, `data-move=`) {
t.Error("the door is serving a table to somebody who has not signed in")
}
}
func TestTheTablesStayShutToStrangers(t *testing.T) {
s := newCasino(t)
mux := http.NewServeMux()
s.casinoRoutes(mux)
for _, path := range []string{"/games/blackjack", "/games/holdem", "/games/uno", "/games/trivia", "/games/hangman", "/games/solitaire"} {
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
if w.Code != http.StatusFound {
t.Errorf("GET %s as a stranger = %d, want a 302 to sign-in", path, w.Code)
}
}
}
func TestTheShareCardNeedsNoSignIn(t *testing.T) {
s := newCasino(t)
mux := http.NewServeMux()
s.casinoRoutes(mux)
w := httptest.NewRecorder()
mux.ServeHTTP(w, httptest.NewRequest("GET", "/games/og.png", nil))
if w.Code != http.StatusOK {
t.Fatalf("GET /games/og.png as a stranger = %d, want 200", w.Code)
}
if ct := w.Header().Get("Content-Type"); ct != "image/png" {
t.Errorf("Content-Type = %q, want image/png", ct)
}
img, err := png.Decode(bytes.NewReader(w.Body.Bytes()))
if err != nil {
t.Fatalf("the share card is not a PNG: %v", err)
}
if b := img.Bounds(); b.Dx() != ogWidth || b.Dy() != ogHeight {
t.Errorf("share card is %dx%d, want %dx%d — every unfurler crops to that", b.Dx(), b.Dy(), ogWidth, ogHeight)
}
}
// An og:image that 404s is worth exactly as much as no og:image, and the two ways
// the casino is reachable disagree about where the picture lives: on the games host
// it's /og.png (hostRouter puts the /games back on), and anywhere else it's
// /games/og.png. So take the URL the page actually advertises and go and get it.
func TestTheAdvertisedShareCardIsReallyThere(t *testing.T) {
for _, host := range []string{"", "games.parodia.dev"} {
name := "no games host"
if host != "" {
name = host
}
t.Run(name, func(t *testing.T) {
s := newCasino(t)
s.cfg.Games.Host = host
mux := http.NewServeMux()
s.casinoRoutes(mux)
srv := s.hostRouter(mux) // what production actually serves
// Ask the door where its picture is.
w := httptest.NewRecorder()
door := httptest.NewRequest("GET", "/games", nil)
if host != "" {
door.Host = host
}
srv.ServeHTTP(w, door)
img := ogImageURL(t, w.Body.String())
// Then go and fetch it, the way a chat server would.
u, err := url.Parse(img)
if err != nil {
t.Fatalf("og:image %q is not a URL: %v", img, err)
}
if u.Host == "" {
t.Fatalf("og:image %q is relative — no unfurler will resolve it", img)
}
w = httptest.NewRecorder()
get := httptest.NewRequest("GET", u.Path, nil)
get.Host = u.Host
srv.ServeHTTP(w, get)
if w.Code != http.StatusOK {
t.Fatalf("the door advertises og:image %s, and fetching it gives %d", img, w.Code)
}
if ct := w.Header().Get("Content-Type"); ct != "image/png" {
t.Errorf("og:image %s served %q, want image/png", img, ct)
}
})
}
}
func ogImageURL(t *testing.T, body string) string {
t.Helper()
m := regexp.MustCompile(`property="og:image" content="([^"]+)"`).FindStringSubmatch(body)
if m == nil {
t.Fatal("the door serves no og:image at all")
}
return m[1]
}
// Both rooms have to have a card, because the room is picked at request time and a
// missing one serves zero bytes rather than an error.
func TestTheShareCardKnowsBothRooms(t *testing.T) {
cards, err := drawShareCards()
if err != nil {
t.Fatal(err)
}
for _, rm := range []room{roomDay, roomNight} {
if len(cards[rm.Slug]) == 0 {
t.Errorf("no share card for %s", rm.Slug)
}
}
if len(cards) != len(ogPalettes) {
t.Errorf("drew %d cards for %d palettes", len(cards), len(ogPalettes))
}
}