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 ( "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()) } } // TestLoginThroughServerEntryUsesOffOwnerPath drives the real server entry // point (HandleSessionOnOwner) through a WRONG password attempt and a correct // login, exercising the off-owner verification path. func TestLoginThroughServerEntryUsesOffOwnerPath(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") conn := newChanConn() sess := &net.Session{Conn: conn} sess.SetState(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) } loaded, err := h.g.AccountStore.LoadAccount("alice") if err != nil { t.Fatal(err) } if !player.CheckPassword("pw", loaded.PasswordHash) { t.Error("stored 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} sess.SetState(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") } }