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_security_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_security_test.go')
| -rw-r--r-- | internal/game/hardening_security_test.go | 100 |
1 files changed, 100 insertions, 0 deletions
diff --git a/internal/game/hardening_security_test.go b/internal/game/hardening_security_test.go new file mode 100644 index 0000000..edf3c29 --- /dev/null +++ b/internal/game/hardening_security_test.go @@ -0,0 +1,100 @@ +package game + +// Regression tests for the security-hardening stage: +// - swapid swaps the two room files through a journal transaction and leaves +// no transient temp file behind +// - the admin privilege check re-reads the on-disk flag instead of trusting +// the cached sess.Account.Admin copy + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestSwapIDSwapsRoomFilesTransactionally(t *testing.T) { + h := newHarness(t) + h.seedAccount("admin") + h.seedCharacter("admin", "admin") + acc, err := h.g.AccountStore.LoadAccount("admin") + if err != nil { + t.Fatal(err) + } + acc.Admin = true + if err := h.g.AccountStore.SaveAccount(acc); err != nil { + t.Fatal(err) + } + sess, _ := h.connect("admin") + + h.g.RunOnOwner(func() { h.g.executeSwapID(sess, []string{"1", "2"}, "swapid 1 2") }) + + r1, err := os.ReadFile(filepath.Join(h.dir, "rooms", "1.yaml")) + if err != nil { + t.Fatal(err) + } + r2, err := os.ReadFile(filepath.Join(h.dir, "rooms", "2.yaml")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(r1), "name: North") { + t.Fatalf("room 1 not swapped:\n%s", r1) + } + if !strings.Contains(string(r2), "name: Central") { + t.Fatalf("room 2 not swapped:\n%s", r2) + } + + // The old implementation renamed through rooms/<id>.yaml.swapid-tmp; the + // transaction-based swap must not leave one behind. + entries, err := os.ReadDir(filepath.Join(h.dir, "rooms")) + if err != nil { + t.Fatal(err) + } + for _, e := range entries { + if strings.Contains(e.Name(), "swapid-tmp") { + t.Fatalf("leftover swap temp file: %s", e.Name()) + } + } + + // A committed transaction removes its journal. + if jentries, err := os.ReadDir(filepath.Join(h.dir, ".journal")); err == nil { + for _, e := range jentries { + t.Fatalf("leftover journal after successful swap: %s", e.Name()) + } + } +} + +func TestCheckAdminReReadsDiskFlag(t *testing.T) { + h := newHarness(t) + h.seedAccount("alice") + h.seedCharacter("alice", "alice") + acc, err := h.g.AccountStore.LoadAccount("alice") + if err != nil { + t.Fatal(err) + } + acc.Admin = true + if err := h.g.AccountStore.SaveAccount(acc); err != nil { + t.Fatal(err) + } + sess, _ := h.connect("alice") + + var got bool + h.g.RunOnOwner(func() { got = h.g.checkAdmin(sess) }) + if !got { + t.Fatal("admin session not recognized") + } + + // Revoke admin on disk; the cached session must no longer be privileged. + revoked, err := h.g.AccountStore.LoadAccount("alice") + if err != nil { + t.Fatal(err) + } + revoked.Admin = false + if err := h.g.AccountStore.SaveAccount(revoked); err != nil { + t.Fatal(err) + } + h.g.RunOnOwner(func() { got = h.g.checkAdmin(sess) }) + if got { + t.Fatal("stale cached admin flag was trusted after disk revocation") + } +} |
