diff options
| author | historia <[not public]> | 2026-09-16 19:57:45 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-09-16 19:57:45 -0400 |
| commit | 6ef40a5739f1723ec26cc32dfc48fe764f776dca (patch) | |
| tree | e215a4dc96ea639bcea07bb589aae3e31c3609bb /internal/game/login_offowner_regression_test.go | |
| parent | 1dad15698845d370e998f7cc5feb8aece3fab58f (diff) | |
| download | thehouseoficarus-6ef40a5739f1723ec26cc32dfc48fe764f776dca.tar.gz | |
fix: durable payouts survive journal failure, cap bank withdrawals, hash passwords off the owner loop.
Diffstat (limited to 'internal/game/login_offowner_regression_test.go')
| -rw-r--r-- | internal/game/login_offowner_regression_test.go | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/internal/game/login_offowner_regression_test.go b/internal/game/login_offowner_regression_test.go new file mode 100644 index 0000000..327de7e --- /dev/null +++ b/internal/game/login_offowner_regression_test.go @@ -0,0 +1,122 @@ +package game + +// Login regressions for the off-owner password path: the production entry +// point verifies passwords and hashes new-account passwords on the +// connection goroutine (never the owner), then completes the state +// transition on the owner. The observable login behavior is unchanged. + +import ( + "crypto/sha256" + "encoding/base64" + "strings" + "testing" + "time" + + "thehouseoficarus/internal/net" + "thehouseoficarus/internal/player" +) + +// waitForOutput polls the queued session writer until substr appears or the +// deadline passes. +func waitForOutput(t *testing.T, conn *chanConn, substr string) { + t.Helper() + deadline := time.Now().Add(3 * time.Second) + for !conn.Contains(substr) && time.Now().Before(deadline) { + time.Sleep(time.Millisecond) + } + if !conn.Contains(substr) { + t.Fatalf("output %q never arrived; got:\n%s", substr, conn.Bytes()) + } +} + +// TestLoginThroughServerEntryUsesOffOwnerPathAndUpgradesLegacyHash drives +// the real server entry point (HandleSessionOnOwner) through: legacy-hash +// account creation, a WRONG password attempt, and a correct login that +// rehashes the legacy SHA-256 hash to Argon2id durably. +func TestLoginThroughServerEntryUsesOffOwnerPathAndUpgradesLegacyHash(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) + } + // Downgrade to a legacy salted SHA-256 hash so the login exercises the + // transparent rehash. + salt := []byte("0123456789abcdef") + sum := sha256.Sum256(append(append([]byte{}, salt...), []byte("pw")...)) + acc.PasswordHash = "sha256:" + base64.RawStdEncoding.EncodeToString(salt) + ":" + strings.ToLower(sha256HashHex(sum)) + if err := h.g.AccountStore.SaveAccount(acc); err != nil { + t.Fatal(err) + } + + conn := newChanConn() + sess := &net.Session{Conn: conn, State: net.StateAccountName} + sess.StartWriter() + lines := make(chan string, 8) + go h.input(sess, lines) + + lines <- "alice" // existing account → password prompt (echo off) + lines <- "wrongpw" // rejected, echo restored, back to account name + waitForOutput(t, conn, "Wrong password.") + lines <- "alice" // account name again + lines <- "pw" // correct password through the off-owner path + waitForOutput(t, conn, "Welcome back to Vesta, alice.") + + if sess.Account == nil || sess.Account.Name != "alice" || len(sess.Account.Characters) != 1 { + t.Fatalf("login did not complete: %+v", sess.Account) + } + upgraded, err := h.g.AccountStore.LoadAccount("alice") + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(upgraded.PasswordHash, "argon2id:") { + t.Errorf("legacy hash not upgraded on login: %s", upgraded.PasswordHash[:12]) + } + if !player.CheckPassword("pw", upgraded.PasswordHash) { + t.Error("upgraded hash no longer verifies the password") + } +} + +// TestNewAccountThroughServerEntryHashesOffOwner drives account creation +// through the real server entry point. +func TestNewAccountThroughServerEntryHashesOffOwner(t *testing.T) { + h := newHarness(t) + conn := newChanConn() + sess := &net.Session{Conn: conn, State: net.StateAccountName} + sess.StartWriter() + lines := make(chan string, 8) + go h.input(sess, lines) + + lines <- "newbie" // unknown name → new-account password prompt + lines <- "hunter2pass" + lines <- "hunter2pw" // mismatch → restart account entry (Account reset) + waitForOutput(t, conn, "Passwords do not match.") + lines <- "newbie" // account name again + lines <- "hunter2pw" + lines <- "hunter2pw" + lines <- "n" // color prompt + waitForOutput(t, conn, "Welcome back to Vesta, newbie.") + + created, err := h.g.AccountStore.LoadAccount("newbie") + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(created.PasswordHash, "argon2id:") { + t.Errorf("created account did not hash with Argon2id: %s", created.PasswordHash[:12]) + } + if !player.CheckPassword("hunter2pw", created.PasswordHash) { + t.Error("created hash does not verify the password") + } +} + +// sha256HashHex formats like hex.EncodeToString for the legacy hash check. +func sha256HashHex(sum [32]byte) string { + const hexdigits = "0123456789abcdef" + out := make([]byte, 64) + for i, b := range sum { + out[i*2] = hexdigits[b>>4] + out[i*2+1] = hexdigits[b&0x0f] + } + return string(out) +} |
