More private room bridge test updates
[gitter.git] / modules / matrix-bridge / test / gitter-bridge-test.js
blobd069e09f0261b90af32848fb0378a97d1595294e
1 'use strict';
3 const assert = require('assert');
4 const sinon = require('sinon');
5 const fixtureLoader = require('gitter-web-test-utils/lib/test-fixtures');
6 const chatService = require('gitter-web-chats');
7 const restSerializer = require('../../../server/serializers/rest-serializer');
8 const GitterBridge = require('../lib/gitter-bridge');
9 const GitterUtils = require('../lib/gitter-utils');
10 const store = require('../lib/store');
11 const getMxidForGitterUser = require('../lib/get-mxid-for-gitter-user');
12 const { getCanonicalAliasLocalpartForGitterRoomUri } = require('../lib/matrix-alias-utils');
14 const strategy = new restSerializer.ChatStrategy();
16 describe('gitter-bridge', () => {
17   const overallFixtures = fixtureLoader.setupEach({
18     userBridge1: {},
19     group1: {}
20   });
22   let gitterBridge;
23   let matrixBridge;
24   let gitterUtils;
25   beforeEach(() => {
26     const clientSpies = {
27       redactEvent: sinon.spy(),
28       resolveRoom: sinon.spy(),
29       deleteRoomAlias: sinon.spy(),
30       getDirectoryVisibility: sinon.spy(),
31       setDirectoryVisibility: sinon.spy(),
32       getRoomMembers: sinon.spy(),
33       unstableApis: {
34         getRoomAliases: sinon.spy()
35       }
36     };
38     const intentSpies = {
39       matrixClient: clientSpies,
40       getStateEvent: sinon.spy(),
41       sendStateEvent: sinon.spy(),
42       getEvent: sinon.spy(() => ({
43         event_id: `$${fixtureLoader.generateGithubId()}:localhost`,
44         sender: '@alice:localhost'
45       })),
46       sendMessage: sinon.spy(() => ({
47         event_id: `$${fixtureLoader.generateGithubId()}:localhost`
48       })),
49       createRoom: sinon.spy(() => ({
50         room_id: `!${fixtureLoader.generateGithubId()}:localhost`
51       })),
52       createAlias: sinon.spy(),
53       setRoomAvatar: sinon.spy(),
54       getProfileInfo: sinon.spy(() => ({})),
55       setDisplayName: sinon.spy(),
56       uploadContent: sinon.spy(),
57       setAvatarUrl: sinon.spy(),
58       invite: sinon.spy(),
59       join: sinon.spy(),
60       leave: sinon.spy(),
61       ban: sinon.spy(),
62       unban: sinon.spy()
63     };
65     matrixBridge = {
66       getIntent: sinon.spy((/*userId*/) => intentSpies)
67     };
69     gitterBridge = new GitterBridge(matrixBridge, overallFixtures.userBridge1.username);
71     gitterUtils = new GitterUtils(
72       matrixBridge,
73       overallFixtures.userBridge1.username,
74       overallFixtures.group1.uri
75     );
76   });
78   describe('onDataChange', () => {
79     describe('handleChatMessageCreateEvent', () => {
80       const fixture = fixtureLoader.setupEach({
81         user1: {},
82         userBridge1: {},
83         group1: {},
84         troupe1: {
85           group: 'group1'
86         },
87         troupePrivate1: {
88           group: 'group1',
89           users: ['user1'],
90           securityDescriptor: {
91             members: 'INVITE',
92             admins: 'MANUAL',
93             public: false
94           }
95         },
96         message1: {
97           user: 'user1',
98           troupe: 'troupe1',
99           text: 'my gitter message'
100         },
101         message2: {
102           user: 'user1',
103           troupe: 'troupe1',
104           text: 'my gitter message2'
105         },
106         messageStatus1: {
107           user: 'user1',
108           troupe: 'troupe1',
109           text: '@user1 my gitter status(/me) message',
110           html:
111             '<span data-link-type="mention" data-screen-name="user1" class="mention">@user1</span> my gitter status(/me) message',
112           status: true
113         },
114         messageThreaded1: {
115           user: 'user1',
116           troupe: 'troupe1',
117           text: 'my gitter threaded message1',
118           parent: 'message1'
119         },
120         messageThreaded2: {
121           user: 'user1',
122           troupe: 'troupe1',
123           text: 'my gitter threaded message2',
124           parent: 'message1'
125         },
126         messageFromVirtualUser1: {
127           user: 'userBridge1',
128           virtualUser: {
129             type: 'matrix',
130             externalId: 'test-person:matrix.org',
131             displayName: 'Tessa'
132           },
133           troupe: 'troupe1',
134           text: 'my virtualUser message'
135         },
136         messageFromBridgeBot1: {
137           user: 'userBridge1',
138           troupe: 'troupe1',
139           text: `I'm the badger bridge bot`
140         },
141         messagePrivate1: {
142           user: 'user1',
143           troupe: 'troupePrivate1',
144           text: 'my private gitter message'
145         }
146       });
148       it('new message gets sent off to Matrix', async () => {
149         const serializedMessage = await restSerializer.serializeObject(fixture.message1, strategy);
151         await gitterBridge.onDataChange({
152           type: 'chatMessage',
153           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
154           operation: 'create',
155           model: serializedMessage
156         });
158         // Room is created for something that hasn't been bridged before
159         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 1);
161         // Message is sent to the new room
162         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
163         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
164           body: fixture.message1.text,
165           format: 'org.matrix.custom.html',
166           formatted_body: fixture.message1.html,
167           msgtype: 'm.text'
168         });
169       });
171       it('new status(/me) message gets sent off to Matrix', async () => {
172         const serializedMessage = await restSerializer.serializeObject(
173           fixture.messageStatus1,
174           strategy
175         );
177         await gitterBridge.onDataChange({
178           type: 'chatMessage',
179           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
180           operation: 'create',
181           model: serializedMessage
182         });
184         // Room is created for something that hasn't been bridged before
185         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 1);
187         // Message is sent to the new room
188         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
189         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
190           body: 'my gitter status(/me) message',
191           format: 'org.matrix.custom.html',
192           formatted_body: 'my gitter status(/me) message',
193           msgtype: 'm.emote'
194         });
195       });
197       it('subsequent multiple messages go to the same room', async () => {
198         const serializedMessage1 = await restSerializer.serializeObject(fixture.message1, strategy);
199         const serializedMessage2 = await restSerializer.serializeObject(fixture.message2, strategy);
201         await gitterBridge.onDataChange({
202           type: 'chatMessage',
203           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
204           operation: 'create',
205           model: serializedMessage1
206         });
208         await gitterBridge.onDataChange({
209           type: 'chatMessage',
210           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
211           operation: 'create',
212           model: serializedMessage2
213         });
215         // Room is only created once
216         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 1);
218         // Messages are sent to the new room
219         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 2);
221         const sendMessageCall1 = matrixBridge.getIntent().sendMessage.getCall(0);
222         const sendMessageCall2 = matrixBridge.getIntent().sendMessage.getCall(1);
223         // Make sure the messages were sent to the same room
224         assert.strictEqual(sendMessageCall1.args[0], sendMessageCall2.args[0]);
225       });
227       it('threaded conversation reply gets sent off to Matrix', async () => {
228         const serializedMessage = await restSerializer.serializeObject(
229           fixture.messageThreaded1,
230           strategy
231         );
233         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
234         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
235         const parentMessageEventId = `$${fixtureLoader.generateGithubId()}:localhost`;
236         await store.storeBridgedMessage(fixture.message1, matrixRoomId, parentMessageEventId);
238         await gitterBridge.onDataChange({
239           type: 'chatMessage',
240           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
241           operation: 'create',
242           model: serializedMessage
243         });
245         // Message is sent to the new room
246         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
247         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
248           body: fixture.messageThreaded1.text,
249           format: 'org.matrix.custom.html',
250           formatted_body: fixture.messageThreaded1.html,
251           msgtype: 'm.text',
252           'm.relates_to': {
253             'm.in_reply_to': {
254               event_id: parentMessageEventId
255             }
256           }
257         });
258       });
260       it('threaded conversation replies to last message in thread gets sent off to Matrix', async () => {
261         const serializedMessage = await restSerializer.serializeObject(
262           fixture.messageThreaded2,
263           strategy
264         );
266         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
267         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
268         const parentMessageEventId = `$${fixtureLoader.generateGithubId()}:localhost`;
269         await store.storeBridgedMessage(fixture.message1, matrixRoomId, parentMessageEventId);
270         const threadReplyMessageEventId1 = `$${fixtureLoader.generateGithubId()}:localhost`;
271         await store.storeBridgedMessage(
272           fixture.messageThreaded1,
273           matrixRoomId,
274           threadReplyMessageEventId1
275         );
277         await gitterBridge.onDataChange({
278           type: 'chatMessage',
279           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
280           operation: 'create',
281           model: serializedMessage
282         });
284         // Message is sent to the new room
285         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
286         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
287           body: fixture.messageThreaded2.text,
288           format: 'org.matrix.custom.html',
289           formatted_body: fixture.messageThreaded2.html,
290           msgtype: 'm.text',
291           'm.relates_to': {
292             'm.in_reply_to': {
293               event_id: threadReplyMessageEventId1
294             }
295           }
296         });
297       });
299       // This is a edge case in the transition between no Matrix bridge and Matrix.
300       // I don't think we need to worry too much about what happens. Just want a test to know
301       // something happens.
302       it('threaded conversation reply where the parent does not exist on Matrix still gets sent', async () => {
303         const serializedMessage = await restSerializer.serializeObject(
304           fixture.messageThreaded1,
305           strategy
306         );
308         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
309         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
310         // We purposely do not associate the bridged message. We are testing that the
311         // message is ignored if the parent message event is not in the database.
312         //const parentMessageEventId = `$${fixtureLoader.generateGithubId()}:localhost`;
313         //await store.storeBridgedMessage(fixture.message1, matrixRoomId, parentMessageEventId);
315         await gitterBridge.onDataChange({
316           type: 'chatMessage',
317           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
318           operation: 'create',
319           model: serializedMessage
320         });
322         // Message is sent to the new room
323         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
324         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
325           body: fixture.messageThreaded1.text,
326           format: 'org.matrix.custom.html',
327           formatted_body: fixture.messageThreaded1.html,
328           msgtype: 'm.text'
329         });
330       });
332       it('new message from virtualUser is suppressed (no echo back and forth)', async () => {
333         const serializedVirtualUserMessage = await restSerializer.serializeObject(
334           fixture.messageFromVirtualUser1,
335           strategy
336         );
338         await gitterBridge.onDataChange({
339           type: 'chatMessage',
340           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
341           operation: 'create',
342           model: serializedVirtualUserMessage
343         });
345         // No room creation
346         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 0);
347         // No message sent
348         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 0);
349       });
351       it('new message in private room is bridged', async () => {
352         const strategy = new restSerializer.ChatStrategy();
353         const serializedMessage = await restSerializer.serializeObject(
354           fixture.messagePrivate1,
355           strategy
356         );
358         await gitterBridge.onDataChange({
359           type: 'chatMessage',
360           url: `/rooms/${fixture.troupePrivate1.id}/chatMessages`,
361           operation: 'create',
362           model: serializedMessage
363         });
365         // Room is created for something that hasn't been bridged before
366         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 1);
367         assert.deepEqual(matrixBridge.getIntent().createRoom.getCall(0).args[0], {
368           createAsClient: true,
369           options: {
370             name: fixture.troupePrivate1.uri,
371             room_alias_name: getCanonicalAliasLocalpartForGitterRoomUri(fixture.troupePrivate1.uri),
372             visibility: 'private',
373             preset: 'private_chat'
374           }
375         });
377         // Message is sent to the new room
378         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
379       });
381       describe('inviteMatrixUserToDmRoomIfNeeded', async () => {
382         const otherPersonMxid = '@alice:localhost';
384         let matrixRoomId;
385         let serializedMessage;
386         beforeEach(async () => {
387           matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
389           serializedMessage = await restSerializer.serializeObject(fixture.message1, strategy);
390         });
392         it('should invite Matrix user back to Matrix DM room if they have left', async () => {
393           const newDmRoom = await gitterUtils.getOrCreateGitterDmRoomByGitterUserAndOtherPersonMxid(
394             fixture.user1,
395             otherPersonMxid
396           );
397           await store.storeBridgedRoom(newDmRoom._id, matrixRoomId);
399           // Stub the other user as left the room
400           matrixBridge.getIntent().getStateEvent = (targetRoomId, type, stateKey) => {
401             if (
402               targetRoomId === matrixRoomId &&
403               type === 'm.room.member' &&
404               stateKey === otherPersonMxid
405             ) {
406               return {
407                 membership: 'leave'
408               };
409             }
410           };
412           await gitterBridge.onDataChange({
413             type: 'chatMessage',
414             url: `/rooms/${newDmRoom._id}/chatMessages`,
415             operation: 'create',
416             model: serializedMessage
417           });
419           assert.strictEqual(matrixBridge.getIntent().invite.callCount, 1);
420           assert.deepEqual(matrixBridge.getIntent().invite.getCall(0).args[1], otherPersonMxid);
421         });
423         it('should invite Matrix user back to Matrix DM room if they were never in the room', async () => {
424           const newDmRoom = await gitterUtils.getOrCreateGitterDmRoomByGitterUserAndOtherPersonMxid(
425             fixture.user1,
426             otherPersonMxid
427           );
428           await store.storeBridgedRoom(newDmRoom._id, matrixRoomId);
430           // Stub the other user as left the room
431           matrixBridge.getIntent().getStateEvent = (targetRoomId, type, stateKey) => {
432             if (
433               targetRoomId === matrixRoomId &&
434               type === 'm.room.member' &&
435               stateKey === otherPersonMxid
436             ) {
437               return null;
438             }
439           };
441           await gitterBridge.onDataChange({
442             type: 'chatMessage',
443             url: `/rooms/${newDmRoom._id}/chatMessages`,
444             operation: 'create',
445             model: serializedMessage
446           });
448           assert.strictEqual(matrixBridge.getIntent().invite.callCount, 1);
449           assert.deepEqual(matrixBridge.getIntent().invite.getCall(0).args[1], otherPersonMxid);
450         });
452         it('should work if Matrix user already in DM room (not mess anything up)', async () => {
453           const newDmRoom = await gitterUtils.getOrCreateGitterDmRoomByGitterUserAndOtherPersonMxid(
454             fixture.user1,
455             otherPersonMxid
456           );
457           await store.storeBridgedRoom(newDmRoom._id, matrixRoomId);
459           // Stub the other user as already in the room
460           matrixBridge.getIntent().getStateEvent = (targetRoomId, type, stateKey) => {
461             if (
462               targetRoomId === matrixRoomId &&
463               type === 'm.room.member' &&
464               stateKey === otherPersonMxid
465             ) {
466               return {
467                 membership: 'join'
468               };
469             }
470           };
472           await gitterBridge.onDataChange({
473             type: 'chatMessage',
474             url: `/rooms/${newDmRoom._id}/chatMessages`,
475             operation: 'create',
476             model: serializedMessage
477           });
479           assert.strictEqual(matrixBridge.getIntent().invite.callCount, 0);
480         });
482         it('should still allow message to send if Matrix user failed to invite', async () => {
483           const newDmRoom = await gitterUtils.getOrCreateGitterDmRoomByGitterUserAndOtherPersonMxid(
484             fixture.user1,
485             otherPersonMxid
486           );
487           await store.storeBridgedRoom(newDmRoom._id, matrixRoomId);
489           // Stub the other user as left the room
490           matrixBridge.getIntent().getStateEvent = (targetRoomId, type, stateKey) => {
491             if (
492               targetRoomId === matrixRoomId &&
493               type === 'm.room.member' &&
494               stateKey === otherPersonMxid
495             ) {
496               return {
497                 membership: 'leave'
498               };
499             }
500           };
502           // Force the invitation to fail!
503           matrixBridge.getIntent().invite = () => Promise.reject('Fake failed to invite');
505           await gitterBridge.onDataChange({
506             type: 'chatMessage',
507             url: `/rooms/${newDmRoom._id}/chatMessages`,
508             operation: 'create',
509             model: serializedMessage
510           });
512           // Make sure the feedback warning message from the bridge user (@gitter-badger)
513           // was sent in the Gitter room to let them know we had trouble inviting the Matrix
514           // side back to the room.
515           const messages = await chatService.findChatMessagesForTroupe(newDmRoom._id);
516           assert.strictEqual(
517             messages[0].text,
518             `Unable to invite Matrix user back to DM room. They probably won't know about the message you just sent.`
519           );
521           // Message is still sent to the new room
522           assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
523           assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
524             body: fixture.message1.text,
525             format: 'org.matrix.custom.html',
526             formatted_body: fixture.message1.html,
527             msgtype: 'm.text'
528           });
529         });
531         it('should not invite anyone for non-DM room', async () => {
532           sinon.spy(gitterBridge, 'inviteMatrixUserToDmRoomIfNeeded');
533           await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
535           await gitterBridge.onDataChange({
536             type: 'chatMessage',
537             url: `/rooms/${fixture.troupe1.id}/chatMessages`,
538             operation: 'create',
539             model: serializedMessage
540           });
542           // null means no invite was necessary for this room
543           const inviteResult = await gitterBridge.inviteMatrixUserToDmRoomIfNeeded.firstCall
544             .returnValue;
545           assert.strictEqual(inviteResult, null);
546         });
548         it('messages from the bridge bot do not trigger invites to be sent out (avoid feedback loop)', async () => {
549           await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
550           // Pass in userBridge1 as the bridging user
551           gitterBridge = new GitterBridge(matrixBridge, fixture.userBridge1.username);
552           sinon.spy(gitterBridge, 'inviteMatrixUserToDmRoomIfNeeded');
554           // Use a message from userBridge1
555           serializedMessage = await restSerializer.serializeObject(
556             fixture.messageFromBridgeBot1,
557             strategy
558           );
560           await gitterBridge.onDataChange({
561             type: 'chatMessage',
562             url: `/rooms/${fixture.troupe1.id}/chatMessages`,
563             operation: 'create',
564             model: serializedMessage
565           });
567           assert.strictEqual(gitterBridge.inviteMatrixUserToDmRoomIfNeeded.callCount, 0);
568         });
569       });
570     });
572     describe('handleChatMessageEditEvent', () => {
573       const fixture = fixtureLoader.setupEach({
574         user1: {},
575         userBridge1: {},
576         group1: {},
577         troupe1: {
578           group: 'group1'
579         },
580         troupePrivate1: {
581           group: 'group1',
582           users: ['user1'],
583           securityDescriptor: {
584             members: 'INVITE',
585             admins: 'MANUAL',
586             public: false
587           }
588         },
589         message1: {
590           user: 'user1',
591           troupe: 'troupe1',
592           text: 'my gitter message'
593         },
594         messageFromVirtualUser1: {
595           user: 'userBridge1',
596           virtualUser: {
597             type: 'matrix',
598             externalId: 'test-person:matrix.org',
599             displayName: 'Tessa'
600           },
601           troupe: 'troupe1',
602           text: 'my virtualUser message'
603         },
604         messagePrivate1: {
605           user: 'user1',
606           troupe: 'troupePrivate1',
607           text: 'my private gitter message'
608         }
609       });
611       it('edit message gets sent off to Matrix', async () => {
612         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
613         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
614         await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
616         const serializedMessage = await restSerializer.serializeObject(fixture.message1, strategy);
618         await gitterBridge.onDataChange({
619           type: 'chatMessage',
620           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
621           operation: 'update',
622           model: {
623             ...serializedMessage,
624             editedAt: new Date().toUTCString()
625           }
626         });
628         // Message edit is sent off to Matrix
629         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
631         assert.deepEqual(matrixBridge.getIntent().sendMessage.getCall(0).args[1], {
632           body: `* ${fixture.message1.text}`,
633           format: 'org.matrix.custom.html',
634           formatted_body: `* ${fixture.message1.html}`,
635           msgtype: 'm.text',
636           'm.new_content': {
637             body: fixture.message1.text,
638             format: 'org.matrix.custom.html',
639             formatted_body: fixture.message1.html,
640             msgtype: 'm.text'
641           },
642           'm.relates_to': {
643             event_id: matrixMessageEventId,
644             rel_type: 'm.replace'
645           }
646         });
647       });
649       it('message with same editedAt date is ignored', async () => {
650         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
651         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
652         await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
654         const serializedMessage = await restSerializer.serializeObject(fixture.message1, strategy);
656         await gitterBridge.onDataChange({
657           type: 'chatMessage',
658           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
659           operation: 'update',
660           model: serializedMessage
661         });
663         // Message edit does not get sent to Matrix since it's already over there
664         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 0);
665       });
667       it('non-bridged message that gets an edit is ignored', async () => {
668         const serializedMessage = await restSerializer.serializeObject(fixture.message1, strategy);
670         // We purposely do not associate the bridged message. We are testing that the
671         // edit is ignored if there is no association in the database.
672         //const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
673         //await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
675         await gitterBridge.onDataChange({
676           type: 'chatMessage',
677           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
678           operation: 'update',
679           model: serializedMessage
680         });
682         // Message edit is ignored if there isn't an associated bridge message
683         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 0);
684       });
686       it('message edit from virtualUser is suppressed (no echo back and forth)', async () => {
687         const serializedVirtualUserMessage = await restSerializer.serializeObject(
688           fixture.messageFromVirtualUser1,
689           strategy
690         );
692         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
693         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
694         await store.storeBridgedMessage(
695           fixture.messageFromVirtualUser1,
696           matrixRoomId,
697           matrixMessageEventId
698         );
700         await gitterBridge.onDataChange({
701           type: 'chatMessage',
702           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
703           operation: 'update',
704           model: serializedVirtualUserMessage
705         });
707         // No message sent
708         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 0);
709       });
711       it('edit in private room are bridged', async () => {
712         const serializedMessage = await restSerializer.serializeObject(
713           fixture.messagePrivate1,
714           strategy
715         );
717         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
718         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
719         await store.storeBridgedMessage(
720           fixture.messagePrivate1,
721           matrixRoomId,
722           matrixMessageEventId
723         );
725         await gitterBridge.onDataChange({
726           type: 'chatMessage',
727           url: `/rooms/${fixture.troupePrivate1.id}/chatMessages`,
728           operation: 'update',
729           model: {
730             ...serializedMessage,
731             editedAt: new Date().toUTCString()
732           }
733         });
735         assert.strictEqual(matrixBridge.getIntent().sendMessage.callCount, 1);
736       });
737     });
739     describe('handleChatMessageRemoveEvent', () => {
740       const fixture = fixtureLoader.setupEach({
741         user1: {},
742         userBridge1: {},
743         group1: {},
744         troupe1: {
745           group: 'group1'
746         },
747         troupePrivate1: {
748           group: 'group1',
749           users: ['user1'],
750           securityDescriptor: {
751             members: 'INVITE',
752             admins: 'MANUAL',
753             public: false
754           }
755         },
756         message1: {
757           user: 'user1',
758           troupe: 'troupe1',
759           text: 'my gitter message'
760         },
761         messagePrivate1: {
762           user: 'user1',
763           troupe: 'troupePrivate1',
764           text: 'my private gitter message'
765         }
766       });
768       it('remove message gets sent off to Matrix', async () => {
769         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
770         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
771         await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
773         await gitterBridge.onDataChange({
774           type: 'chatMessage',
775           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
776           operation: 'remove',
777           model: { id: fixture.message1.id }
778         });
780         // Message remove is sent off to Matrix
781         assert.strictEqual(matrixBridge.getIntent().matrixClient.redactEvent.callCount, 1);
782         assert.deepEqual(
783           matrixBridge.getIntent().matrixClient.redactEvent.getCall(0).args[1],
784           matrixMessageEventId
785         );
786       });
788       it('non-bridged message that gets removed is ignored', async () => {
789         // We purposely do not associate bridged message. We are testing that the
790         // remove is ignored if no association in the database.
791         //const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
792         //await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
794         await gitterBridge.onDataChange({
795           type: 'chatMessage',
796           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
797           operation: 'remove',
798           model: { id: fixture.message1.id }
799         });
801         // Message remove is ignored if there isn't an associated bridge message
802         assert.strictEqual(matrixBridge.getIntent().matrixClient.redactEvent.callCount, 0);
803       });
805       it('message remove in private room is bridged', async () => {
806         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
807         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
808         await store.storeBridgedMessage(
809           fixture.messagePrivate1,
810           matrixRoomId,
811           matrixMessageEventId
812         );
814         await gitterBridge.onDataChange({
815           type: 'chatMessage',
816           url: `/rooms/${fixture.troupePrivate1.id}/chatMessages`,
817           operation: 'remove',
818           model: { id: fixture.messagePrivate1.id }
819         });
821         assert.strictEqual(matrixBridge.getIntent().matrixClient.redactEvent.callCount, 1);
822       });
824       it('when the Matrix API call to lookup the message author fails(`intent.getEvent()`), still deletes the message (using bridge user)', async () => {
825         // Make the event lookup Matrix API call fail
826         matrixBridge.getIntent().getEvent = () => {
827           throw new Error('Fake error and failed to fetch event');
828         };
830         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
831         const matrixMessageEventId = `$${fixtureLoader.generateGithubId()}`;
832         await store.storeBridgedMessage(fixture.message1, matrixRoomId, matrixMessageEventId);
834         await gitterBridge.onDataChange({
835           type: 'chatMessage',
836           url: `/rooms/${fixture.troupe1.id}/chatMessages`,
837           operation: 'remove',
838           model: { id: fixture.message1.id }
839         });
841         // Message remove is sent off to Matrix
842         assert.strictEqual(matrixBridge.getIntent().matrixClient.redactEvent.callCount, 1);
843         assert.deepEqual(
844           matrixBridge.getIntent().matrixClient.redactEvent.getCall(0).args[1],
845           matrixMessageEventId
846         );
847       });
848     });
850     describe('handleRoomUpdateEvent', () => {
851       const fixture = fixtureLoader.setupEach({
852         group1: {},
853         troupe1: {
854           group: 'group1',
855           topic: 'foo'
856         },
857         troupePrivate1: {
858           group: 'group1',
859           users: ['user1'],
860           securityDescriptor: {
861             members: 'INVITE',
862             admins: 'MANUAL',
863             public: false
864           }
865         }
866       });
868       it('room patch gets sent off to Matrix', async () => {
869         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
870         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
872         await gitterBridge.onDataChange({
873           type: 'room',
874           url: `/rooms/${fixture.troupe1.id}`,
875           operation: 'patch',
876           model: { id: fixture.troupe1.id, topic: 'bar' }
877         });
879         // Find the spy call where the topic was updated
880         const topicCall = matrixBridge
881           .getIntent()
882           .sendStateEvent.getCalls()
883           .find(call => {
884             const [mid, eventType] = call.args;
885             if (mid === matrixRoomId && eventType === 'm.room.topic') {
886               return true;
887             }
888           });
889         assert.deepEqual(topicCall.args, [
890           matrixRoomId,
891           'm.room.topic',
892           '',
893           {
894             // This value should really be 'bar' no worries, this is just a side-effect of mocking the `onDataChange`
895             // instead of actually making an update to the room in the databse
896             topic: 'foo'
897           }
898         ]);
899       });
901       it('private room patch is bridged', async () => {
902         await gitterBridge.onDataChange({
903           type: 'room',
904           url: `/rooms/${fixture.troupePrivate1.id}`,
905           operation: 'patch',
906           model: { id: fixture.troupePrivate1.id, topic: 'bar' }
907         });
909         const sendStateEventCalls = matrixBridge.getIntent().sendStateEvent.getCalls();
910         assert(
911           sendStateEventCalls.length > 0,
912           `sendStateEvent was called ${sendStateEventCalls.length} times, expected at least 1 call`
913         );
914       });
916       it('room update gets sent off to Matrix (same as patch)', async () => {
917         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
918         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
920         const strategy = new restSerializer.TroupeStrategy();
921         const serializedRoom = await restSerializer.serializeObject(fixture.troupe1, strategy);
923         await gitterBridge.onDataChange({
924           type: 'room',
925           url: `/rooms/${fixture.troupe1.id}`,
926           operation: 'update',
927           model: serializedRoom
928         });
930         const sendStateEventCalls = matrixBridge.getIntent().sendStateEvent.getCalls();
931         assert(
932           sendStateEventCalls.length > 0,
933           `sendStateEvent was called ${sendStateEventCalls.length} times, expected at least 1 call`
934         );
935       });
937       it('private room update is bridged', async () => {
938         const strategy = new restSerializer.TroupeStrategy();
939         const serializedRoom = await restSerializer.serializeObject(fixture.troupe1, strategy);
941         await gitterBridge.onDataChange({
942           type: 'room',
943           url: `/rooms/${fixture.troupePrivate1.id}`,
944           operation: 'update',
945           model: serializedRoom
946         });
948         const sendStateEventCalls = matrixBridge.getIntent().sendStateEvent.getCalls();
949         assert(
950           sendStateEventCalls.length > 0,
951           `sendStateEvent was called ${sendStateEventCalls.length} times, expected at least 1 call`
952         );
953       });
954     });
956     describe('handleRoomRemoveEvent', () => {
957       const fixture = fixtureLoader.setupEach({
958         group1: {},
959         troupe1: {
960           group: 'group1',
961           topic: 'foo'
962         },
963         troupePrivate1: {
964           group: 'group1',
965           users: ['user1'],
966           securityDescriptor: {
967             members: 'INVITE',
968             admins: 'MANUAL',
969             public: false
970           }
971         }
972       });
974       it('deleted Gitter room shuts down the room on the Matrix side', async () => {
975         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
976         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
978         await gitterBridge.onDataChange({
979           type: 'room',
980           url: `/rooms/${fixture.troupe1.id}`,
981           operation: 'remove',
982           model: { id: fixture.troupe1.id }
983         });
985         // Find the spy call where the join rules are changed so no one else can join
986         const joinRuleCall = matrixBridge
987           .getIntent()
988           .sendStateEvent.getCalls()
989           .find(call => {
990             const [mid, eventType] = call.args;
991             if (mid === matrixRoomId && eventType === 'm.room.join_rules') {
992               return true;
993             }
994           });
995         assert.deepEqual(joinRuleCall.args, [
996           matrixRoomId,
997           'm.room.join_rules',
998           '',
999           {
1000             join_rule: 'invite'
1001           }
1002         ]);
1003       });
1004     });
1006     describe('handleUserJoiningRoom', () => {
1007       const fixture = fixtureLoader.setupEach({
1008         user1: {},
1009         troupe1: {}
1010       });
1012       it('user join membership syncs to Matrix', async () => {
1013         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1014         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1015         const mxidForGitterUser = getMxidForGitterUser(fixture.user1);
1017         await gitterBridge.onDataChange({
1018           type: 'user',
1019           url: `/rooms/${fixture.troupe1.id}/users`,
1020           operation: 'create',
1021           model: { id: fixture.user1.id }
1022         });
1024         assert.strictEqual(matrixBridge.getIntent.callCount, 3);
1025         assert.strictEqual(matrixBridge.getIntent.getCall(2).args[0], mxidForGitterUser);
1026         assert.strictEqual(matrixBridge.getIntent().join.callCount, 1);
1027         assert.strictEqual(matrixBridge.getIntent().join.getCall(0).args[0], matrixRoomId);
1028       });
1030       it(`user join is ignored when the Matrix room isn't created yet`, async () => {
1031         // This is commented out on purpose, we are testing that the join is ignored
1032         // when the Matrix room hasn't been created yet.
1033         //await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1035         await gitterBridge.onDataChange({
1036           type: 'user',
1037           url: `/rooms/${fixture.troupe1.id}/users`,
1038           operation: 'create',
1039           model: { id: fixture.user1.id }
1040         });
1042         assert.strictEqual(matrixBridge.getIntent().join.callCount, 0);
1043       });
1045       it('no action occurs when user join fails for normal room', async () => {
1046         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1047         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1049         // Make the event lookup Matrix API call fail
1050         matrixBridge.getIntent().join = () => {
1051           throw new Error('Failed to join room');
1052         };
1054         await gitterBridge.onDataChange({
1055           type: 'user',
1056           url: `/rooms/${fixture.troupe1.id}/users`,
1057           operation: 'create',
1058           model: { id: fixture.user1.id }
1059         });
1061         // No room creation
1062         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 0);
1063       });
1065       it('new Matrix DM created when user join fails for DM room', async () => {
1066         const otherPersonMxid = '@alice:localhost';
1067         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1068         const newDmRoom = await gitterUtils.getOrCreateGitterDmRoomByGitterUserAndOtherPersonMxid(
1069           fixture.user1,
1070           otherPersonMxid
1071         );
1072         await store.storeBridgedRoom(newDmRoom._id, matrixRoomId);
1074         // Make the event lookup Matrix API call fail
1075         matrixBridge.getIntent().join = () => {
1076           throw new Error('Failed to join room');
1077         };
1079         await gitterBridge.onDataChange({
1080           type: 'user',
1081           url: `/rooms/${newDmRoom._id}/users`,
1082           operation: 'create',
1083           model: { id: fixture.user1.id }
1084         });
1086         // New DM room is created
1087         assert.strictEqual(matrixBridge.getIntent().createRoom.callCount, 1);
1088         assert.deepEqual(matrixBridge.getIntent().createRoom.getCall(0).args[0], {
1089           createAsClient: true,
1090           options: {
1091             visibility: 'private',
1092             preset: 'trusted_private_chat',
1093             is_direct: true,
1094             invite: [otherPersonMxid]
1095           }
1096         });
1097       });
1098     });
1100     describe('handleUserLeavingRoom', () => {
1101       const fixture = fixtureLoader.setupEach({
1102         user1: {},
1103         troupe1: {}
1104       });
1106       it('user leave membership syncs to Matrix', async () => {
1107         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1108         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1109         const mxidForGitterUser = getMxidForGitterUser(fixture.user1);
1111         await gitterBridge.onDataChange({
1112           type: 'user',
1113           url: `/rooms/${fixture.troupe1.id}/users`,
1114           operation: 'remove',
1115           model: { id: fixture.user1.id }
1116         });
1118         assert.strictEqual(matrixBridge.getIntent.callCount, 3);
1119         assert.strictEqual(matrixBridge.getIntent.getCall(2).args[0], mxidForGitterUser);
1120         assert.strictEqual(matrixBridge.getIntent().leave.callCount, 1);
1121         assert.strictEqual(matrixBridge.getIntent().leave.getCall(0).args[0], matrixRoomId);
1122       });
1124       it(`user leave is ignored when the Matrix room isn't created yet`, async () => {
1125         // This is commented out on purpose, we are testing that the leave is ignored
1126         // when the Matrix room hasn't been created yet.
1127         //await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1129         await gitterBridge.onDataChange({
1130           type: 'user',
1131           url: `/rooms/${fixture.troupe1.id}/users`,
1132           operation: 'remove',
1133           model: { id: fixture.user1.id }
1134         });
1136         assert.strictEqual(matrixBridge.getIntent().leave.callCount, 0);
1137       });
1138     });
1140     describe('handleRoomBanEvent', () => {
1141       const fixture = fixtureLoader.setupEach({
1142         user1: {},
1143         troupe1: {}
1144       });
1146       it('bridges ban for Gitter user', async () => {
1147         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1148         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1149         const mxidForGitterUser = getMxidForGitterUser(fixture.user1);
1151         await gitterBridge.onDataChange({
1152           type: 'ban',
1153           url: `/rooms/${fixture.troupe1.id}/bans`,
1154           operation: 'create',
1155           model: { userId: fixture.user1.id }
1156         });
1158         assert.strictEqual(matrixBridge.getIntent().ban.callCount, 1);
1159         assert.strictEqual(matrixBridge.getIntent().ban.getCall(0).args[0], matrixRoomId);
1160         assert.strictEqual(matrixBridge.getIntent().ban.getCall(0).args[1], mxidForGitterUser);
1161       });
1163       it('bridges unban for Gitter user', async () => {
1164         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1165         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1166         const mxidForGitterUser = getMxidForGitterUser(fixture.user1);
1168         await gitterBridge.onDataChange({
1169           type: 'ban',
1170           url: `/rooms/${fixture.troupe1.id}/bans`,
1171           operation: 'remove',
1172           model: { userId: fixture.user1.id }
1173         });
1175         assert.strictEqual(matrixBridge.getIntent().unban.callCount, 1);
1176         assert.strictEqual(matrixBridge.getIntent().unban.getCall(0).args[0], matrixRoomId);
1177         assert.strictEqual(matrixBridge.getIntent().unban.getCall(0).args[1], mxidForGitterUser);
1178       });
1180       it('bridges ban for virtualUser', async () => {
1181         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1182         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1184         await gitterBridge.onDataChange({
1185           type: 'ban',
1186           url: `/rooms/${fixture.troupe1.id}/bans`,
1187           operation: 'create',
1188           model: {
1189             virtualUser: {
1190               type: 'matrix',
1191               externalId: 'bad-guy:matrix.org'
1192             }
1193           }
1194         });
1196         assert.strictEqual(matrixBridge.getIntent().ban.callCount, 1);
1197         assert.strictEqual(matrixBridge.getIntent().ban.getCall(0).args[0], matrixRoomId);
1198         assert.strictEqual(matrixBridge.getIntent().ban.getCall(0).args[1], '@bad-guy:matrix.org');
1199       });
1201       it('bridges unban for virtualUser', async () => {
1202         const matrixRoomId = `!${fixtureLoader.generateGithubId()}:localhost`;
1203         await store.storeBridgedRoom(fixture.troupe1.id, matrixRoomId);
1205         await gitterBridge.onDataChange({
1206           type: 'ban',
1207           url: `/rooms/${fixture.troupe1.id}/bans`,
1208           operation: 'remove',
1209           model: {
1210             virtualUser: {
1211               type: 'matrix',
1212               externalId: 'bad-guy:matrix.org'
1213             }
1214           }
1215         });
1217         assert.strictEqual(matrixBridge.getIntent().unban.callCount, 1);
1218         assert.strictEqual(matrixBridge.getIntent().unban.getCall(0).args[0], matrixRoomId);
1219         assert.strictEqual(
1220           matrixBridge.getIntent().unban.getCall(0).args[1],
1221           '@bad-guy:matrix.org'
1222         );
1223       });
1224     });
1225   });