94534ad0f2
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 16s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m8s
The games-list status filter offered only active/finished; add 'open' (auto-match games awaiting an opponent) to the subnav and accept it in normalizeGameStatus. Render test covers the new filter link.
129 lines
5.7 KiB
Go
129 lines
5.7 KiB
Go
package adminconsole
|
|
|
|
import (
|
|
"bytes"
|
|
"html/template"
|
|
"io/fs"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestRendererRendersEveryPage parses the embedded templates and renders each
|
|
// page with a representative view, asserting the page executes, carries the
|
|
// shared layout chrome and shows a distinctive value.
|
|
func TestRendererRendersEveryPage(t *testing.T) {
|
|
r, err := NewRenderer()
|
|
if err != nil {
|
|
t.Fatalf("new renderer: %v", err)
|
|
}
|
|
cases := []struct {
|
|
page string
|
|
data any
|
|
want string
|
|
}{
|
|
{"dashboard", DashboardView{Accounts: 3, Variants: []VariantVersions{{Variant: "scrabble_en", Latest: "v1", Versions: []string{"v1"}}}}, "Dashboard"},
|
|
{"users", UsersView{Items: []UserRow{{ID: "a1", DisplayName: "Kaya", FlaggedHighRate: true}}, Pager: NewPager(1, 50, 1)}, "high-rate"},
|
|
{"user_detail", UserDetailView{ID: "a1", DisplayName: "Kaya", HasStats: true, Stats: StatsRow{Wins: 2}, TelegramID: "123", ConnectorEnabled: true}, "Send Telegram message"},
|
|
{"user_detail", UserDetailView{ID: "a1", DisplayName: "Kaya", FlaggedHighRateAt: "2026-06-10 12:00"}, "Clear high-rate flag"},
|
|
{"throttled", ThrottledView{
|
|
Episodes: []ThrottleEpisodeRow{{Class: "user", Key: "a1", UserID: "a1", Rejected: 1234, FirstSeen: "2026-06-10 12:00", LastSeen: "2026-06-10 12:05"}},
|
|
Flagged: []FlaggedAccountRow{{ID: "a1", DisplayName: "Kaya", FlaggedAt: "2026-06-10 12:05"}},
|
|
FlagThreshold: 1000, FlagWindow: "10m0s",
|
|
}, "Recent episodes"},
|
|
{"games", GamesView{Items: []GameRow{{ID: "g1", Variant: "scrabble_en", Status: "active"}}, Status: "active", Pager: NewPager(1, 50, 1)}, "g1"},
|
|
{"games", GamesView{Items: []GameRow{{ID: "g-open", Variant: "scrabble_en", Status: "open"}}, Status: "open", Pager: NewPager(1, 50, 1)}, "?status=open"},
|
|
{"game_detail", GameDetailView{ID: "g1", Variant: "scrabble_en", Seats: []SeatRow{{Seat: 0, DisplayName: "Kaya"}}}, "Seats"},
|
|
{"complaints", ComplaintsView{Items: []ComplaintRow{{ID: "c1", Word: "qi", Status: "open"}}, Status: "open", Pager: NewPager(1, 50, 1)}, "qi"},
|
|
{"messages", MessagesView{Items: []MessageRow{{ID: "m1", SenderID: "a1", SenderName: "Kaya", Source: "telegram", Body: "good luck", GameID: "g1"}}, Pager: NewPager(1, 50, 1)}, "good luck"},
|
|
{"complaint_detail", ComplaintDetailView{ID: "c1", Word: "qi", Variant: "scrabble_en"}, "Resolve"},
|
|
{"dictionary", DictionaryView{Variants: []VariantVersions{{Variant: "scrabble_en", Latest: "v1", Versions: []string{"v1"}}}, Changes: []DictChangeRow{{Variant: "scrabble_en", Word: "qi", Action: "add"}}}, "Hot-reload"},
|
|
{"broadcast", BroadcastView{ConnectorEnabled: true}, "Post to the game channel"},
|
|
{"message", MessageView{Heading: "Done", Body: "ok", Back: "/_gm/"}, "Done"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.page, func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
if err := r.Render(&buf, tc.page, PageData{Title: tc.page, Data: tc.data}); err != nil {
|
|
t.Fatalf("render %s: %v", tc.page, err)
|
|
}
|
|
out := buf.String()
|
|
if !strings.Contains(out, tc.want) {
|
|
t.Errorf("render %s: missing %q in output", tc.page, tc.want)
|
|
}
|
|
if !strings.Contains(out, "Scrabble · admin") {
|
|
t.Errorf("render %s: missing layout chrome", tc.page)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestPagerLinksPreserveFilterQuery guards the paginated lists whose links carry a
|
|
// pre-encoded filter query (url.Values.Encode) past the page number. The query fragment
|
|
// must reach the link verbatim: the contextual escaper would otherwise re-encode its
|
|
// structural "=" and "&" (turning "kind=robots" into the broken "kind%3drobots"), dropping
|
|
// the active filter on every page step. FilterQuery is typed template.URL to prevent that.
|
|
func TestPagerLinksPreserveFilterQuery(t *testing.T) {
|
|
r := MustNewRenderer()
|
|
|
|
t.Run("users", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
view := UsersView{Robots: true, FilterQuery: template.URL("kind=robots"), Pager: NewPager(2, 50, 200)}
|
|
if err := r.Render(&buf, "users", PageData{Title: "Users", Data: view}); err != nil {
|
|
t.Fatalf("render users: %v", err)
|
|
}
|
|
out := buf.String()
|
|
for _, want := range []string{
|
|
`href="/_gm/users?kind=robots&page=1"`,
|
|
`href="/_gm/users?kind=robots&page=3"`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("users pager: missing %q in:\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "kind%3drobots") {
|
|
t.Error("users pager: filter query was double-encoded (kind%3drobots)")
|
|
}
|
|
})
|
|
|
|
t.Run("messages", func(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
view := MessagesView{FilterQuery: template.URL("game=abc&user=def"), Pager: NewPager(2, 50, 200)}
|
|
if err := r.Render(&buf, "messages", PageData{Title: "Messages", Data: view}); err != nil {
|
|
t.Fatalf("render messages: %v", err)
|
|
}
|
|
out := buf.String()
|
|
for _, want := range []string{
|
|
`href="/_gm/messages.csv?game=abc&user=def"`,
|
|
`href="/_gm/messages?game=abc&user=def&page=1"`,
|
|
`href="/_gm/messages?game=abc&user=def&page=3"`,
|
|
} {
|
|
if !strings.Contains(out, want) {
|
|
t.Errorf("messages pager: missing %q in:\n%s", want, out)
|
|
}
|
|
}
|
|
if strings.Contains(out, "%3d") || strings.Contains(out, "%26") {
|
|
t.Error("messages pager: filter query was double-encoded (%3d / %26)")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestRendererUnknownPage reports an error for a page that does not exist.
|
|
func TestRendererUnknownPage(t *testing.T) {
|
|
r := MustNewRenderer()
|
|
if err := r.Render(&bytes.Buffer{}, "nope", PageData{}); err == nil {
|
|
t.Fatal("expected an error rendering an unknown page")
|
|
}
|
|
}
|
|
|
|
// TestAssets confirms the stylesheet is embedded and reachable under the assets
|
|
// root.
|
|
func TestAssets(t *testing.T) {
|
|
fsys, err := Assets()
|
|
if err != nil {
|
|
t.Fatalf("assets: %v", err)
|
|
}
|
|
if _, err := fs.Stat(fsys, "console.css"); err != nil {
|
|
t.Errorf("console.css not embedded: %v", err)
|
|
}
|
|
}
|