package sync import ( "testing" "github.com/google/uuid" ) // TestBroadcastIsPerUser pins the 2026-11-02 chokepoint: sync events are // private to the acting user. The pre-fix helpers fanned out through the // untargeted Broadcast, delivering one user's book progress — and // annotation payloads, which can carry note text — to every connected // client on the instance. The untargeted method no longer exists (the // compiler enforces an audience on every emitter); this test enforces the // routing at runtime, in the spirit of TestRlsInvariant: no event may // cross a user boundary. func TestBroadcastIsPerUser(t *testing.T) { cm := NewConnectionManager() mkConn := func(user string) *DeviceConnection { conn := &DeviceConnection{ ID: uuid.NewString(), UserID: user, DeviceType: "test", DeviceName: user + "-device", Send: make(chan BroadcastMessage, 8), } cm.AddConnection(conn) return conn } // Alice has two devices connected; Bob has one. a1 := mkConn("alice") a2 := mkConn("alice") b1 := mkConn("bob") source := SourceDevice{ID: "dev-1", Name: "alice-kobo", Type: "kobo"} cm.BroadcastProgressUpdate("alice", uuid.New(), 42.5, source) cm.BroadcastAnnotationUpdate("alice", uuid.New(), "highlight", map[string]any{"highlight_id": "h1"}, source) cm.BroadcastConflictNotification("alice", uuid.New(), "detection", "conflict-1") drain := func(conn *DeviceConnection) []BroadcastMessage { var got []BroadcastMessage for { select { case msg := <-conn.Send: got = append(got, msg) default: return got } } } // Alice's devices each received all three of her events... for _, conn := range []*DeviceConnection{a1, a2} { got := drain(conn) if len(got) != 3 { t.Fatalf("%s got %d messages, want 3", conn.DeviceName, len(got)) } for _, msg := range got { // Conflict events are server-detected and carry no source; // progress and annotation events identify the originating // device so clients can ignore their own echo. if msg.Type != "conflict" && (msg.Source == nil || msg.Source.Name != "alice-kobo") { t.Errorf("%s: unexpected event source on %q: %+v", conn.DeviceName, msg.Type, msg.Source) } } } // ...and Bob received nothing user-derived from Alice's sync // activity. Scan events are the one sanctioned instance-wide fan-out // (BroadcastToAll — job ids and counts, no user data); everything // carrying a user's books or annotations stops at that user's edge. if leaked := drain(b1); len(leaked) != 0 { t.Fatalf("bob's client received %d message(s) from alice's sync activity — cross-user leak", len(leaked)) } } // TestBroadcastToAllReachesEveryone pins the sanctioned exception: scan // activity is instance-wide (the sitewide header indicator runs on it), // while everything user-derived stays per-user (TestBroadcastIsPerUser). func TestBroadcastToAllReachesEveryone(t *testing.T) { cm := NewConnectionManager() var conns []*DeviceConnection for _, user := range []string{"alice", "bob", "carol"} { conn := &DeviceConnection{ ID: uuid.NewString(), UserID: user, DeviceType: "test", DeviceName: user + "-device", Send: make(chan BroadcastMessage, 4), } cm.AddConnection(conn) conns = append(conns, conn) } cm.BroadcastToAll(BroadcastMessage{Type: MessageTypeScanProgress}) for _, conn := range conns { select { case msg := <-conn.Send: if msg.Type != MessageTypeScanProgress { t.Errorf("%s: got %q, want scan_progress", conn.DeviceName, msg.Type) } default: t.Errorf("%s: received nothing from BroadcastToAll", conn.DeviceName) } } }