aboutsummaryrefslogtreecommitdiff
path: root/internal/game/hardening_security_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/hardening_security_test.go')
-rw-r--r--internal/game/hardening_security_test.go100
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")
+ }
+}