diff options
| author | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-18 05:31:09 -0400 |
| commit | 153acb7f644d755c45533b7c5311e3ffa47e8973 (patch) | |
| tree | 7645e6f7b65bc3830b078e3b9336d2e6b199d584 /internal/game/hardening_purge_session_reset_test.go | |
| parent | 87d08f0151e643951769e323995f1c5ed89ee200 (diff) | |
| download | thehouseoficarus-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.go | 50 |
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) + } +} |
