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/.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") } }