package game // The off-owner login paths must never read owner-owned session fields: // the owner goroutine replaces Session.Account at any moment (account // republish after a rename or purge races any concurrent login), so the // connection goroutine verifies passwords against the race-free authName // snapshot instead. import ( "testing" "thehouseoficarus/internal/net" ) // TestOffOwnerLoginDoesNotRaceAccountPublish drives the real server entry // point through a login while a republish loop keeps replacing the // session's cached account on the owner — the exact race a second // authenticated session (or an admin-triggered account update) creates // while the first session is typing its password. Run with -race. func TestOffOwnerLoginDoesNotRaceAccountPublish(t *testing.T) { h := newHarness(t) h.seedAccount("alice") h.seedCharacter("alice", "alice") sess := &net.Session{Conn: newChanConn(), State: net.StateAccountName} sess.StartWriter() defer sess.Close() h.hub.Add(sess) done := make(chan struct{}) go func() { defer close(done) for i := 0; i < 200; i++ { h.g.RunOnOwner(func() { h.g.publishAccountUpdate("alice", "alice") }) } }() h.g.HandleSessionOnOwner(sess, "alice") // owner: offers the password prompt h.g.HandleSessionOnOwner(sess, "pw") // off-owner: verifies the password <-done if sess.AuthName() != "alice" { t.Fatalf("password prompt lost its account name: %q", sess.AuthName()) } if sess.Account == nil || sess.Account.Name != "alice" { t.Fatalf("login did not complete: %+v", sess.Account) } } // TestPasswordPromptRejectsEmptyAndOversizedInput keeps the documented // fast-path behavior of the off-owner password handler after the // authName rework: an empty line re-prompts (echo still off) and an // oversized line is rejected without running Argon2id on it. func TestPasswordPromptRejectsEmptyAndOversizedInput(t *testing.T) { h := newHarness(t) h.seedAccount("alice") sess := &net.Session{Conn: newChanConn(), State: net.StateAccountName} sess.StartWriter() defer sess.Close() h.hub.Add(sess) h.g.HandleSessionOnOwner(sess, "alice") if sess.State != net.StatePassword { t.Fatalf("state after account name = %v, want StatePassword", sess.State) } h.g.HandleSessionOnOwner(sess, "") if sess.State != net.StatePassword { t.Fatalf("empty password line must stay in StatePassword, got %v", sess.State) } h.g.HandleSessionOnOwner(sess, string(make([]byte, 200))) if sess.State != net.StateAccountName { t.Fatalf("oversized password must return to StateAccountName, got %v", sess.State) } }