aboutsummaryrefslogtreecommitdiff
path: root/internal/game/login_race_regression_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/game/login_race_regression_test.go')
-rw-r--r--internal/game/login_race_regression_test.go77
1 files changed, 77 insertions, 0 deletions
diff --git a/internal/game/login_race_regression_test.go b/internal/game/login_race_regression_test.go
new file mode 100644
index 0000000..6448cf5
--- /dev/null
+++ b/internal/game/login_race_regression_test.go
@@ -0,0 +1,77 @@
+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)
+ }
+}