aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_purge_session_reset_test.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 05:31:09 -0400
committerhistoria <[not public]>2026-09-18 05:31:09 -0400
commit153acb7f644d755c45533b7c5311e3ffa47e8973 (patch)
tree7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_purge_session_reset_test.go
parent87d08f0151e643951769e323995f1c5ed89ee200 (diff)
downloadthehouseoficarus-153acb7f644d755c45533b7c5311e3ffa47e8973.tar.gz
fix: harden engine across casino, economy, concurrency, security, and content
Diffstat (limited to 'internal/game/hardening_purge_session_reset_test.go')
-rw-r--r--internal/game/hardening_purge_session_reset_test.go50
1 files changed, 50 insertions, 0 deletions
diff --git a/internal/game/hardening_purge_session_reset_test.go b/internal/game/hardening_purge_session_reset_test.go
new file mode 100644
index 0000000..c3462a6
--- /dev/null
+++ b/internal/game/hardening_purge_session_reset_test.go
@@ -0,0 +1,50 @@
+package game
+
+// Regression (P0): a purged account must leave every one of its sessions in a
+// safe protocol state. A queued-mode Close only drops the transport after the
+// writer drains its outbox, so input already buffered in the connection can
+// still be dispatched to the owner after the purge; a session left in the
+// character menu with a nil account then dereferences sess.Account and kills
+// the owner goroutine (and with it every session and all unsaved state).
+
+import (
+ "testing"
+
+ "thehouseoficarus/internal/net"
+)
+
+func TestPurgeResetsEveryAccountSession(t *testing.T) {
+ h := newHarness(t)
+ h.seedAccount("alice")
+ h.seedCharacter("alice", "alice")
+
+ other := h.connectMenu("alice")
+ purger := h.connectMenu("alice")
+
+ h.g.RunOnOwner(func() { h.g.handlePurgeAccount(purger, "PURGE alice") })
+
+ for name, sess := range map[string]*net.Session{"other": other, "purger": purger} {
+ if got := sess.State(); got != net.StateAccountName {
+ t.Errorf("%s session state = %v, want StateAccountName", name, got)
+ }
+ if sess.Account != nil {
+ t.Errorf("%s session retained the purged account: %+v", name, sess.Account)
+ }
+ }
+
+ // Input already buffered in either transport is still routed to the owner:
+ // it must land on the account-name prompt instead of faulting.
+ before := h.g.owner.panics.Load()
+ h.g.RunOnOwner(func() {
+ h.g.handleMenu(other, "c")
+ h.g.handleMenu(other, "l")
+ h.g.handlePurgeAccount(purger, "PURGE alice")
+ h.g.handleNewCharName(purger, "bob")
+ })
+ if got := h.g.owner.panics.Load(); got != before {
+ t.Fatalf("post-purge input panicked the owner (panic count %d -> %d)", before, got)
+ }
+ if got := other.State(); got != net.StateAccountName {
+ t.Errorf("state = %v after post-purge input, want StateAccountName", got)
+ }
+}