@@ -2,12 +2,15 @@ import 'package:checks/checks.dart';
2
2
import 'package:flutter_test/flutter_test.dart' ;
3
3
import 'package:zulip/api/model/model.dart' ;
4
4
import 'package:zulip/model/recent_senders.dart' ;
5
+ import 'package:zulip/model/store.dart' ;
5
6
import '../example_data.dart' as eg;
7
+ import 'test_store.dart' ;
6
8
7
9
/// [messages] should be sorted by [id] ascending.
8
10
void checkMatchesMessages (RecentSenders model, List <Message > messages) {
9
11
final Map <int , Map <int , Set <int >>> messagesByUserInStream = {};
10
12
final Map <int , Map <TopicName , Map <int , Set <int >>>> messagesByUserInTopic = {};
13
+ messages.sort ((a, b) => a.id - b.id);
11
14
for (final message in messages) {
12
15
if (message is ! StreamMessage ) {
13
16
throw UnsupportedError ('Message of type ${message .runtimeType } is not expected.' );
@@ -142,6 +145,169 @@ void main() {
142
145
});
143
146
});
144
147
148
+ group ('RecentSenders.handleUpdateMessageUpdate' , () {
149
+ late PerAccountStore store;
150
+ late RecentSenders model;
151
+
152
+ final origChannel = eg.stream (); final newChannel = eg.stream ();
153
+ final origTopic = 'origTopic' ; final newTopic = 'newTopic' ;
154
+ final userX = eg.user (); final userY = eg.user ();
155
+
156
+ Future <void > prepare (List <Message > messages) async {
157
+ store = eg.store ();
158
+ await store.addMessages (messages);
159
+ await store.addStreams ([origChannel, newChannel]);
160
+ await store.addUsers ([userX, userY]);
161
+ model = store.recentSenders;
162
+ }
163
+
164
+ List <StreamMessage > copyMessagesWith (Iterable <StreamMessage > messages, {
165
+ ZulipStream ? newChannel,
166
+ String ? newTopic,
167
+ }) {
168
+ assert (newChannel != null || newTopic != null );
169
+ return messages.map ((message) => StreamMessage .fromJson (
170
+ message.toJson ()
171
+ ..['stream_id' ] = newChannel? .streamId ?? message.streamId
172
+ // See [StreamMessage.displayRecipient] for why this is needed.
173
+ ..['display_recipient' ] = newChannel? .name ?? message.displayRecipient!
174
+
175
+ ..['subject' ] = newTopic ?? message.topic
176
+ )).toList ();
177
+ }
178
+
179
+ test ('move a conversation entirely, with additional unknown messages' , () async {
180
+ final messages = List .generate (10 , (i) => eg.streamMessage (
181
+ stream: origChannel, topic: origTopic, sender: userX));
182
+ await prepare (messages);
183
+ final unknownMessages = List .generate (10 , (i) => eg.streamMessage (
184
+ stream: origChannel, topic: origTopic, sender: userX));
185
+ checkMatchesMessages (model, messages);
186
+
187
+ final streamSenderIdsBefore = model.streamSenders
188
+ [origChannel.streamId]! [userX.userId]! .ids;
189
+ final topicSenderIdsBefore = model.topicSenders
190
+ [origChannel.streamId]! [TopicName (origTopic)]! [userX.userId]! .ids;
191
+
192
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
193
+ origMessages: messages + unknownMessages,
194
+ newStreamId: newChannel.streamId));
195
+ checkMatchesMessages (model, copyMessagesWith (
196
+ messages, newChannel: newChannel));
197
+
198
+ // Check we avoided creating a new list for the moved message IDs.
199
+ final streamSenderIdsAfter = model.streamSenders
200
+ [newChannel.streamId]! [userX.userId]! .ids;
201
+ final topicSenderIdsAfter = model.topicSenders
202
+ [newChannel.streamId]! [TopicName (origTopic)]! [userX.userId]! .ids;
203
+ check (streamSenderIdsBefore).identicalTo (streamSenderIdsAfter);
204
+ check (topicSenderIdsBefore).identicalTo (topicSenderIdsAfter);
205
+ });
206
+
207
+ test ('move a conversation exactly' , () async {
208
+ final messages = List .generate (10 , (i) => eg.streamMessage (
209
+ stream: origChannel, topic: origTopic, sender: userX));
210
+ await prepare (messages);
211
+
212
+ final streamSenderIdsBefore = model.streamSenders
213
+ [origChannel.streamId]! [userX.userId]! .ids;
214
+ final topicSenderIdsBefore = model.topicSenders
215
+ [origChannel.streamId]! [TopicName (origTopic)]! [userX.userId]! .ids;
216
+
217
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
218
+ origMessages: messages,
219
+ newStreamId: newChannel.streamId,
220
+ newTopicStr: newTopic));
221
+ checkMatchesMessages (model, copyMessagesWith (
222
+ messages, newChannel: newChannel, newTopic: newTopic));
223
+
224
+ // Check we avoided creating a new list for the moved message IDs.
225
+ final streamSenderIdsAfter = model.streamSenders
226
+ [newChannel.streamId]! [userX.userId]! .ids;
227
+ final topicSenderIdsAfter = model.topicSenders
228
+ [newChannel.streamId]! [TopicName (newTopic)]! [userX.userId]! .ids;
229
+ check (streamSenderIdsBefore).identicalTo (streamSenderIdsAfter);
230
+ check (topicSenderIdsBefore).identicalTo (topicSenderIdsAfter);
231
+ });
232
+
233
+ test ('move a conversation partially to a different channel' , () async {
234
+ final messages = List .generate (10 , (i) => eg.streamMessage (
235
+ stream: origChannel, topic: origTopic));
236
+ final movedMessages = messages.take (5 ).toList ();
237
+ final otherMessages = messages.skip (5 );
238
+ await prepare (messages);
239
+
240
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
241
+ origMessages: movedMessages,
242
+ newStreamId: newChannel.streamId));
243
+ checkMatchesMessages (model, [
244
+ ...copyMessagesWith (movedMessages, newChannel: newChannel),
245
+ ...otherMessages,
246
+ ]);
247
+ });
248
+
249
+ test ('move a conversation partially to a different topic, within the same channel' , () async {
250
+ final messages = List .generate (10 , (i) => eg.streamMessage (
251
+ stream: origChannel, topic: origTopic, sender: userX));
252
+ final movedMessages = messages.take (5 ).toList ();
253
+ final otherMessages = messages.skip (5 );
254
+ await prepare (messages);
255
+
256
+ final streamSenderIdsBefore = model.streamSenders
257
+ [origChannel.streamId]! [userX.userId]! .ids;
258
+
259
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
260
+ origMessages: movedMessages,
261
+ newTopicStr: newTopic));
262
+ checkMatchesMessages (model, [
263
+ ...copyMessagesWith (movedMessages, newTopic: newTopic),
264
+ ...otherMessages,
265
+ ]);
266
+
267
+ // Check that we did not touch stream message IDs tracker
268
+ // when there wasn't a stream move.
269
+ final streamSenderIdsAfter = model.streamSenders
270
+ [origChannel.streamId]! [userX.userId]! .ids;
271
+ check (streamSenderIdsBefore).identicalTo (streamSenderIdsAfter);
272
+ });
273
+
274
+ test ('move a conversation with multiple senders' , () async {
275
+ final messages = [
276
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userX),
277
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userX),
278
+ eg.streamMessage (stream: origChannel, topic: origTopic, sender: userY),
279
+ ];
280
+ await prepare (messages);
281
+
282
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
283
+ origMessages: messages,
284
+ newStreamId: newChannel.streamId));
285
+ checkMatchesMessages (model, copyMessagesWith (
286
+ messages, newChannel: newChannel));
287
+ });
288
+
289
+ test ('move a converstion, but message IDs from the event are not sorted in ascending order' , () async {
290
+ final messages = List .generate (10 , (i) => eg.streamMessage (
291
+ id: 100 - i, stream: origChannel, topic: origTopic));
292
+ await prepare (messages);
293
+
294
+ await store.handleEvent (eg.updateMessageEventMoveFrom (
295
+ origMessages: messages,
296
+ newStreamId: newChannel.streamId));
297
+ checkMatchesMessages (model,
298
+ copyMessagesWith (messages, newChannel: newChannel));
299
+ });
300
+
301
+ test ('message edit update without move' , () async {
302
+ final messages = List .generate (10 , (i) => eg.streamMessage (
303
+ stream: origChannel, topic: origTopic));
304
+ await prepare (messages);
305
+
306
+ await store.handleEvent (eg.updateMessageEditEvent (messages[0 ]));
307
+ checkMatchesMessages (model, messages);
308
+ });
309
+ });
310
+
145
311
test ('RecentSenders.handleDeleteMessageEvent' , () {
146
312
final model = RecentSenders ();
147
313
final stream = eg.stream ();
0 commit comments