From 153acb7f644d755c45533b7c5311e3ffa47e8973 Mon Sep 17 00:00:00 2001 From: historia <[not public]> Date: Fri, 18 Sep 2026 05:31:09 -0400 Subject: fix: harden engine across casino, economy, concurrency, security, and content --- internal/game/correct_online_name_case_test.go | 85 ++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 internal/game/correct_online_name_case_test.go (limited to 'internal/game/correct_online_name_case_test.go') diff --git a/internal/game/correct_online_name_case_test.go b/internal/game/correct_online_name_case_test.go new file mode 100644 index 0000000..74c344c --- /dev/null +++ b/internal/game/correct_online_name_case_test.go @@ -0,0 +1,85 @@ +package game + +// Regression: character/account liveness must be matched case-insensitively, +// like the game's own membership check (isMemberOf). The admin raw file editor +// passes a file NAME ("players/characters/ALICE.yaml"), and an account file's +// character list is hand-editable, so an exact-match lookup both let an online +// character's file be overwritten and let an online character's account be +// purged. + +import ( + "strings" + "testing" + + "thehouseoficarus/internal/net" +) + +func TestAdminPlayerFileOnlineMatchesNamesCaseInsensitively(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + h.seedAccount("carol") + h.seedCharacter("carol", "carol") + + // The account file spells its character list differently from the live + // session's character name. + acc, err := h.g.AccountStore.LoadAccount("alice") + if err != nil { + t.Fatalf("load account: %v", err) + } + acc.Characters = []string{"ALICE"} + if err := h.g.AccountStore.SaveAccount(acc); err != nil { + t.Fatalf("save account: %v", err) + } + + h.connect("alice") + + cases := []struct { + name string + path string + want bool + }{ + {"online character, exact case", "players/characters/alice.yaml", true}, + {"online character, upper case", "players/characters/ALICE.yaml", true}, + {"online character, mixed case", "players/characters/Alice.yaml", true}, + {"account of an online character, list case differs", "players/accounts/alice.yaml", true}, + {"offline character, upper case", "players/characters/CAROL.yaml", false}, + {"offline account", "players/accounts/carol.yaml", false}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if got := h.g.AdminPlayerFileOnline(tc.path); got != tc.want { + t.Fatalf("AdminPlayerFileOnline(%q) = %v, want %v", tc.path, got, tc.want) + } + }) + } +} + +func TestPurgeRefusesCharacterListedInAnotherCase(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + + acc, err := h.g.AccountStore.LoadAccount("alice") + if err != nil { + t.Fatalf("load account: %v", err) + } + acc.Characters = []string{"ALICE"} + if err := h.g.AccountStore.SaveAccount(acc); err != nil { + t.Fatalf("save account: %v", err) + } + + h.connect("alice") + conn := newChanConn() + menu := &net.Session{Conn: conn, Account: acc} + menu.SetState(net.StateMenu) + + h.g.RunOnOwner(func() { h.g.handlePurgeAccount(menu, "PURGE alice") }) + + if out := string(conn.Bytes()); !strings.Contains(out, "is logged in") { + t.Fatalf("purge was not refused for a live character: %q", out) + } + if _, err := h.g.AccountStore.LoadAccount("alice"); err != nil { + t.Fatalf("account was purged while its character was online: %v", err) + } +} -- cgit v1.2.3