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