Skip to content

Commit 5959db3

Browse files
committed
refactor(thread-unread): project room type directly; add roomclient + multi-site tests (CodeRabbit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BocBvJn2N9YYMmciA5EWJX
1 parent 6aac04d commit 5959db3

3 files changed

Lines changed: 69 additions & 4 deletions

File tree

room-service/store_mongo.go

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1777,12 +1777,24 @@ func (s *MongoStore) GetThreadRoomInfos(ctx context.Context, threadRoomIDs []str
17771777

17781778
typeByRoom := make(map[string]model.RoomType, len(roomIDs))
17791779
if len(roomIDs) > 0 {
1780-
rooms, err := s.ListRoomsByIDs(ctx, roomIDs)
1780+
roomCursor, err := s.rooms.Find(ctx,
1781+
bson.M{"_id": bson.M{"$in": roomIDs}},
1782+
options.Find().SetProjection(bson.M{"_id": 1, "type": 1}),
1783+
)
17811784
if err != nil {
1782-
return nil, fmt.Errorf("list rooms by ids: %w", err)
1785+
return nil, fmt.Errorf("list room types: %w", err)
17831786
}
1784-
for i := range rooms {
1785-
typeByRoom[rooms[i].ID] = rooms[i].Type
1787+
defer roomCursor.Close(ctx)
1788+
1789+
var roomTypes []struct {
1790+
ID string `bson:"_id"`
1791+
Type model.RoomType `bson:"type"`
1792+
}
1793+
if err := roomCursor.All(ctx, &roomTypes); err != nil {
1794+
return nil, fmt.Errorf("list room types: %w", err)
1795+
}
1796+
for i := range roomTypes {
1797+
typeByRoom[roomTypes[i].ID] = roomTypes[i].Type
17861798
}
17871799
}
17881800

user-service/roomclient/client_integration_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,36 @@ func TestGetThreadRoomInfoBatch_Integration(t *testing.T) {
155155
require.True(t, errors.As(err, &e))
156156
assert.Equal(t, errcode.CodeBadRequest, e.Code)
157157
})
158+
159+
t.Run("no responder — returns error wrapping thread-room-info rpc", func(t *testing.T) {
160+
nc := dial(t)
161+
// Intentionally no subscriber: nc.Request must fail with "no responders".
162+
163+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
164+
defer cancel()
165+
166+
_, err := New(nc, "site-a").GetThreadRoomInfoBatch(ctx, "site-a", []string{"tr1"})
167+
require.Error(t, err)
168+
assert.Contains(t, err.Error(), "thread-room-info rpc")
169+
})
170+
171+
t.Run("cross-site siteID routing — uses siteID param not c.siteID", func(t *testing.T) {
172+
nc := dial(t)
173+
174+
// Responder on "site-b" subject proves the method routes on siteID param, not c.siteID.
175+
sub, err := nc.Subscribe(subject.ThreadRoomInfoBatch("site-b"), func(m otelnats.Msg) {
176+
out, _ := json.Marshal(model.ThreadRoomInfoBatchResponse{
177+
Threads: []model.ThreadRoomInfo{{ThreadRoomID: "tr2", Found: true, LastMsgAt: 99, RoomType: model.RoomTypeChannel}},
178+
})
179+
_ = m.Msg.Respond(out)
180+
})
181+
require.NoError(t, err)
182+
t.Cleanup(func() { _ = sub.Unsubscribe() })
183+
184+
got, err := New(nc, "site-a").GetThreadRoomInfoBatch(context.Background(), "site-b", []string{"tr2"})
185+
require.NoError(t, err)
186+
require.Len(t, got, 1)
187+
})
158188
}
159189

160190
func TestCreateDMRoom_Integration(t *testing.T) {

user-service/service/threadunread_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,29 @@ func TestGetThreadUnread_SiteFailureDegrades(t *testing.T) {
6565
assert.Equal(t, []string{"site-b"}, resp.UnavailableSites)
6666
}
6767

68+
func TestGetThreadUnread_MultiSiteMixedSuccessAndFailure(t *testing.T) {
69+
ctrl := gomock.NewController(t)
70+
ts := mocks.NewMockThreadSubscriptionRepository(ctrl)
71+
rc := mocks.NewMockRoomClient(ctrl)
72+
73+
ts.EXPECT().ListByAccount(gomock.Any(), "alice").Return([]model.ThreadSubRef{
74+
{ThreadRoomID: "trA", SiteID: "site-a", LastSeenAt: ptrTime(100)},
75+
{ThreadRoomID: "trC", SiteID: "site-c", LastSeenAt: nil},
76+
}, nil)
77+
rc.EXPECT().GetThreadRoomInfoBatch(gomock.Any(), "site-a", []string{"trA"}).
78+
Return([]model.ThreadRoomInfo{{ThreadRoomID: "trA", Found: true, LastMsgAt: 200, RoomType: model.RoomTypeChannel}}, nil)
79+
rc.EXPECT().GetThreadRoomInfoBatch(gomock.Any(), "site-c", []string{"trC"}).
80+
Return(nil, errors.New("site-c unreachable"))
81+
82+
svc := newThreadUnreadService(t, ts, rc)
83+
resp, err := svc.GetThreadUnread(ctx("alice", "site-a"), model.ThreadUnreadRequest{})
84+
require.NoError(t, err)
85+
assert.True(t, resp.Unread)
86+
require.NotNil(t, resp.LastMessageAt)
87+
assert.Equal(t, int64(200), *resp.LastMessageAt)
88+
assert.Equal(t, []string{"site-c"}, resp.UnavailableSites)
89+
}
90+
6891
func TestGetThreadUnread_NoSubs(t *testing.T) {
6992
ctrl := gomock.NewController(t)
7093
ts := mocks.NewMockThreadSubscriptionRepository(ctrl)

0 commit comments

Comments
 (0)