package relay import ( "errors" "testing" "time" ) // register is a test helper: every session must be Register-ed before it can be // Acquire-d. Sender "s" / receiver "r" unless a test needs specific names. func register(t *testing.T, m *Manager, id, user string) { t.Helper() if err := m.Register(id, user, "s", "r"); err != nil { t.Fatalf("register %s/%s: %v", id, user, err) } } func TestManager_AcquireReturnsSameSession(t *testing.T) { m := NewManager(8, 1024) register(t, m, "s1", "alice") s1, err := m.Acquire("s1", "alice", "s") if err != nil { t.Fatalf("acquire 1: %v", err) } s2, err := m.Acquire("s1", "alice", "r") if err != nil { t.Fatalf("acquire 2: %v", err) } if s1 != s2 { t.Error("expected same session pointer on repeat Acquire") } } // Acquire never mints a session: an id that was never Register-ed is rejected. // This is the core R1 fix — the old Acquire created one on demand, so any // authenticated client could conjure sessions for arbitrary ids and exhaust // every slot. func TestManager_AcquireUnregisteredRejected(t *testing.T) { m := NewManager(8, 1024) if _, err := m.Acquire("ghost", "alice", "s"); !errors.Is(err, ErrNotRegistered) { t.Errorf("expected ErrNotRegistered, got %v", err) } if got := m.Active(); got != 0 { t.Errorf("a rejected Acquire must not create a session; Active=%d", got) } } func TestManager_DifferentUserForbidden(t *testing.T) { m := NewManager(8, 1024) register(t, m, "s1", "alice") if _, err := m.Acquire("s1", "mallory", "s"); !errors.Is(err, ErrForbidden) { t.Errorf("expected ErrForbidden, got %v", err) } } // Only the registered sender/receiver device may join (G2). A third device of // the same account — or a missing device name — is forbidden. func TestManager_NonParticipantForbidden(t *testing.T) { m := NewManager(8, 1024) if err := m.Register("s1", "alice", "laptop", "phone"); err != nil { t.Fatalf("register: %v", err) } if _, err := m.Acquire("s1", "alice", "tablet"); !errors.Is(err, ErrForbidden) { t.Errorf("non-participant device: expected ErrForbidden, got %v", err) } if _, err := m.Acquire("s1", "alice", ""); !errors.Is(err, ErrForbidden) { t.Errorf("empty device: expected ErrForbidden, got %v", err) } if _, err := m.Acquire("s1", "alice", "laptop"); err != nil { t.Errorf("sender device must be allowed: %v", err) } if _, err := m.Acquire("s1", "alice", "phone"); err != nil { t.Errorf("receiver device must be allowed: %v", err) } } // Register, not Acquire, now enforces the global cap. func TestManager_GlobalCapEnforced(t *testing.T) { m := NewManager(2, 1024) if err := m.Register("a", "u1", "s", "r"); err != nil { t.Fatalf("a: %v", err) } if err := m.Register("b", "u2", "s", "r"); err != nil { t.Fatalf("b: %v", err) } if err := m.Register("c", "u3", "s", "r"); !errors.Is(err, ErrTooManySessions) { t.Errorf("expected ErrTooManySessions, got %v", err) } if got := m.Active(); got != 2 { t.Errorf("Active: got %d, want 2", got) } } // A single user can't monopolise every global slot — the per-user cap stops // them well before the global cap, leaving room for other users. func TestManager_PerUserCapEnforced(t *testing.T) { m := NewManager(8, 1024) // per-user default is 3 for i, id := range []string{"a", "b", "c"} { if err := m.Register(id, "greedy", "s", "r"); err != nil { t.Fatalf("register %d: %v", i, err) } } if err := m.Register("d", "greedy", "s", "r"); !errors.Is(err, ErrTooManySessions) { t.Errorf("4th session for one user: expected ErrTooManySessions, got %v", err) } // A different user is unaffected — global slots remain. if err := m.Register("e", "other", "s", "r"); err != nil { t.Errorf("other user should still register: %v", err) } } // Register is idempotent for the same owner (e.g. a retried /fallback) and does // not consume a second slot. func TestManager_RegisterIdempotent(t *testing.T) { m := NewManager(8, 1024) register(t, m, "s1", "alice") register(t, m, "s1", "alice") if got := m.Active(); got != 1 { t.Errorf("duplicate Register must not add a slot; Active=%d", got) } if err := m.Register("s1", "mallory", "s", "r"); !errors.Is(err, ErrForbidden) { t.Errorf("re-register under another user: expected ErrForbidden, got %v", err) } } func TestManager_ReleaseFreesSlot(t *testing.T) { m := NewManager(1, 1024) register(t, m, "a", "u") m.Release("a") if got := m.Active(); got != 0 { t.Errorf("Active after release: got %d, want 0", got) } // A new session can now slot in (both global and per-user counts dropped). if err := m.Register("b", "u", "s", "r"); err != nil { t.Errorf("register after release: %v", err) } } func TestManager_ReleaseAbortsSession(t *testing.T) { m := NewManager(1, 1024) register(t, m, "a", "u") s, err := m.Acquire("a", "u", "s") // allocates the ring if err != nil { t.Fatalf("acquire: %v", err) } m.Release("a") if _, err := s.Write(nil, []byte("x")); !errors.Is(err, ErrClosed) { t.Errorf("write after release: got %v, want ErrClosed", err) } } // The reaper reclaims a session whose last activity is older than the idle TTL, // dropping both the global and per-user counts so the slot is reusable. func TestManager_ReaperReclaimsIdle(t *testing.T) { m := NewManager(8, 1024) register(t, m, "a", "u") // Just-registered: a sweep "now" leaves it alone. if n := m.reapIdle(time.Now().Unix()); n != 0 { t.Errorf("fresh session reclaimed: got %d, want 0", n) } // Sweep far enough in the future that it's past the idle TTL. future := time.Now().Add(DefaultIdleTTL + time.Minute).Unix() if n := m.reapIdle(future); n != 1 { t.Errorf("idle session not reclaimed: got %d, want 1", n) } if got := m.Active(); got != 0 { t.Errorf("Active after reap: got %d, want 0", got) } // The per-user slot was freed too. if err := m.Register("b", "u", "s", "r"); err != nil { t.Errorf("register after reap: %v", err) } } // The stream is single-consumer: a second concurrent reader is refused so it // can't drain bytes out from under the first and corrupt the receiver's copy. func TestSession_SingleReader(t *testing.T) { m := NewManager(8, 1024) register(t, m, "a", "u") s, err := m.Acquire("a", "u", "r") if err != nil { t.Fatalf("acquire: %v", err) } if !s.AcquireReader() { t.Fatal("first AcquireReader should succeed") } if s.AcquireReader() { t.Error("second AcquireReader must fail while the first holds it") } s.ReleaseReader() if !s.AcquireReader() { t.Error("AcquireReader should succeed again after release") } }