@webex/internal-plugin-conversation 2.59.1 → 2.59.3-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/.eslintrc.js +6 -6
  2. package/README.md +47 -47
  3. package/babel.config.js +3 -3
  4. package/dist/activities.js +4 -4
  5. package/dist/activities.js.map +1 -1
  6. package/dist/activity-thread-ordering.js +34 -34
  7. package/dist/activity-thread-ordering.js.map +1 -1
  8. package/dist/config.js +12 -12
  9. package/dist/config.js.map +1 -1
  10. package/dist/constants.js.map +1 -1
  11. package/dist/conversation.js +474 -474
  12. package/dist/conversation.js.map +1 -1
  13. package/dist/convo-error.js +4 -4
  14. package/dist/convo-error.js.map +1 -1
  15. package/dist/decryption-transforms.js +155 -155
  16. package/dist/decryption-transforms.js.map +1 -1
  17. package/dist/encryption-transforms.js.map +1 -1
  18. package/dist/index.js +2 -2
  19. package/dist/index.js.map +1 -1
  20. package/dist/share-activity.js +57 -57
  21. package/dist/share-activity.js.map +1 -1
  22. package/dist/to-array.js +7 -7
  23. package/dist/to-array.js.map +1 -1
  24. package/jest.config.js +3 -3
  25. package/package.json +21 -20
  26. package/process +1 -1
  27. package/src/activities.js +157 -157
  28. package/src/activity-thread-ordering.js +283 -283
  29. package/src/activity-threading.md +282 -282
  30. package/src/config.js +37 -37
  31. package/src/constants.js +3 -3
  32. package/src/conversation.js +2535 -2535
  33. package/src/convo-error.js +15 -15
  34. package/src/decryption-transforms.js +541 -541
  35. package/src/encryption-transforms.js +345 -345
  36. package/src/index.js +327 -327
  37. package/src/share-activity.js +436 -436
  38. package/src/to-array.js +29 -29
  39. package/test/integration/spec/create.js +290 -290
  40. package/test/integration/spec/encryption.js +333 -333
  41. package/test/integration/spec/get.js +1255 -1255
  42. package/test/integration/spec/mercury.js +94 -94
  43. package/test/integration/spec/share.js +537 -537
  44. package/test/integration/spec/verbs.js +1041 -1041
  45. package/test/unit/spec/conversation.js +823 -823
  46. package/test/unit/spec/decrypt-transforms.js +460 -460
  47. package/test/unit/spec/encryption-transforms.js +93 -93
  48. package/test/unit/spec/share-activity.js +178 -178
@@ -1,1041 +1,1041 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import '@webex/internal-plugin-conversation';
6
-
7
- import {patterns} from '@webex/common';
8
- import WebexCore, {WebexHttpError} from '@webex/webex-core';
9
- import {assert} from '@webex/test-helper-chai';
10
- import testUsers from '@webex/test-helper-test-users';
11
- import {find, map} from 'lodash';
12
- import uuid from 'uuid';
13
- import fh from '@webex/test-helper-file';
14
- import {skipInNode} from '@webex/test-helper-mocha';
15
-
16
- describe('plugin-conversation', function () {
17
- this.timeout(30000);
18
- describe('verbs', () => {
19
- let checkov, mccoy, participants, webex, spock;
20
-
21
- before(() =>
22
- testUsers.create({count: 3}).then(async (users) => {
23
- [spock, mccoy, checkov] = participants = users;
24
-
25
- // Pause for 5 seconds for CI
26
- await new Promise((done) => setTimeout(done, 5000));
27
-
28
- webex = new WebexCore({
29
- credentials: {
30
- authorization: spock.token,
31
- },
32
- });
33
-
34
- mccoy.webex = new WebexCore({
35
- credentials: {
36
- authorization: mccoy.token,
37
- },
38
- });
39
-
40
- return Promise.all([
41
- webex.internal.mercury.connect(),
42
- mccoy.webex.internal.mercury.connect(),
43
- ]);
44
- })
45
- );
46
-
47
- after(() =>
48
- Promise.all([
49
- webex && webex.internal.mercury.disconnect(),
50
- mccoy && mccoy.webex.internal.mercury.disconnect(),
51
- ])
52
- );
53
-
54
- function makeEmailAddress() {
55
- return `webex-js-sdk--test-${uuid.v4()}@example.com`;
56
- }
57
-
58
- let conversation;
59
-
60
- beforeEach(() => {
61
- if (conversation) {
62
- return Promise.resolve();
63
- }
64
-
65
- return webex.internal.conversation.create({participants}).then((c) => {
66
- conversation = c;
67
- });
68
- });
69
-
70
- describe('#add()', () => {
71
- let email;
72
-
73
- beforeEach(() => {
74
- email = makeEmailAddress();
75
- });
76
-
77
- beforeEach(() =>
78
- webex.internal.conversation
79
- .create({participants: [checkov]}, {forceGrouped: true})
80
- .then((c) => {
81
- conversation = c;
82
- })
83
- );
84
-
85
- it('adds the specified user to the specified conversation', () =>
86
- webex.internal.conversation.add(conversation, mccoy).then((activity) => {
87
- assert.isActivity(activity);
88
- assert.property(activity, 'kmsMessage');
89
- }));
90
-
91
- it("grants the specified user access to the conversation's key", () =>
92
- webex.internal.conversation
93
- .post(conversation, {displayName: 'PROOF!'})
94
- .then(() => webex.internal.conversation.add(conversation, mccoy))
95
- .then(() => mccoy.webex.internal.conversation.get(conversation, {activitiesLimit: 10}))
96
- .then((c) => {
97
- assert.isConversation(c);
98
- const activity = find(c.activities.items, {verb: 'post'});
99
-
100
- assert.equal(activity.object.displayName, 'PROOF!');
101
- }));
102
-
103
- // TODO: Issues with side boarding users too soon. Skipping until it's fixed
104
- it.skip('sideboards a non-existent user', () =>
105
- webex.internal.conversation
106
- .add(conversation, email)
107
- .then((activity) => {
108
- assert.isActivity(activity);
109
-
110
- return webex.internal.conversation.get(conversation, {includeParticipants: true});
111
- })
112
- .then((c) => {
113
- assert.isConversation(c);
114
- const participant = find(c.participants.items, {emailAddress: email});
115
-
116
- assert.include(participant.tags, 'SIDE_BOARDED');
117
- assert.match(participant.id, patterns.uuid);
118
- }));
119
- });
120
-
121
- describe('#assign()', () => {
122
- before(() =>
123
- webex.internal.conversation.create({participants}).then((c) => {
124
- conversation = c;
125
- })
126
- );
127
-
128
- let sampleImageSmallOnePng = 'sample-image-small-one.png';
129
-
130
- before(() =>
131
- fh.fetch(sampleImageSmallOnePng).then((res) => {
132
- sampleImageSmallOnePng = res;
133
- })
134
- );
135
-
136
- it('assigns an avatar to a room', () =>
137
- webex.internal.conversation
138
- .assign(conversation, sampleImageSmallOnePng)
139
- .then(() => webex.internal.conversation.get(conversation))
140
- .then((c) => {
141
- assert.property(c, 'avatar');
142
-
143
- assert.property(c.avatar, 'files');
144
- assert.property(c.avatar.files, 'items');
145
- assert.lengthOf(c.avatar.files.items, 1);
146
- assert.property(c.avatar.files.items[0], 'fileSize');
147
- assert.property(c.avatar.files.items[0], 'mimeType');
148
- assert.property(c.avatar.files.items[0], 'objectType');
149
- assert.property(c.avatar.files.items[0], 'scr');
150
- assert.property(c.avatar.files.items[0], 'url');
151
- assert.equal(c.avatar.objectType, 'content');
152
-
153
- assert.isString(c.avatarEncryptionKeyUrl);
154
- assert.isObject(c.avatar.files.items[0].scr, 'The scr was decrypted');
155
- assert.equal(c.avatar.files.items[0].displayName, 'sample-image-small-one.png');
156
-
157
- assert.property(c, 'avatarEncryptionKeyUrl');
158
- }));
159
- });
160
-
161
- describe('#leave()', () => {
162
- afterEach(() => {
163
- conversation = null;
164
- });
165
-
166
- it('removes the current user from the specified conversation', () =>
167
- webex.internal.conversation
168
- .leave(conversation)
169
- .then((activity) => {
170
- assert.isActivity(activity);
171
-
172
- return assert.isRejected(webex.internal.conversation.get(conversation));
173
- })
174
- .then((reason) => {
175
- assert.statusCode(reason, 404);
176
-
177
- return assert.isRejected(
178
- webex.internal.encryption.kms.fetchKey({
179
- uri: conversation.defaultActivityEncryptionKeyUrl,
180
- })
181
- );
182
- })
183
- .then((reason) => assert.equal(reason.status, 403)));
184
-
185
- it('removes the specified user from the specified conversation', () =>
186
- webex.internal.conversation
187
- .leave(conversation, mccoy)
188
- .then((activity) => {
189
- assert.isActivity(activity);
190
-
191
- return assert.isRejected(mccoy.webex.internal.conversation.get(conversation));
192
- })
193
- .then((reason) => {
194
- assert.statusCode(reason, 404);
195
-
196
- return assert.isRejected(
197
- mccoy.webex.internal.encryption.kms.fetchKey({
198
- uri: conversation.defaultActivityEncryptionKeyUrl,
199
- })
200
- );
201
- })
202
- .then((reason) => assert.equal(reason.status, 403)));
203
-
204
- describe('with deleted users', () => {
205
- let redshirt;
206
-
207
- beforeEach(() =>
208
- testUsers.create({count: 1}).then(([rs]) => {
209
- redshirt = rs;
210
-
211
- return webex.internal.conversation.add(conversation, rs);
212
- })
213
- );
214
-
215
- it('removes the specified deleted user from the specified conversation', () =>
216
- webex.internal.conversation
217
- .leave(conversation, redshirt)
218
- .then(() => webex.internal.conversation.get(conversation, {includeParticipants: true}))
219
- .then((c) => {
220
- assert.lengthOf(c.participants.items, 3);
221
- assert.notInclude(map(c.participants.items, 'id'), redshirt.id);
222
- }));
223
- });
224
- });
225
-
226
- describe('#leave() with id only', () => {
227
- afterEach(() => {
228
- conversation = null;
229
- });
230
-
231
- it('removes the current user by id only', () =>
232
- webex.internal.conversation
233
- .leave({
234
- id: conversation.id,
235
- defaultActivityEncryptionKeyUrl: conversation.defaultActivityEncryptionKeyUrl,
236
- kmsResourceObjectUrl: conversation.kmsResourceObjectUrl,
237
- })
238
- .then((activity) => {
239
- assert.isActivity(activity);
240
-
241
- return assert.isRejected(webex.internal.conversation.get(conversation));
242
- })
243
- .then((reason) => {
244
- assert.statusCode(reason, 404);
245
-
246
- return assert.isRejected(
247
- webex.internal.encryption.kms.fetchKey({
248
- uri: conversation.defaultActivityEncryptionKeyUrl,
249
- })
250
- );
251
- })
252
- .then((reason) => assert.equal(reason.status, 403)));
253
- });
254
-
255
- describe('#post()', () => {
256
- let message, richMessage;
257
-
258
- beforeEach(() => {
259
- message = 'mccoy, THIS IS A TEST MESSAGE';
260
- richMessage = `<webex-mention data-object-id="${mccoy.id}" data-object-type="person">mccoy</webex-mention>, THIS IS A TEST MESSAGE`;
261
- });
262
-
263
- // disable until helper-html has node support
264
- skipInNode(describe)('when there are html tags in rich messages', () => {
265
- const allTagsUsedInThisTest = {
266
- div: [],
267
- b: [],
268
- span: [],
269
- };
270
-
271
- [
272
- {
273
- it: 'allows allowed outbound and inbound tags',
274
- allowedOutboundTags: {div: []},
275
- allowedInboundTags: {div: []},
276
- outboundMessage: '<div>HELLO</div>',
277
- outboundFilteredMessage: '<div>HELLO</div>',
278
- inboundMessage: '<div>HELLO</div>',
279
- },
280
- {
281
- it: 'filters and escapes disallowed outbound tags',
282
- allowedOutboundTags: {},
283
- allowedInboundTags: {},
284
- outboundMessage: '<div><b>HELLO</b></div>',
285
- outboundFilteredMessage: '&lt;div&gt;&lt;b&gt;HELLO&lt;/b&gt;&lt;/div&gt;',
286
- inboundMessage: '&lt;div&gt;&lt;b&gt;HELLO&lt;/b&gt;&lt;/div&gt;',
287
- },
288
- {
289
- it: 'filters disallowed inbound tags',
290
- allowedOutboundTags: {div: [], b: []},
291
- allowedInboundTags: {b: []},
292
- outboundMessage: '<div><b>HELLO</b></div>',
293
- outboundFilteredMessage: '<div><b>HELLO</b></div>',
294
- inboundMessage: '<b>HELLO</b>',
295
- },
296
- {
297
- it: 'filters and escapes the correct outbound tags',
298
- allowedOutboundTags: {div: [], span: []},
299
- allowedInboundTags: {},
300
- outboundMessage: "<div><b>HELLO</b><span> it's me</span></div>",
301
- outboundFilteredMessage: "<div>&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span></div>",
302
- inboundMessage: "&lt;b&gt;HELLO&lt;/b&gt; it's me",
303
- },
304
- {
305
- it: 'filters the correct inbound tags and filters and escapes the correct outbound tags',
306
- allowedOutboundTags: {div: [], span: []},
307
- allowedInboundTags: {span: []},
308
- outboundMessage: "<div><b>HELLO</b><span> it's me</span></div>",
309
- outboundFilteredMessage: "<div>&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span></div>",
310
- inboundMessage: "&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span>",
311
- },
312
- ].forEach((def) => {
313
- it(def.it, () => {
314
- webex.config.conversation.allowedOutboundTags = def.allowedOutboundTags;
315
- // since responses to spock's post will count as 'inbound', we
316
- // enable all the tags for allowedInboundTags so that we know
317
- // the message is filtered by only the outbound rules
318
- webex.config.conversation.allowedInboundTags = allTagsUsedInThisTest;
319
-
320
- return webex.internal.conversation
321
- .post(conversation, {
322
- displayName: message,
323
- content: def.outboundMessage,
324
- })
325
- .then((activity) => {
326
- assert.equal(activity.object.content, def.outboundFilteredMessage);
327
- mccoy.webex.config.conversation.allowedInboundTags = def.allowedInboundTags;
328
-
329
- return mccoy.webex.internal.conversation.get(conversation, {activitiesLimit: 1});
330
- })
331
- .then((convo) => {
332
- // check latest message
333
- const activity = find(convo.activities.items, {verb: 'post'});
334
-
335
- assert.equal(activity.object.content, def.inboundMessage);
336
- });
337
- });
338
- });
339
- });
340
-
341
- describe('when encryption key need to rotate', () => {
342
- it('can post a plain text successfully', () => {
343
- const {defaultActivityEncryptionKeyUrl} = conversation;
344
-
345
- Promise.all([
346
- webex
347
- .request({
348
- method: 'GET',
349
- api: 'conversation',
350
- resource: `/conversations/${conversation.id}/healed?resetKey=true`,
351
- })
352
- .then(() =>
353
- webex.request({
354
- method: 'GET',
355
- api: 'conversation',
356
- resource: `conversations/${conversation.id}`,
357
- })
358
- ),
359
- ])
360
- .then((res) => {
361
- res.body.defaultActivityEncryptionKeyUrl = defaultActivityEncryptionKeyUrl;
362
-
363
- return webex.internal.conversation.post(res.body, message);
364
- })
365
- .then((res) => {
366
- assert.notEqual(
367
- res.body.defaultActivityEncryptionKeyUrl,
368
- defaultActivityEncryptionKeyUrl
369
- );
370
- });
371
- });
372
- });
373
-
374
- it('posts a comment to the specified conversation', () =>
375
- webex.internal.conversation.post(conversation, message).then((activity) => {
376
- assert.isActivity(activity);
377
-
378
- assert.isEncryptedActivity(activity);
379
- assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
380
-
381
- assert.equal(activity.object.displayName, message);
382
- }));
383
-
384
- it("updates the specified conversation's unread status", () =>
385
- mccoy.webex.internal.conversation.get(conversation).then((c) => {
386
- const {lastSeenActivityDate, lastReadableActivityDate} = c;
387
-
388
- return webex.internal.conversation.post(conversation, message).then(() =>
389
- mccoy.webex.internal.conversation.get(conversation).then((c2) => {
390
- assert.equal(c2.lastSeenActivityDate, lastSeenActivityDate);
391
- assert.isAbove(
392
- Date.parse(c2.lastReadableActivityDate),
393
- Date.parse(lastReadableActivityDate)
394
- );
395
- })
396
- );
397
- }));
398
-
399
- it('posts rich content to the specified conversation', () =>
400
- webex.internal.conversation
401
- .post(conversation, {
402
- displayName: message,
403
- content: richMessage,
404
- })
405
- .then((activity) => {
406
- assert.isActivity(activity);
407
-
408
- assert.isEncryptedActivity(activity);
409
- assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
410
-
411
- assert.equal(activity.object.displayName, message);
412
- assert.equal(activity.object.content, richMessage);
413
- }));
414
-
415
- it('submits mentions to the specified conversation', () =>
416
- webex.internal.conversation
417
- .post(conversation, {
418
- displayName: message,
419
- content: richMessage,
420
- mentions: {
421
- items: [
422
- {
423
- id: mccoy.id,
424
- objectType: 'person',
425
- },
426
- ],
427
- },
428
- })
429
- .then((activity) => {
430
- assert.isActivity(activity);
431
-
432
- assert.isEncryptedActivity(activity);
433
- assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
434
-
435
- assert.equal(activity.object.displayName, message);
436
- assert.equal(activity.object.content, richMessage);
437
-
438
- assert.isDefined(activity.object.mentions);
439
- assert.isDefined(activity.object.mentions.items);
440
- assert.lengthOf(activity.object.mentions.items, 1);
441
- assert.equal(activity.object.mentions.items[0].id, mccoy.id);
442
- }));
443
- });
444
-
445
- describe('#update()', () => {
446
- it('renames the specified conversation', () =>
447
- webex.internal.conversation
448
- .update(conversation, {
449
- displayName: 'displayName2',
450
- objectType: 'conversation',
451
- })
452
- .then((c) => webex.internal.conversation.get({url: c.target.url}))
453
- .then((c) => assert.equal(c.displayName, 'displayName2')));
454
- });
455
-
456
- describe('#unassign()', () => {
457
- before(() =>
458
- webex.internal.conversation.create({participants}).then((c) => {
459
- conversation = c;
460
- })
461
- );
462
-
463
- let sampleImageSmallOnePng = 'sample-image-small-one.png';
464
-
465
- before(() =>
466
- fh.fetch(sampleImageSmallOnePng).then((res) => {
467
- sampleImageSmallOnePng = res;
468
- })
469
- );
470
-
471
- beforeEach(() => webex.internal.conversation.assign(conversation, sampleImageSmallOnePng));
472
-
473
- it('unassigns an avatar from a room', () =>
474
- webex.internal.conversation.unassign(conversation).then(() =>
475
- webex.internal.conversation.get(conversation).then((c) => {
476
- assert.notProperty(c, 'avatar');
477
- assert.notProperty(c, 'avatarEncryptionKeyUrl');
478
- })
479
- ));
480
- });
481
-
482
- describe('#updateKey()', () => {
483
- beforeEach(() =>
484
- webex.internal.conversation
485
- .create({participants, comment: 'THIS IS A COMMENT'})
486
- .then((c) => {
487
- conversation = c;
488
- })
489
- );
490
-
491
- it('assigns an unused key to the specified conversation', () =>
492
- webex.internal.conversation
493
- .updateKey(conversation)
494
- .then((activity) => {
495
- assert.isActivity(activity);
496
-
497
- return webex.internal.conversation.get(conversation);
498
- })
499
- .then((c) => {
500
- assert.isDefined(c.defaultActivityEncryptionKeyUrl);
501
- assert.notEqual(
502
- c.defaultActivityEncryptionKeyUrl,
503
- conversation.defaultActivityEncryptionKeyUrl
504
- );
505
- }));
506
-
507
- it('assigns the specified key to the specified conversation', () =>
508
- webex.internal.encryption.kms.createUnboundKeys({count: 1}).then(([key]) =>
509
- webex.internal.conversation
510
- .updateKey(conversation, key)
511
- .then((activity) => {
512
- assert.isActivity(activity);
513
- assert.equal(activity.object.defaultActivityEncryptionKeyUrl, key.uri);
514
-
515
- return webex.internal.conversation.get(conversation);
516
- })
517
- .then((c) => {
518
- assert.isDefined(c.defaultActivityEncryptionKeyUrl);
519
- assert.notEqual(
520
- c.defaultActivityEncryptionKeyUrl,
521
- conversation.defaultActivityEncryptionKeyUrl
522
- );
523
- })
524
- ));
525
-
526
- it('grants access to the key for all users in the conversation', () =>
527
- webex.internal.conversation
528
- .updateKey(conversation)
529
- .then((activity) => {
530
- assert.isActivity(activity);
531
-
532
- return mccoy.webex.internal.conversation.get({
533
- url: conversation.url,
534
- participantsLimit: 0,
535
- activitiesLimit: 0,
536
- });
537
- })
538
- .then((c) => {
539
- assert.isDefined(c.defaultActivityEncryptionKeyUrl);
540
- assert.notEqual(
541
- c.defaultActivityEncryptionKeyUrl,
542
- conversation.defaultActivityEncryptionKeyUrl
543
- );
544
-
545
- return mccoy.webex.internal.encryption.kms.fetchKey({
546
- uri: c.defaultActivityEncryptionKeyUrl,
547
- });
548
- }));
549
- });
550
-
551
- describe('#updateTypingStatus()', () => {
552
- beforeEach(() =>
553
- webex.internal.conversation
554
- .create({participants, comment: 'THIS IS A COMMENT'})
555
- .then((c) => {
556
- conversation = c;
557
- })
558
- );
559
-
560
- it('sets the typing indicator for the specified conversation', () =>
561
- webex.internal.conversation
562
- .updateTypingStatus(conversation, {typing: true})
563
- .then(({statusCode}) => {
564
- assert.equal(statusCode, 204);
565
- }));
566
-
567
- it('clears the typing indicator for the specified conversation', () =>
568
- webex.internal.conversation
569
- .updateTypingStatus(conversation, {typing: false})
570
- .then(({statusCode}) => {
571
- assert.equal(statusCode, 204);
572
- }));
573
-
574
- it('fails if called with a bad conversation object', () => {
575
- let error;
576
-
577
- return webex.internal.conversation
578
- .updateTypingStatus({}, {typing: false})
579
- .catch((reason) => {
580
- error = reason;
581
- })
582
- .then(() => {
583
- assert.isDefined(error);
584
- });
585
- });
586
-
587
- it('infers id from conversation url if missing', () => {
588
- Reflect.deleteProperty(conversation, 'id');
589
-
590
- return webex.internal.conversation
591
- .updateTypingStatus(conversation, {typing: true})
592
- .then(({statusCode}) => {
593
- assert.equal(statusCode, 204);
594
- });
595
- });
596
- });
597
-
598
- describe('verbs that update conversation tags', () => {
599
- [
600
- {
601
- itString: 'favorites the specified conversation',
602
- tag: 'FAVORITE',
603
- verb: 'favorite',
604
- },
605
- {
606
- itString: 'hides the specified conversation',
607
- tag: 'HIDDEN',
608
- verb: 'hide',
609
- },
610
- {
611
- itString: 'locks the specified conversation',
612
- tag: 'LOCKED',
613
- verb: 'lock',
614
- },
615
- {
616
- itString: 'mutes the specified conversation',
617
- tag: 'MUTED',
618
- verb: 'mute',
619
- },
620
- ].forEach(({tag, verb, itString}) => {
621
- describe(`#${verb}()`, () => {
622
- it(itString, () =>
623
- webex.internal.conversation[verb](conversation)
624
- .then((activity) => {
625
- assert.isActivity(activity);
626
- })
627
- .then(() => webex.internal.conversation.get(conversation))
628
- .then((c) => assert.include(c.tags, tag))
629
- );
630
- });
631
- });
632
-
633
- [
634
- {
635
- itString: 'unfavorites the specified conversation',
636
- setupVerb: 'favorite',
637
- tag: 'FAVORITE',
638
- verb: 'unfavorite',
639
- },
640
- {
641
- itString: 'unhides the specified conversation',
642
- setupVerb: 'hide',
643
- tag: 'HIDDEN',
644
- verb: 'unhide',
645
- },
646
- {
647
- itString: 'unlocks the specified conversation',
648
- setupVerb: 'lock',
649
- tag: 'LOCKED',
650
- verb: 'unlock',
651
- },
652
- {
653
- itString: 'unmutes the specified conversation',
654
- setupVerb: 'mute',
655
- tag: 'MUTED',
656
- verb: 'unmute',
657
- },
658
- ].forEach(({tag, verb, itString, setupVerb}) => {
659
- describe(`#${verb}()`, () => {
660
- beforeEach(() =>
661
- webex.internal.conversation[setupVerb](conversation).catch((reason) => {
662
- if (reason.statusCode !== 403) {
663
- throw reason;
664
- }
665
- })
666
- );
667
-
668
- it(itString, () =>
669
- webex.internal.conversation[verb](conversation)
670
- .then((activity) => {
671
- assert.isActivity(activity);
672
- })
673
- .then(() => webex.internal.conversation.get(conversation))
674
- .then((c) => assert.notInclude(c.tags, tag))
675
- );
676
- });
677
- });
678
-
679
- describe('#setSpaceProperty()', () => {
680
- afterEach(() => {
681
- conversation = null;
682
- });
683
- describe('when the current user is a moderator', () => {
684
- it('sets announcement mode for the specified conversation', () =>
685
- webex.internal.conversation
686
- .assignModerator(conversation)
687
- .catch(allowConflicts)
688
- .then(() => webex.internal.conversation.lock(conversation))
689
- .catch(allowConflicts)
690
- .then(() =>
691
- webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
692
- )
693
- .then((activity) => {
694
- assert.isActivity(activity);
695
- })
696
- .then(() => webex.internal.conversation.get(conversation))
697
- .then((c) => assert.include(c.tags, 'ANNOUNCEMENT')));
698
- });
699
-
700
- describe('when the current user is not a moderator', () => {
701
- it('fails to set announcement mode for the specified conversation', () =>
702
- webex.internal.conversation
703
- .assignModerator(conversation)
704
- .catch(allowConflicts)
705
- .then(() => webex.internal.conversation.lock(conversation))
706
- .catch(allowConflicts)
707
- .then(() =>
708
- assert.isRejected(
709
- mccoy.webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
710
- )
711
- )
712
- .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
713
- });
714
- });
715
-
716
- describe('#unsetSpaceProperty()', () => {
717
- afterEach(() => {
718
- conversation = null;
719
- });
720
- describe('when the current user is a moderator', () => {
721
- it('unsets announcement mode for the specified conversation', () =>
722
- webex.internal.conversation
723
- .assignModerator(conversation)
724
- .catch(allowConflicts)
725
- .then(() => webex.internal.conversation.lock(conversation))
726
- .catch(allowConflicts)
727
- .then(() =>
728
- webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
729
- )
730
- .then((activity) => {
731
- assert.isActivity(activity);
732
- })
733
- .then(() =>
734
- webex.internal.conversation.unsetSpaceProperty(conversation, 'ANNOUNCEMENT')
735
- )
736
- .then(() => webex.internal.conversation.get(conversation))
737
- .then((c) => assert.notInclude(c.tags, 'ANNOUNCEMENT')));
738
- });
739
-
740
- describe('when the current user is not a moderator', () => {
741
- it('fails to unset announcement mode for the specified conversation', () =>
742
- webex.internal.conversation
743
- .assignModerator(conversation)
744
- .catch(allowConflicts)
745
- .then(() => webex.internal.conversation.lock(conversation))
746
- .catch(allowConflicts)
747
- .then(() =>
748
- webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
749
- )
750
- .then((activity) => {
751
- assert.isActivity(activity);
752
- })
753
- .then(() =>
754
- assert.isRejected(
755
- mccoy.webex.internal.conversation.unsetSpaceProperty(conversation, 'ANNOUNCEMENT')
756
- )
757
- )
758
- .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
759
- });
760
- });
761
-
762
- describe('#removeAllMuteTags()', () => {
763
- it('removes all mute tags on the convo', () =>
764
- webex.internal.conversation
765
- .muteMessages(conversation)
766
- .then(() => webex.internal.conversation.muteMentions(conversation))
767
- .then(() => webex.internal.conversation.removeAllMuteTags(conversation))
768
- .then(() => webex.internal.conversation.get(conversation))
769
- .then((c) => {
770
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
771
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
772
- assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
773
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
774
- }));
775
-
776
- it('removes all unmute tags on the convo', () =>
777
- webex.internal.conversation
778
- .unmuteMentions(conversation)
779
- .then(() => webex.internal.conversation.unmuteMessages(conversation))
780
- .then(() => webex.internal.conversation.removeAllMuteTags(conversation))
781
- .then(() => webex.internal.conversation.get(conversation))
782
- .then((c) => {
783
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
784
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
785
- assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
786
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
787
- }));
788
- });
789
-
790
- describe('#muteMentions()', () => {
791
- it('mutes the specified conversation of Mentions only', () =>
792
- webex.internal.conversation
793
- .muteMentions(conversation)
794
- .then(() => webex.internal.conversation.get(conversation))
795
- .then((c) => {
796
- assert.include(c.tags, 'MENTION_NOTIFICATIONS_OFF');
797
- assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
798
- }));
799
- });
800
-
801
- describe('#unmuteMentions()', () => {
802
- before(() => webex.internal.conversation.muteMentions(conversation));
803
-
804
- it('unmutes the specified conversation of Mentions', () =>
805
- webex.internal.conversation
806
- .unmuteMentions(conversation)
807
- .then(() => webex.internal.conversation.get(conversation))
808
- .then((c) => {
809
- assert.include(c.tags, 'MENTION_NOTIFICATIONS_ON');
810
- assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_OFF');
811
- }));
812
- });
813
-
814
- describe('#muteMessages()', () => {
815
- it('mutes the specified conversation of Messages only', () =>
816
- webex.internal.conversation
817
- .muteMessages(conversation)
818
- .then(() => webex.internal.conversation.get(conversation))
819
- .then((c) => {
820
- assert.include(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
821
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
822
- }));
823
- });
824
-
825
- describe('#unmuteMessages()', () => {
826
- before(() => webex.internal.conversation.muteMessages(conversation));
827
-
828
- it('unmutes the specified conversation of Messages only', () =>
829
- webex.internal.conversation
830
- .unmuteMessages(conversation)
831
- .then(() => webex.internal.conversation.get(conversation))
832
- .then((c) => {
833
- assert.include(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
834
- assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
835
- }));
836
- });
837
-
838
- describe('#ignore()', () => {
839
- it('ignores the specified conversation', () =>
840
- webex.internal.conversation
841
- .ignore(conversation)
842
- .then(() => webex.internal.conversation.get(conversation))
843
- .then((c) => {
844
- assert.include(c.tags, 'IGNORED');
845
- }));
846
- });
847
-
848
- describe('#unignore()', () => {
849
- before(() => webex.internal.conversation.ignore(conversation));
850
-
851
- it('ignores the specified conversation', () =>
852
- webex.internal.conversation
853
- .unignore(conversation)
854
- .then(() => webex.internal.conversation.get(conversation))
855
- .then((c) => {
856
- assert.notInclude(c.tags, 'IGNORED');
857
- }));
858
- });
859
- });
860
-
861
- describe('verbs that update objects', () => {
862
- let conversation;
863
-
864
- before(() => {
865
- if (!conversation) {
866
- return webex.internal.conversation
867
- .create({displayName: 'displayName', participants})
868
- .then((c) => {
869
- conversation = c;
870
- });
871
- }
872
-
873
- return Promise.resolve();
874
- });
875
-
876
- describe('#acknowledge()', () => {
877
- it('acknowledges the specified activity', () =>
878
- webex.internal.conversation
879
- .post(conversation, {displayName: 'A comment to acknowledge'})
880
- .then((activity) =>
881
- mccoy.webex.internal.conversation.acknowledge(conversation, activity)
882
- )
883
- .then((ack) =>
884
- webex.internal.conversation
885
- .get(conversation, {activitiesLimit: 1})
886
- .then((c) => assert.equal(c.activities.items[0].url, ack.object.url))
887
- ));
888
- });
889
-
890
- describe('#assignModerator()', () => {
891
- it('assigns a moderator to a conversation', () =>
892
- webex.internal.conversation
893
- .assignModerator(conversation, spock)
894
- .then(() =>
895
- webex.internal.conversation.get(conversation, {
896
- activitiesLimit: 5,
897
- includeParticipants: true,
898
- })
899
- )
900
- .then((c) => {
901
- const moderators = c.participants.items.filter(
902
- (p) => p.roomProperties && p.roomProperties.isModerator
903
- );
904
-
905
- assert.lengthOf(moderators, 1);
906
- assert.equal(moderators[0].id, spock.id);
907
- }));
908
- });
909
-
910
- describe('#delete()', () => {
911
- let sampleImageSmallOnePng = 'sample-image-small-one.png';
912
-
913
- before(() =>
914
- fh.fetch(sampleImageSmallOnePng).then((res) => {
915
- sampleImageSmallOnePng = res;
916
- })
917
- );
918
-
919
- it("deletes the current user's post", () =>
920
- webex.internal.conversation
921
- .post(conversation, {displayName: 'Delete Me 1'})
922
- .then((a) => webex.internal.conversation.delete(conversation, a))
923
- .then(() => new Promise((resolve) => setTimeout(resolve, 2000)))
924
- .then(() => webex.internal.conversation.get(conversation, {activitiesLimit: 2}))
925
- .then((c) => {
926
- assert.equal(c.activities.items[0].verb, 'tombstone');
927
- assert.equal(c.activities.items[1].verb, 'delete');
928
- }));
929
-
930
- it("deletes the current user's share", () =>
931
- webex.internal.conversation
932
- .share(conversation, [sampleImageSmallOnePng])
933
- .then((a) => webex.internal.conversation.delete(conversation, a))
934
- .then(() => new Promise((resolve) => setTimeout(resolve, 2000)))
935
- .then(() => webex.internal.conversation.get(conversation, {activitiesLimit: 2}))
936
- .then((c) => {
937
- assert.equal(c.activities.items[0].verb, 'tombstone');
938
- assert.equal(c.activities.items[1].verb, 'delete');
939
- }));
940
-
941
- describe('when the current user is a moderator', () => {
942
- it("deletes any user's content", () =>
943
- webex.internal.conversation
944
- .assignModerator(conversation)
945
- .catch(allowConflicts)
946
- .then(() => webex.internal.conversation.lock(conversation))
947
- .catch(allowConflicts)
948
- .then(() =>
949
- mccoy.webex.internal.conversation.post(conversation, {displayName: 'Delete Me 2'})
950
- )
951
- .then((a) => webex.internal.conversation.delete(conversation, a)));
952
- });
953
-
954
- describe('when the current user is not a moderator', () => {
955
- it("fails to delete other users' content", () =>
956
- webex.internal.conversation
957
- .assignModerator(conversation)
958
- .catch(allowConflicts)
959
- .then(() => webex.internal.conversation.lock(conversation))
960
- .catch(allowConflicts)
961
- .then(() =>
962
- webex.internal.conversation.post(conversation, {displayName: 'Delete Me 3'})
963
- )
964
- .then((a) =>
965
- assert.isRejected(mccoy.webex.internal.conversation.delete(conversation, a))
966
- )
967
- .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
968
- });
969
- });
970
-
971
- describe('#addReaction', () => {
972
- it('adds a reaction', () =>
973
- webex.internal.conversation
974
- .post(conversation, {displayName: 'React Me 1'})
975
- .then((activity) =>
976
- webex.internal.conversation.addReaction(conversation, 'smiley', activity)
977
- )
978
- .then((reaction) => {
979
- assert.equal(reaction.verb, 'add');
980
- assert.equal(reaction.object.objectType, 'reaction2');
981
- assert.equal(reaction.object.displayName, 'smiley');
982
- }));
983
- });
984
-
985
- describe('#deleteReaction', () => {
986
- it('deletes a reaction', () =>
987
- webex.internal.conversation
988
- .post(conversation, {displayName: 'React Me 1'})
989
- .then((activity) =>
990
- webex.internal.conversation.addReaction(conversation, 'smiley', activity)
991
- )
992
- .then((reaction) =>
993
- webex.internal.conversation.deleteReaction(conversation, reaction.id)
994
- )
995
- .then((deleteReaction) => {
996
- assert.equal(deleteReaction.verb, 'delete');
997
- assert.equal(deleteReaction.object.verb, 'tombstone');
998
- assert.equal(deleteReaction.object.parent.type, 'reaction');
999
- }));
1000
- });
1001
-
1002
- describe('#unassignModerator()', () => {
1003
- it('removes a moderator from a conversation', () =>
1004
- webex.internal.conversation
1005
- .assignModerator(conversation, spock)
1006
- .catch(allowConflicts)
1007
- .then(() => webex.internal.conversation.unassignModerator(conversation, spock))
1008
- .then(() =>
1009
- webex.internal.conversation.get(conversation, {
1010
- activitiesLimit: 5,
1011
- includeParticipants: true,
1012
- })
1013
- )
1014
- .then((c) => {
1015
- const moderators = c.participants.items.filter(
1016
- (p) => p.roomProperties && p.roomProperties.isModerator
1017
- );
1018
-
1019
- assert.lengthOf(moderators, 0);
1020
- }));
1021
- });
1022
-
1023
- describe('#update()', () => {
1024
- it('renames the specified conversation', () =>
1025
- webex.internal.conversation
1026
- .update(conversation, {
1027
- displayName: 'displayName2',
1028
- objectType: 'conversation',
1029
- })
1030
- .then((c) => webex.internal.conversation.get({url: c.target.url}))
1031
- .then((c) => assert.equal(c.displayName, 'displayName2')));
1032
- });
1033
- });
1034
- });
1035
- });
1036
-
1037
- function allowConflicts(reason) {
1038
- if (!(reason instanceof WebexHttpError.BadRequest)) {
1039
- throw reason;
1040
- }
1041
- }
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import '@webex/internal-plugin-conversation';
6
+
7
+ import {patterns} from '@webex/common';
8
+ import WebexCore, {WebexHttpError} from '@webex/webex-core';
9
+ import {assert} from '@webex/test-helper-chai';
10
+ import testUsers from '@webex/test-helper-test-users';
11
+ import {find, map} from 'lodash';
12
+ import uuid from 'uuid';
13
+ import fh from '@webex/test-helper-file';
14
+ import {skipInNode} from '@webex/test-helper-mocha';
15
+
16
+ describe('plugin-conversation', function () {
17
+ this.timeout(30000);
18
+ describe('verbs', () => {
19
+ let checkov, mccoy, participants, webex, spock;
20
+
21
+ before(() =>
22
+ testUsers.create({count: 3}).then(async (users) => {
23
+ [spock, mccoy, checkov] = participants = users;
24
+
25
+ // Pause for 5 seconds for CI
26
+ await new Promise((done) => setTimeout(done, 5000));
27
+
28
+ webex = new WebexCore({
29
+ credentials: {
30
+ authorization: spock.token,
31
+ },
32
+ });
33
+
34
+ mccoy.webex = new WebexCore({
35
+ credentials: {
36
+ authorization: mccoy.token,
37
+ },
38
+ });
39
+
40
+ return Promise.all([
41
+ webex.internal.mercury.connect(),
42
+ mccoy.webex.internal.mercury.connect(),
43
+ ]);
44
+ })
45
+ );
46
+
47
+ after(() =>
48
+ Promise.all([
49
+ webex && webex.internal.mercury.disconnect(),
50
+ mccoy && mccoy.webex.internal.mercury.disconnect(),
51
+ ])
52
+ );
53
+
54
+ function makeEmailAddress() {
55
+ return `webex-js-sdk--test-${uuid.v4()}@example.com`;
56
+ }
57
+
58
+ let conversation;
59
+
60
+ beforeEach(() => {
61
+ if (conversation) {
62
+ return Promise.resolve();
63
+ }
64
+
65
+ return webex.internal.conversation.create({participants}).then((c) => {
66
+ conversation = c;
67
+ });
68
+ });
69
+
70
+ describe('#add()', () => {
71
+ let email;
72
+
73
+ beforeEach(() => {
74
+ email = makeEmailAddress();
75
+ });
76
+
77
+ beforeEach(() =>
78
+ webex.internal.conversation
79
+ .create({participants: [checkov]}, {forceGrouped: true})
80
+ .then((c) => {
81
+ conversation = c;
82
+ })
83
+ );
84
+
85
+ it('adds the specified user to the specified conversation', () =>
86
+ webex.internal.conversation.add(conversation, mccoy).then((activity) => {
87
+ assert.isActivity(activity);
88
+ assert.property(activity, 'kmsMessage');
89
+ }));
90
+
91
+ it("grants the specified user access to the conversation's key", () =>
92
+ webex.internal.conversation
93
+ .post(conversation, {displayName: 'PROOF!'})
94
+ .then(() => webex.internal.conversation.add(conversation, mccoy))
95
+ .then(() => mccoy.webex.internal.conversation.get(conversation, {activitiesLimit: 10}))
96
+ .then((c) => {
97
+ assert.isConversation(c);
98
+ const activity = find(c.activities.items, {verb: 'post'});
99
+
100
+ assert.equal(activity.object.displayName, 'PROOF!');
101
+ }));
102
+
103
+ // TODO: Issues with side boarding users too soon. Skipping until it's fixed
104
+ it.skip('sideboards a non-existent user', () =>
105
+ webex.internal.conversation
106
+ .add(conversation, email)
107
+ .then((activity) => {
108
+ assert.isActivity(activity);
109
+
110
+ return webex.internal.conversation.get(conversation, {includeParticipants: true});
111
+ })
112
+ .then((c) => {
113
+ assert.isConversation(c);
114
+ const participant = find(c.participants.items, {emailAddress: email});
115
+
116
+ assert.include(participant.tags, 'SIDE_BOARDED');
117
+ assert.match(participant.id, patterns.uuid);
118
+ }));
119
+ });
120
+
121
+ describe('#assign()', () => {
122
+ before(() =>
123
+ webex.internal.conversation.create({participants}).then((c) => {
124
+ conversation = c;
125
+ })
126
+ );
127
+
128
+ let sampleImageSmallOnePng = 'sample-image-small-one.png';
129
+
130
+ before(() =>
131
+ fh.fetch(sampleImageSmallOnePng).then((res) => {
132
+ sampleImageSmallOnePng = res;
133
+ })
134
+ );
135
+
136
+ it('assigns an avatar to a room', () =>
137
+ webex.internal.conversation
138
+ .assign(conversation, sampleImageSmallOnePng)
139
+ .then(() => webex.internal.conversation.get(conversation))
140
+ .then((c) => {
141
+ assert.property(c, 'avatar');
142
+
143
+ assert.property(c.avatar, 'files');
144
+ assert.property(c.avatar.files, 'items');
145
+ assert.lengthOf(c.avatar.files.items, 1);
146
+ assert.property(c.avatar.files.items[0], 'fileSize');
147
+ assert.property(c.avatar.files.items[0], 'mimeType');
148
+ assert.property(c.avatar.files.items[0], 'objectType');
149
+ assert.property(c.avatar.files.items[0], 'scr');
150
+ assert.property(c.avatar.files.items[0], 'url');
151
+ assert.equal(c.avatar.objectType, 'content');
152
+
153
+ assert.isString(c.avatarEncryptionKeyUrl);
154
+ assert.isObject(c.avatar.files.items[0].scr, 'The scr was decrypted');
155
+ assert.equal(c.avatar.files.items[0].displayName, 'sample-image-small-one.png');
156
+
157
+ assert.property(c, 'avatarEncryptionKeyUrl');
158
+ }));
159
+ });
160
+
161
+ describe('#leave()', () => {
162
+ afterEach(() => {
163
+ conversation = null;
164
+ });
165
+
166
+ it('removes the current user from the specified conversation', () =>
167
+ webex.internal.conversation
168
+ .leave(conversation)
169
+ .then((activity) => {
170
+ assert.isActivity(activity);
171
+
172
+ return assert.isRejected(webex.internal.conversation.get(conversation));
173
+ })
174
+ .then((reason) => {
175
+ assert.statusCode(reason, 404);
176
+
177
+ return assert.isRejected(
178
+ webex.internal.encryption.kms.fetchKey({
179
+ uri: conversation.defaultActivityEncryptionKeyUrl,
180
+ })
181
+ );
182
+ })
183
+ .then((reason) => assert.equal(reason.status, 403)));
184
+
185
+ it('removes the specified user from the specified conversation', () =>
186
+ webex.internal.conversation
187
+ .leave(conversation, mccoy)
188
+ .then((activity) => {
189
+ assert.isActivity(activity);
190
+
191
+ return assert.isRejected(mccoy.webex.internal.conversation.get(conversation));
192
+ })
193
+ .then((reason) => {
194
+ assert.statusCode(reason, 404);
195
+
196
+ return assert.isRejected(
197
+ mccoy.webex.internal.encryption.kms.fetchKey({
198
+ uri: conversation.defaultActivityEncryptionKeyUrl,
199
+ })
200
+ );
201
+ })
202
+ .then((reason) => assert.equal(reason.status, 403)));
203
+
204
+ describe('with deleted users', () => {
205
+ let redshirt;
206
+
207
+ beforeEach(() =>
208
+ testUsers.create({count: 1}).then(([rs]) => {
209
+ redshirt = rs;
210
+
211
+ return webex.internal.conversation.add(conversation, rs);
212
+ })
213
+ );
214
+
215
+ it('removes the specified deleted user from the specified conversation', () =>
216
+ webex.internal.conversation
217
+ .leave(conversation, redshirt)
218
+ .then(() => webex.internal.conversation.get(conversation, {includeParticipants: true}))
219
+ .then((c) => {
220
+ assert.lengthOf(c.participants.items, 3);
221
+ assert.notInclude(map(c.participants.items, 'id'), redshirt.id);
222
+ }));
223
+ });
224
+ });
225
+
226
+ describe('#leave() with id only', () => {
227
+ afterEach(() => {
228
+ conversation = null;
229
+ });
230
+
231
+ it('removes the current user by id only', () =>
232
+ webex.internal.conversation
233
+ .leave({
234
+ id: conversation.id,
235
+ defaultActivityEncryptionKeyUrl: conversation.defaultActivityEncryptionKeyUrl,
236
+ kmsResourceObjectUrl: conversation.kmsResourceObjectUrl,
237
+ })
238
+ .then((activity) => {
239
+ assert.isActivity(activity);
240
+
241
+ return assert.isRejected(webex.internal.conversation.get(conversation));
242
+ })
243
+ .then((reason) => {
244
+ assert.statusCode(reason, 404);
245
+
246
+ return assert.isRejected(
247
+ webex.internal.encryption.kms.fetchKey({
248
+ uri: conversation.defaultActivityEncryptionKeyUrl,
249
+ })
250
+ );
251
+ })
252
+ .then((reason) => assert.equal(reason.status, 403)));
253
+ });
254
+
255
+ describe('#post()', () => {
256
+ let message, richMessage;
257
+
258
+ beforeEach(() => {
259
+ message = 'mccoy, THIS IS A TEST MESSAGE';
260
+ richMessage = `<webex-mention data-object-id="${mccoy.id}" data-object-type="person">mccoy</webex-mention>, THIS IS A TEST MESSAGE`;
261
+ });
262
+
263
+ // disable until helper-html has node support
264
+ skipInNode(describe)('when there are html tags in rich messages', () => {
265
+ const allTagsUsedInThisTest = {
266
+ div: [],
267
+ b: [],
268
+ span: [],
269
+ };
270
+
271
+ [
272
+ {
273
+ it: 'allows allowed outbound and inbound tags',
274
+ allowedOutboundTags: {div: []},
275
+ allowedInboundTags: {div: []},
276
+ outboundMessage: '<div>HELLO</div>',
277
+ outboundFilteredMessage: '<div>HELLO</div>',
278
+ inboundMessage: '<div>HELLO</div>',
279
+ },
280
+ {
281
+ it: 'filters and escapes disallowed outbound tags',
282
+ allowedOutboundTags: {},
283
+ allowedInboundTags: {},
284
+ outboundMessage: '<div><b>HELLO</b></div>',
285
+ outboundFilteredMessage: '&lt;div&gt;&lt;b&gt;HELLO&lt;/b&gt;&lt;/div&gt;',
286
+ inboundMessage: '&lt;div&gt;&lt;b&gt;HELLO&lt;/b&gt;&lt;/div&gt;',
287
+ },
288
+ {
289
+ it: 'filters disallowed inbound tags',
290
+ allowedOutboundTags: {div: [], b: []},
291
+ allowedInboundTags: {b: []},
292
+ outboundMessage: '<div><b>HELLO</b></div>',
293
+ outboundFilteredMessage: '<div><b>HELLO</b></div>',
294
+ inboundMessage: '<b>HELLO</b>',
295
+ },
296
+ {
297
+ it: 'filters and escapes the correct outbound tags',
298
+ allowedOutboundTags: {div: [], span: []},
299
+ allowedInboundTags: {},
300
+ outboundMessage: "<div><b>HELLO</b><span> it's me</span></div>",
301
+ outboundFilteredMessage: "<div>&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span></div>",
302
+ inboundMessage: "&lt;b&gt;HELLO&lt;/b&gt; it's me",
303
+ },
304
+ {
305
+ it: 'filters the correct inbound tags and filters and escapes the correct outbound tags',
306
+ allowedOutboundTags: {div: [], span: []},
307
+ allowedInboundTags: {span: []},
308
+ outboundMessage: "<div><b>HELLO</b><span> it's me</span></div>",
309
+ outboundFilteredMessage: "<div>&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span></div>",
310
+ inboundMessage: "&lt;b&gt;HELLO&lt;/b&gt;<span> it's me</span>",
311
+ },
312
+ ].forEach((def) => {
313
+ it(def.it, () => {
314
+ webex.config.conversation.allowedOutboundTags = def.allowedOutboundTags;
315
+ // since responses to spock's post will count as 'inbound', we
316
+ // enable all the tags for allowedInboundTags so that we know
317
+ // the message is filtered by only the outbound rules
318
+ webex.config.conversation.allowedInboundTags = allTagsUsedInThisTest;
319
+
320
+ return webex.internal.conversation
321
+ .post(conversation, {
322
+ displayName: message,
323
+ content: def.outboundMessage,
324
+ })
325
+ .then((activity) => {
326
+ assert.equal(activity.object.content, def.outboundFilteredMessage);
327
+ mccoy.webex.config.conversation.allowedInboundTags = def.allowedInboundTags;
328
+
329
+ return mccoy.webex.internal.conversation.get(conversation, {activitiesLimit: 1});
330
+ })
331
+ .then((convo) => {
332
+ // check latest message
333
+ const activity = find(convo.activities.items, {verb: 'post'});
334
+
335
+ assert.equal(activity.object.content, def.inboundMessage);
336
+ });
337
+ });
338
+ });
339
+ });
340
+
341
+ describe('when encryption key need to rotate', () => {
342
+ it('can post a plain text successfully', () => {
343
+ const {defaultActivityEncryptionKeyUrl} = conversation;
344
+
345
+ Promise.all([
346
+ webex
347
+ .request({
348
+ method: 'GET',
349
+ api: 'conversation',
350
+ resource: `/conversations/${conversation.id}/healed?resetKey=true`,
351
+ })
352
+ .then(() =>
353
+ webex.request({
354
+ method: 'GET',
355
+ api: 'conversation',
356
+ resource: `conversations/${conversation.id}`,
357
+ })
358
+ ),
359
+ ])
360
+ .then((res) => {
361
+ res.body.defaultActivityEncryptionKeyUrl = defaultActivityEncryptionKeyUrl;
362
+
363
+ return webex.internal.conversation.post(res.body, message);
364
+ })
365
+ .then((res) => {
366
+ assert.notEqual(
367
+ res.body.defaultActivityEncryptionKeyUrl,
368
+ defaultActivityEncryptionKeyUrl
369
+ );
370
+ });
371
+ });
372
+ });
373
+
374
+ it('posts a comment to the specified conversation', () =>
375
+ webex.internal.conversation.post(conversation, message).then((activity) => {
376
+ assert.isActivity(activity);
377
+
378
+ assert.isEncryptedActivity(activity);
379
+ assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
380
+
381
+ assert.equal(activity.object.displayName, message);
382
+ }));
383
+
384
+ it("updates the specified conversation's unread status", () =>
385
+ mccoy.webex.internal.conversation.get(conversation).then((c) => {
386
+ const {lastSeenActivityDate, lastReadableActivityDate} = c;
387
+
388
+ return webex.internal.conversation.post(conversation, message).then(() =>
389
+ mccoy.webex.internal.conversation.get(conversation).then((c2) => {
390
+ assert.equal(c2.lastSeenActivityDate, lastSeenActivityDate);
391
+ assert.isAbove(
392
+ Date.parse(c2.lastReadableActivityDate),
393
+ Date.parse(lastReadableActivityDate)
394
+ );
395
+ })
396
+ );
397
+ }));
398
+
399
+ it('posts rich content to the specified conversation', () =>
400
+ webex.internal.conversation
401
+ .post(conversation, {
402
+ displayName: message,
403
+ content: richMessage,
404
+ })
405
+ .then((activity) => {
406
+ assert.isActivity(activity);
407
+
408
+ assert.isEncryptedActivity(activity);
409
+ assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
410
+
411
+ assert.equal(activity.object.displayName, message);
412
+ assert.equal(activity.object.content, richMessage);
413
+ }));
414
+
415
+ it('submits mentions to the specified conversation', () =>
416
+ webex.internal.conversation
417
+ .post(conversation, {
418
+ displayName: message,
419
+ content: richMessage,
420
+ mentions: {
421
+ items: [
422
+ {
423
+ id: mccoy.id,
424
+ objectType: 'person',
425
+ },
426
+ ],
427
+ },
428
+ })
429
+ .then((activity) => {
430
+ assert.isActivity(activity);
431
+
432
+ assert.isEncryptedActivity(activity);
433
+ assert.equal(activity.encryptionKeyUrl, conversation.defaultActivityEncryptionKeyUrl);
434
+
435
+ assert.equal(activity.object.displayName, message);
436
+ assert.equal(activity.object.content, richMessage);
437
+
438
+ assert.isDefined(activity.object.mentions);
439
+ assert.isDefined(activity.object.mentions.items);
440
+ assert.lengthOf(activity.object.mentions.items, 1);
441
+ assert.equal(activity.object.mentions.items[0].id, mccoy.id);
442
+ }));
443
+ });
444
+
445
+ describe('#update()', () => {
446
+ it('renames the specified conversation', () =>
447
+ webex.internal.conversation
448
+ .update(conversation, {
449
+ displayName: 'displayName2',
450
+ objectType: 'conversation',
451
+ })
452
+ .then((c) => webex.internal.conversation.get({url: c.target.url}))
453
+ .then((c) => assert.equal(c.displayName, 'displayName2')));
454
+ });
455
+
456
+ describe('#unassign()', () => {
457
+ before(() =>
458
+ webex.internal.conversation.create({participants}).then((c) => {
459
+ conversation = c;
460
+ })
461
+ );
462
+
463
+ let sampleImageSmallOnePng = 'sample-image-small-one.png';
464
+
465
+ before(() =>
466
+ fh.fetch(sampleImageSmallOnePng).then((res) => {
467
+ sampleImageSmallOnePng = res;
468
+ })
469
+ );
470
+
471
+ beforeEach(() => webex.internal.conversation.assign(conversation, sampleImageSmallOnePng));
472
+
473
+ it('unassigns an avatar from a room', () =>
474
+ webex.internal.conversation.unassign(conversation).then(() =>
475
+ webex.internal.conversation.get(conversation).then((c) => {
476
+ assert.notProperty(c, 'avatar');
477
+ assert.notProperty(c, 'avatarEncryptionKeyUrl');
478
+ })
479
+ ));
480
+ });
481
+
482
+ describe('#updateKey()', () => {
483
+ beforeEach(() =>
484
+ webex.internal.conversation
485
+ .create({participants, comment: 'THIS IS A COMMENT'})
486
+ .then((c) => {
487
+ conversation = c;
488
+ })
489
+ );
490
+
491
+ it('assigns an unused key to the specified conversation', () =>
492
+ webex.internal.conversation
493
+ .updateKey(conversation)
494
+ .then((activity) => {
495
+ assert.isActivity(activity);
496
+
497
+ return webex.internal.conversation.get(conversation);
498
+ })
499
+ .then((c) => {
500
+ assert.isDefined(c.defaultActivityEncryptionKeyUrl);
501
+ assert.notEqual(
502
+ c.defaultActivityEncryptionKeyUrl,
503
+ conversation.defaultActivityEncryptionKeyUrl
504
+ );
505
+ }));
506
+
507
+ it('assigns the specified key to the specified conversation', () =>
508
+ webex.internal.encryption.kms.createUnboundKeys({count: 1}).then(([key]) =>
509
+ webex.internal.conversation
510
+ .updateKey(conversation, key)
511
+ .then((activity) => {
512
+ assert.isActivity(activity);
513
+ assert.equal(activity.object.defaultActivityEncryptionKeyUrl, key.uri);
514
+
515
+ return webex.internal.conversation.get(conversation);
516
+ })
517
+ .then((c) => {
518
+ assert.isDefined(c.defaultActivityEncryptionKeyUrl);
519
+ assert.notEqual(
520
+ c.defaultActivityEncryptionKeyUrl,
521
+ conversation.defaultActivityEncryptionKeyUrl
522
+ );
523
+ })
524
+ ));
525
+
526
+ it('grants access to the key for all users in the conversation', () =>
527
+ webex.internal.conversation
528
+ .updateKey(conversation)
529
+ .then((activity) => {
530
+ assert.isActivity(activity);
531
+
532
+ return mccoy.webex.internal.conversation.get({
533
+ url: conversation.url,
534
+ participantsLimit: 0,
535
+ activitiesLimit: 0,
536
+ });
537
+ })
538
+ .then((c) => {
539
+ assert.isDefined(c.defaultActivityEncryptionKeyUrl);
540
+ assert.notEqual(
541
+ c.defaultActivityEncryptionKeyUrl,
542
+ conversation.defaultActivityEncryptionKeyUrl
543
+ );
544
+
545
+ return mccoy.webex.internal.encryption.kms.fetchKey({
546
+ uri: c.defaultActivityEncryptionKeyUrl,
547
+ });
548
+ }));
549
+ });
550
+
551
+ describe('#updateTypingStatus()', () => {
552
+ beforeEach(() =>
553
+ webex.internal.conversation
554
+ .create({participants, comment: 'THIS IS A COMMENT'})
555
+ .then((c) => {
556
+ conversation = c;
557
+ })
558
+ );
559
+
560
+ it('sets the typing indicator for the specified conversation', () =>
561
+ webex.internal.conversation
562
+ .updateTypingStatus(conversation, {typing: true})
563
+ .then(({statusCode}) => {
564
+ assert.equal(statusCode, 204);
565
+ }));
566
+
567
+ it('clears the typing indicator for the specified conversation', () =>
568
+ webex.internal.conversation
569
+ .updateTypingStatus(conversation, {typing: false})
570
+ .then(({statusCode}) => {
571
+ assert.equal(statusCode, 204);
572
+ }));
573
+
574
+ it('fails if called with a bad conversation object', () => {
575
+ let error;
576
+
577
+ return webex.internal.conversation
578
+ .updateTypingStatus({}, {typing: false})
579
+ .catch((reason) => {
580
+ error = reason;
581
+ })
582
+ .then(() => {
583
+ assert.isDefined(error);
584
+ });
585
+ });
586
+
587
+ it('infers id from conversation url if missing', () => {
588
+ Reflect.deleteProperty(conversation, 'id');
589
+
590
+ return webex.internal.conversation
591
+ .updateTypingStatus(conversation, {typing: true})
592
+ .then(({statusCode}) => {
593
+ assert.equal(statusCode, 204);
594
+ });
595
+ });
596
+ });
597
+
598
+ describe('verbs that update conversation tags', () => {
599
+ [
600
+ {
601
+ itString: 'favorites the specified conversation',
602
+ tag: 'FAVORITE',
603
+ verb: 'favorite',
604
+ },
605
+ {
606
+ itString: 'hides the specified conversation',
607
+ tag: 'HIDDEN',
608
+ verb: 'hide',
609
+ },
610
+ {
611
+ itString: 'locks the specified conversation',
612
+ tag: 'LOCKED',
613
+ verb: 'lock',
614
+ },
615
+ {
616
+ itString: 'mutes the specified conversation',
617
+ tag: 'MUTED',
618
+ verb: 'mute',
619
+ },
620
+ ].forEach(({tag, verb, itString}) => {
621
+ describe(`#${verb}()`, () => {
622
+ it(itString, () =>
623
+ webex.internal.conversation[verb](conversation)
624
+ .then((activity) => {
625
+ assert.isActivity(activity);
626
+ })
627
+ .then(() => webex.internal.conversation.get(conversation))
628
+ .then((c) => assert.include(c.tags, tag))
629
+ );
630
+ });
631
+ });
632
+
633
+ [
634
+ {
635
+ itString: 'unfavorites the specified conversation',
636
+ setupVerb: 'favorite',
637
+ tag: 'FAVORITE',
638
+ verb: 'unfavorite',
639
+ },
640
+ {
641
+ itString: 'unhides the specified conversation',
642
+ setupVerb: 'hide',
643
+ tag: 'HIDDEN',
644
+ verb: 'unhide',
645
+ },
646
+ {
647
+ itString: 'unlocks the specified conversation',
648
+ setupVerb: 'lock',
649
+ tag: 'LOCKED',
650
+ verb: 'unlock',
651
+ },
652
+ {
653
+ itString: 'unmutes the specified conversation',
654
+ setupVerb: 'mute',
655
+ tag: 'MUTED',
656
+ verb: 'unmute',
657
+ },
658
+ ].forEach(({tag, verb, itString, setupVerb}) => {
659
+ describe(`#${verb}()`, () => {
660
+ beforeEach(() =>
661
+ webex.internal.conversation[setupVerb](conversation).catch((reason) => {
662
+ if (reason.statusCode !== 403) {
663
+ throw reason;
664
+ }
665
+ })
666
+ );
667
+
668
+ it(itString, () =>
669
+ webex.internal.conversation[verb](conversation)
670
+ .then((activity) => {
671
+ assert.isActivity(activity);
672
+ })
673
+ .then(() => webex.internal.conversation.get(conversation))
674
+ .then((c) => assert.notInclude(c.tags, tag))
675
+ );
676
+ });
677
+ });
678
+
679
+ describe('#setSpaceProperty()', () => {
680
+ afterEach(() => {
681
+ conversation = null;
682
+ });
683
+ describe('when the current user is a moderator', () => {
684
+ it('sets announcement mode for the specified conversation', () =>
685
+ webex.internal.conversation
686
+ .assignModerator(conversation)
687
+ .catch(allowConflicts)
688
+ .then(() => webex.internal.conversation.lock(conversation))
689
+ .catch(allowConflicts)
690
+ .then(() =>
691
+ webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
692
+ )
693
+ .then((activity) => {
694
+ assert.isActivity(activity);
695
+ })
696
+ .then(() => webex.internal.conversation.get(conversation))
697
+ .then((c) => assert.include(c.tags, 'ANNOUNCEMENT')));
698
+ });
699
+
700
+ describe('when the current user is not a moderator', () => {
701
+ it('fails to set announcement mode for the specified conversation', () =>
702
+ webex.internal.conversation
703
+ .assignModerator(conversation)
704
+ .catch(allowConflicts)
705
+ .then(() => webex.internal.conversation.lock(conversation))
706
+ .catch(allowConflicts)
707
+ .then(() =>
708
+ assert.isRejected(
709
+ mccoy.webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
710
+ )
711
+ )
712
+ .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
713
+ });
714
+ });
715
+
716
+ describe('#unsetSpaceProperty()', () => {
717
+ afterEach(() => {
718
+ conversation = null;
719
+ });
720
+ describe('when the current user is a moderator', () => {
721
+ it('unsets announcement mode for the specified conversation', () =>
722
+ webex.internal.conversation
723
+ .assignModerator(conversation)
724
+ .catch(allowConflicts)
725
+ .then(() => webex.internal.conversation.lock(conversation))
726
+ .catch(allowConflicts)
727
+ .then(() =>
728
+ webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
729
+ )
730
+ .then((activity) => {
731
+ assert.isActivity(activity);
732
+ })
733
+ .then(() =>
734
+ webex.internal.conversation.unsetSpaceProperty(conversation, 'ANNOUNCEMENT')
735
+ )
736
+ .then(() => webex.internal.conversation.get(conversation))
737
+ .then((c) => assert.notInclude(c.tags, 'ANNOUNCEMENT')));
738
+ });
739
+
740
+ describe('when the current user is not a moderator', () => {
741
+ it('fails to unset announcement mode for the specified conversation', () =>
742
+ webex.internal.conversation
743
+ .assignModerator(conversation)
744
+ .catch(allowConflicts)
745
+ .then(() => webex.internal.conversation.lock(conversation))
746
+ .catch(allowConflicts)
747
+ .then(() =>
748
+ webex.internal.conversation.setSpaceProperty(conversation, 'ANNOUNCEMENT')
749
+ )
750
+ .then((activity) => {
751
+ assert.isActivity(activity);
752
+ })
753
+ .then(() =>
754
+ assert.isRejected(
755
+ mccoy.webex.internal.conversation.unsetSpaceProperty(conversation, 'ANNOUNCEMENT')
756
+ )
757
+ )
758
+ .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
759
+ });
760
+ });
761
+
762
+ describe('#removeAllMuteTags()', () => {
763
+ it('removes all mute tags on the convo', () =>
764
+ webex.internal.conversation
765
+ .muteMessages(conversation)
766
+ .then(() => webex.internal.conversation.muteMentions(conversation))
767
+ .then(() => webex.internal.conversation.removeAllMuteTags(conversation))
768
+ .then(() => webex.internal.conversation.get(conversation))
769
+ .then((c) => {
770
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
771
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
772
+ assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
773
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
774
+ }));
775
+
776
+ it('removes all unmute tags on the convo', () =>
777
+ webex.internal.conversation
778
+ .unmuteMentions(conversation)
779
+ .then(() => webex.internal.conversation.unmuteMessages(conversation))
780
+ .then(() => webex.internal.conversation.removeAllMuteTags(conversation))
781
+ .then(() => webex.internal.conversation.get(conversation))
782
+ .then((c) => {
783
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
784
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
785
+ assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
786
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
787
+ }));
788
+ });
789
+
790
+ describe('#muteMentions()', () => {
791
+ it('mutes the specified conversation of Mentions only', () =>
792
+ webex.internal.conversation
793
+ .muteMentions(conversation)
794
+ .then(() => webex.internal.conversation.get(conversation))
795
+ .then((c) => {
796
+ assert.include(c.tags, 'MENTION_NOTIFICATIONS_OFF');
797
+ assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_ON');
798
+ }));
799
+ });
800
+
801
+ describe('#unmuteMentions()', () => {
802
+ before(() => webex.internal.conversation.muteMentions(conversation));
803
+
804
+ it('unmutes the specified conversation of Mentions', () =>
805
+ webex.internal.conversation
806
+ .unmuteMentions(conversation)
807
+ .then(() => webex.internal.conversation.get(conversation))
808
+ .then((c) => {
809
+ assert.include(c.tags, 'MENTION_NOTIFICATIONS_ON');
810
+ assert.notInclude(c.tags, 'MENTION_NOTIFICATIONS_OFF');
811
+ }));
812
+ });
813
+
814
+ describe('#muteMessages()', () => {
815
+ it('mutes the specified conversation of Messages only', () =>
816
+ webex.internal.conversation
817
+ .muteMessages(conversation)
818
+ .then(() => webex.internal.conversation.get(conversation))
819
+ .then((c) => {
820
+ assert.include(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
821
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
822
+ }));
823
+ });
824
+
825
+ describe('#unmuteMessages()', () => {
826
+ before(() => webex.internal.conversation.muteMessages(conversation));
827
+
828
+ it('unmutes the specified conversation of Messages only', () =>
829
+ webex.internal.conversation
830
+ .unmuteMessages(conversation)
831
+ .then(() => webex.internal.conversation.get(conversation))
832
+ .then((c) => {
833
+ assert.include(c.tags, 'MESSAGE_NOTIFICATIONS_ON');
834
+ assert.notInclude(c.tags, 'MESSAGE_NOTIFICATIONS_OFF');
835
+ }));
836
+ });
837
+
838
+ describe('#ignore()', () => {
839
+ it('ignores the specified conversation', () =>
840
+ webex.internal.conversation
841
+ .ignore(conversation)
842
+ .then(() => webex.internal.conversation.get(conversation))
843
+ .then((c) => {
844
+ assert.include(c.tags, 'IGNORED');
845
+ }));
846
+ });
847
+
848
+ describe('#unignore()', () => {
849
+ before(() => webex.internal.conversation.ignore(conversation));
850
+
851
+ it('ignores the specified conversation', () =>
852
+ webex.internal.conversation
853
+ .unignore(conversation)
854
+ .then(() => webex.internal.conversation.get(conversation))
855
+ .then((c) => {
856
+ assert.notInclude(c.tags, 'IGNORED');
857
+ }));
858
+ });
859
+ });
860
+
861
+ describe('verbs that update objects', () => {
862
+ let conversation;
863
+
864
+ before(() => {
865
+ if (!conversation) {
866
+ return webex.internal.conversation
867
+ .create({displayName: 'displayName', participants})
868
+ .then((c) => {
869
+ conversation = c;
870
+ });
871
+ }
872
+
873
+ return Promise.resolve();
874
+ });
875
+
876
+ describe('#acknowledge()', () => {
877
+ it('acknowledges the specified activity', () =>
878
+ webex.internal.conversation
879
+ .post(conversation, {displayName: 'A comment to acknowledge'})
880
+ .then((activity) =>
881
+ mccoy.webex.internal.conversation.acknowledge(conversation, activity)
882
+ )
883
+ .then((ack) =>
884
+ webex.internal.conversation
885
+ .get(conversation, {activitiesLimit: 1})
886
+ .then((c) => assert.equal(c.activities.items[0].url, ack.object.url))
887
+ ));
888
+ });
889
+
890
+ describe('#assignModerator()', () => {
891
+ it('assigns a moderator to a conversation', () =>
892
+ webex.internal.conversation
893
+ .assignModerator(conversation, spock)
894
+ .then(() =>
895
+ webex.internal.conversation.get(conversation, {
896
+ activitiesLimit: 5,
897
+ includeParticipants: true,
898
+ })
899
+ )
900
+ .then((c) => {
901
+ const moderators = c.participants.items.filter(
902
+ (p) => p.roomProperties && p.roomProperties.isModerator
903
+ );
904
+
905
+ assert.lengthOf(moderators, 1);
906
+ assert.equal(moderators[0].id, spock.id);
907
+ }));
908
+ });
909
+
910
+ describe('#delete()', () => {
911
+ let sampleImageSmallOnePng = 'sample-image-small-one.png';
912
+
913
+ before(() =>
914
+ fh.fetch(sampleImageSmallOnePng).then((res) => {
915
+ sampleImageSmallOnePng = res;
916
+ })
917
+ );
918
+
919
+ it("deletes the current user's post", () =>
920
+ webex.internal.conversation
921
+ .post(conversation, {displayName: 'Delete Me 1'})
922
+ .then((a) => webex.internal.conversation.delete(conversation, a))
923
+ .then(() => new Promise((resolve) => setTimeout(resolve, 2000)))
924
+ .then(() => webex.internal.conversation.get(conversation, {activitiesLimit: 2}))
925
+ .then((c) => {
926
+ assert.equal(c.activities.items[0].verb, 'tombstone');
927
+ assert.equal(c.activities.items[1].verb, 'delete');
928
+ }));
929
+
930
+ it("deletes the current user's share", () =>
931
+ webex.internal.conversation
932
+ .share(conversation, [sampleImageSmallOnePng])
933
+ .then((a) => webex.internal.conversation.delete(conversation, a))
934
+ .then(() => new Promise((resolve) => setTimeout(resolve, 2000)))
935
+ .then(() => webex.internal.conversation.get(conversation, {activitiesLimit: 2}))
936
+ .then((c) => {
937
+ assert.equal(c.activities.items[0].verb, 'tombstone');
938
+ assert.equal(c.activities.items[1].verb, 'delete');
939
+ }));
940
+
941
+ describe('when the current user is a moderator', () => {
942
+ it("deletes any user's content", () =>
943
+ webex.internal.conversation
944
+ .assignModerator(conversation)
945
+ .catch(allowConflicts)
946
+ .then(() => webex.internal.conversation.lock(conversation))
947
+ .catch(allowConflicts)
948
+ .then(() =>
949
+ mccoy.webex.internal.conversation.post(conversation, {displayName: 'Delete Me 2'})
950
+ )
951
+ .then((a) => webex.internal.conversation.delete(conversation, a)));
952
+ });
953
+
954
+ describe('when the current user is not a moderator', () => {
955
+ it("fails to delete other users' content", () =>
956
+ webex.internal.conversation
957
+ .assignModerator(conversation)
958
+ .catch(allowConflicts)
959
+ .then(() => webex.internal.conversation.lock(conversation))
960
+ .catch(allowConflicts)
961
+ .then(() =>
962
+ webex.internal.conversation.post(conversation, {displayName: 'Delete Me 3'})
963
+ )
964
+ .then((a) =>
965
+ assert.isRejected(mccoy.webex.internal.conversation.delete(conversation, a))
966
+ )
967
+ .then((reason) => assert.instanceOf(reason, WebexHttpError.Forbidden)));
968
+ });
969
+ });
970
+
971
+ describe('#addReaction', () => {
972
+ it('adds a reaction', () =>
973
+ webex.internal.conversation
974
+ .post(conversation, {displayName: 'React Me 1'})
975
+ .then((activity) =>
976
+ webex.internal.conversation.addReaction(conversation, 'smiley', activity)
977
+ )
978
+ .then((reaction) => {
979
+ assert.equal(reaction.verb, 'add');
980
+ assert.equal(reaction.object.objectType, 'reaction2');
981
+ assert.equal(reaction.object.displayName, 'smiley');
982
+ }));
983
+ });
984
+
985
+ describe('#deleteReaction', () => {
986
+ it('deletes a reaction', () =>
987
+ webex.internal.conversation
988
+ .post(conversation, {displayName: 'React Me 1'})
989
+ .then((activity) =>
990
+ webex.internal.conversation.addReaction(conversation, 'smiley', activity)
991
+ )
992
+ .then((reaction) =>
993
+ webex.internal.conversation.deleteReaction(conversation, reaction.id)
994
+ )
995
+ .then((deleteReaction) => {
996
+ assert.equal(deleteReaction.verb, 'delete');
997
+ assert.equal(deleteReaction.object.verb, 'tombstone');
998
+ assert.equal(deleteReaction.object.parent.type, 'reaction');
999
+ }));
1000
+ });
1001
+
1002
+ describe('#unassignModerator()', () => {
1003
+ it('removes a moderator from a conversation', () =>
1004
+ webex.internal.conversation
1005
+ .assignModerator(conversation, spock)
1006
+ .catch(allowConflicts)
1007
+ .then(() => webex.internal.conversation.unassignModerator(conversation, spock))
1008
+ .then(() =>
1009
+ webex.internal.conversation.get(conversation, {
1010
+ activitiesLimit: 5,
1011
+ includeParticipants: true,
1012
+ })
1013
+ )
1014
+ .then((c) => {
1015
+ const moderators = c.participants.items.filter(
1016
+ (p) => p.roomProperties && p.roomProperties.isModerator
1017
+ );
1018
+
1019
+ assert.lengthOf(moderators, 0);
1020
+ }));
1021
+ });
1022
+
1023
+ describe('#update()', () => {
1024
+ it('renames the specified conversation', () =>
1025
+ webex.internal.conversation
1026
+ .update(conversation, {
1027
+ displayName: 'displayName2',
1028
+ objectType: 'conversation',
1029
+ })
1030
+ .then((c) => webex.internal.conversation.get({url: c.target.url}))
1031
+ .then((c) => assert.equal(c.displayName, 'displayName2')));
1032
+ });
1033
+ });
1034
+ });
1035
+ });
1036
+
1037
+ function allowConflicts(reason) {
1038
+ if (!(reason instanceof WebexHttpError.BadRequest)) {
1039
+ throw reason;
1040
+ }
1041
+ }