feat(membership): room discovery — GET /members/{endpoint}/rooms + ListMyRooms
A peer invited to an encrypted room needs to find it: the control plane is
pull-based (no server push of invitations), so add a discovery endpoint that
lists every room an endpoint belongs to, with the room's metadata and the
endpoint's role.
- store.ListRoomsForEndpoint: JOIN members+rooms, ordered by room id, empty
slice (not error) for an endpoint in no rooms.
- membershipd: GET /members/{endpoint}/rooms returns {room_id, subject, epoch,
policy, role}[].
- client.ListMyRooms + RoomRef: a bot polls this to discover and then Join +
Subscribe rooms it was invited to.
Tests: store-level (owner in N rooms, member in one, unknown endpoint → []) and
client-level e2e through the embedded harness (B discovers a room A invited it
to, without prior knowledge of the room id; owner sees role=owner).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,58 @@ func TestMigrationsCreateSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomsForEndpoint(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
|
||||
// Owner of two rooms; a member in only the first.
|
||||
owner, member := "owner-ep", "member-ep"
|
||||
mk := func(id, subj string) RoomInfo {
|
||||
return RoomInfo{RoomID: id, Subject: subj, Encrypt: true, Persist: true, SignMsgs: true, OwnerEndpoint: owner}
|
||||
}
|
||||
if err := s.CreateRoom(mk("room-a", "room.a"), []byte("os"), []byte("ok"), []byte("k")); err != nil {
|
||||
t.Fatalf("CreateRoom a: %v", err)
|
||||
}
|
||||
if err := s.CreateRoom(mk("room-b", "room.b"), []byte("os"), []byte("ok"), []byte("k")); err != nil {
|
||||
t.Fatalf("CreateRoom b: %v", err)
|
||||
}
|
||||
if err := s.AddMember("room-a", Member{Endpoint: member, Role: "member", SignPub: []byte("s"), KexPub: []byte("k")}, 1, []byte("mk")); err != nil {
|
||||
t.Fatalf("AddMember: %v", err)
|
||||
}
|
||||
|
||||
// Owner is in both rooms, as owner, ordered by room id.
|
||||
ownerRooms, err := s.ListRoomsForEndpoint(owner)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoomsForEndpoint owner: %v", err)
|
||||
}
|
||||
if len(ownerRooms) != 2 {
|
||||
t.Fatalf("owner: expected 2 rooms, got %d", len(ownerRooms))
|
||||
}
|
||||
if ownerRooms[0].RoomID != "room-a" || ownerRooms[1].RoomID != "room-b" {
|
||||
t.Fatalf("owner rooms not ordered: %+v", ownerRooms)
|
||||
}
|
||||
if ownerRooms[0].Role != "owner" || !ownerRooms[0].Encrypt || ownerRooms[0].Subject != "room.a" {
|
||||
t.Fatalf("owner room metadata wrong: %+v", ownerRooms[0])
|
||||
}
|
||||
|
||||
// Member is in exactly one room, as member.
|
||||
memberRooms, err := s.ListRoomsForEndpoint(member)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoomsForEndpoint member: %v", err)
|
||||
}
|
||||
if len(memberRooms) != 1 || memberRooms[0].RoomID != "room-a" || memberRooms[0].Role != "member" {
|
||||
t.Fatalf("member rooms wrong: %+v", memberRooms)
|
||||
}
|
||||
|
||||
// An unknown endpoint yields an empty slice, not an error.
|
||||
none, err := s.ListRoomsForEndpoint("nobody")
|
||||
if err != nil {
|
||||
t.Fatalf("ListRoomsForEndpoint nobody: %v", err)
|
||||
}
|
||||
if len(none) != 0 {
|
||||
t.Fatalf("expected no rooms for unknown endpoint, got %+v", none)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoomMemberKeyRoundTrip(t *testing.T) {
|
||||
s := openTestStore(t)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user