@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,1255 +1,1255 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
-
5
- import '@webex/internal-plugin-conversation';
6
-
7
- import WebexCore from '@webex/webex-core';
8
- import {assert} from '@webex/test-helper-chai';
9
- import sinon from 'sinon';
10
- import testUsers from '@webex/test-helper-test-users';
11
- import fh from '@webex/test-helper-file';
12
- import makeLocalUrl from '@webex/test-helper-make-local-url';
13
- import {map, find, findIndex, findLast} from 'lodash';
14
- import retry from '@webex/test-helper-retry';
15
-
16
- const postMessage = (webex, convo) => (msg) =>
17
- webex.internal.conversation.post(convo, {displayName: msg});
18
-
19
- const postReply = (webex, conversation) => (threadObj) =>
20
- webex.internal.conversation.post(conversation, threadObj.displayName, {
21
- parentActivityId: threadObj.parentActivityId,
22
- activityType: 'reply',
23
- });
24
-
25
- const threadDisplayNames = ['thread 1', 'thread 2', 'thread 3'];
26
- const createThreadObjs = (parents) => {
27
- const threadObjects = [];
28
-
29
- for (const msg of threadDisplayNames) {
30
- for (const [ix, parentAct] of parents.entries()) {
31
- // add threads to every other, plus randoms for variability
32
- if (ix % 2 || Math.round(Math.random())) {
33
- threadObjects.push({
34
- displayName: `${parentAct.object.displayName} ${msg}`,
35
- parentActivityId: parentAct.id,
36
- });
37
- }
38
- }
39
- }
40
-
41
- return threadObjects;
42
- };
43
-
44
- describe('plugin-conversation', function () {
45
- this.timeout(120000);
46
-
47
- describe('when fetching conversations', () => {
48
- let kirk, mccoy, participants, scott, webex, spock, suluEU, checkov;
49
-
50
- before('create tests users and connect three to mercury', () =>
51
- Promise.all([
52
- testUsers.create({count: 5}),
53
- testUsers.create({count: 1, config: {orgId: process.env.EU_PRIMARY_ORG_ID}}),
54
- ]).then(([users, usersEU]) => {
55
- [spock, mccoy, kirk, scott, checkov] = users;
56
- [suluEU] = usersEU;
57
- participants = [spock, mccoy, kirk];
58
-
59
- spock.webex = new WebexCore({
60
- credentials: {
61
- supertoken: spock.token,
62
- },
63
- });
64
-
65
- webex = spock.webex;
66
-
67
- suluEU.webex = new WebexCore({
68
- credentials: {
69
- supertoken: suluEU.token,
70
- },
71
- });
72
-
73
- checkov.webex = new WebexCore({
74
- credentials: {
75
- supertoken: checkov.token,
76
- },
77
- });
78
-
79
- return Promise.all(
80
- [suluEU, checkov, spock].map((user) =>
81
- user.webex.internal.services
82
- .waitForCatalog('postauth')
83
- .then(() => user.webex.internal.mercury.connect())
84
- )
85
- );
86
- })
87
- );
88
-
89
- after(() =>
90
- Promise.all([suluEU, checkov, spock].map((user) => user.webex.internal.mercury.disconnect()))
91
- );
92
-
93
- describe('#download()', () => {
94
- let sampleImageSmallOnePng = 'sample-image-small-one.png';
95
-
96
- let conversation, conversationRequestSpy;
97
-
98
- before('create conversation', () =>
99
- webex.internal.conversation.create({participants}).then((c) => {
100
- conversation = c;
101
- })
102
- );
103
-
104
- before('fetch image fixture', () =>
105
- fh.fetch(sampleImageSmallOnePng).then((res) => {
106
- sampleImageSmallOnePng = res;
107
- })
108
- );
109
-
110
- beforeEach(() => {
111
- conversationRequestSpy = sinon.spy(webex.internal.conversation, 'request');
112
- });
113
-
114
- afterEach(() => conversationRequestSpy.restore());
115
-
116
- it('rejects for invalid options argument', () =>
117
- webex.internal.conversation
118
- .share(conversation, [sampleImageSmallOnePng])
119
- .then((activity) => {
120
- const item = activity.object.files.items[0];
121
-
122
- item.options = {
123
- params: {
124
- allow: 'invalidOption',
125
- },
126
- };
127
-
128
- assert.isRejected(webex.internal.conversation.download(item));
129
- }));
130
-
131
- it('downloads and decrypts an encrypted file', () =>
132
- webex.internal.conversation
133
- .share(conversation, [sampleImageSmallOnePng])
134
- .then((activity) => webex.internal.conversation.download(activity.object.files.items[0]))
135
- .then((f) =>
136
- fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
137
- ));
138
-
139
- it('emits download progress events for encrypted files', () =>
140
- webex.internal.conversation
141
- .share(conversation, [sampleImageSmallOnePng])
142
- .then((activity) => {
143
- const spy = sinon.spy();
144
-
145
- return webex.internal.conversation
146
- .download(activity.object.files.items[0])
147
- .on('progress', spy)
148
- .then(() => assert.called(spy));
149
- }));
150
-
151
- it('downloads and decrypts a file without a scr key', () =>
152
- webex.internal.conversation
153
- .download({
154
- scr: {
155
- loc: makeLocalUrl('/sample-image-small-one.png'),
156
- },
157
- })
158
- .then((f) =>
159
- fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
160
- )
161
- .then(() =>
162
- conversationRequestSpy.returnValues[0].then((res) => {
163
- assert.property(res.options.headers, 'cisco-no-http-redirect');
164
- assert.property(res.options.headers, 'spark-user-agent');
165
- assert.property(res.options.headers, 'trackingid');
166
- })
167
- ));
168
-
169
- it('downloads and decrypts a non-encrypted file', () =>
170
- webex.internal.conversation
171
- .download({url: makeLocalUrl('/sample-image-small-one.png')})
172
- .then((f) =>
173
- fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
174
- )
175
- .then(() =>
176
- conversationRequestSpy.returnValues[0].then((res) => {
177
- assert.property(res.options.headers, 'cisco-no-http-redirect');
178
- assert.property(res.options.headers, 'spark-user-agent');
179
- assert.property(res.options.headers, 'trackingid');
180
- })
181
- ));
182
-
183
- it('downloads non-encrypted file with specific options headers', () =>
184
- webex.internal.conversation
185
- .download(
186
- {url: makeLocalUrl('/sample-image-small-one.png')},
187
- {
188
- headers: {
189
- 'cisco-no-http-redirect': null,
190
- 'spark-user-agent': null,
191
- trackingid: null,
192
- },
193
- }
194
- )
195
- .then((f) =>
196
- fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
197
- )
198
- .then(() =>
199
- conversationRequestSpy.returnValues[0].then((res) => {
200
- assert.isUndefined(res.options.headers['cisco-no-http-redirect']);
201
- assert.isUndefined(res.options.headers['spark-user-agent']);
202
- assert.isUndefined(res.options.headers.trackingid);
203
- })
204
- ));
205
-
206
- it('emits download progress events for non-encrypted files', () => {
207
- const spy = sinon.spy();
208
-
209
- return webex.internal.conversation
210
- .download({url: makeLocalUrl('/sample-image-small-one.png')})
211
- .on('progress', spy)
212
- .then((f) =>
213
- fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
214
- )
215
- .then(() => assert.called(spy));
216
- });
217
-
218
- describe('reads exif data and', () => {
219
- let fileItem;
220
- let sampleImagePortraitJpeg = 'Portrait_7.jpg';
221
-
222
- before('fetch image fixture', () =>
223
- fh.fetch(sampleImagePortraitJpeg).then((res) => {
224
- sampleImagePortraitJpeg = res;
225
- sampleImagePortraitJpeg.displayName = 'Portrait_7.jpg';
226
- sampleImagePortraitJpeg.mimeType = 'image/jpeg';
227
- })
228
- );
229
- it('does not add exif data', () =>
230
- webex.internal.conversation
231
- .share(conversation, [sampleImagePortraitJpeg])
232
- .then((activity) => {
233
- fileItem = activity.object.files.items[0];
234
-
235
- return webex.internal.conversation.download(fileItem, {shouldNotAddExifData: true});
236
- })
237
- .then((f) => {
238
- assert.equal(fileItem.orientation, undefined);
239
-
240
- return fh.isMatchingFile(f, sampleImagePortraitJpeg);
241
- })
242
- .then((result) => assert.isTrue(result)));
243
-
244
- it('adds exif data', () =>
245
- webex.internal.conversation
246
- .share(conversation, [sampleImagePortraitJpeg])
247
- .then((activity) => {
248
- fileItem = activity.object.files.items[0];
249
-
250
- return webex.internal.conversation.download(fileItem);
251
- })
252
- .then((f) => {
253
- assert.equal(fileItem.orientation, 7);
254
-
255
- return fh.isMatchingFile(f, sampleImagePortraitJpeg);
256
- })
257
- .then((result) => assert.isTrue(result)));
258
- });
259
- });
260
-
261
- describe('#get()', () => {
262
- let conversation, conversation2;
263
-
264
- before('create conversations', () =>
265
- Promise.all([
266
- webex.internal.conversation.create({participants: [mccoy.id]}).then((c) => {
267
- conversation = c;
268
- }),
269
- webex.internal.conversation.create({participants: [scott.id]}).then((c) => {
270
- conversation2 = c;
271
- }),
272
- ])
273
- );
274
-
275
- it('retrieves a single conversation by url', () =>
276
- webex.internal.conversation.get({url: conversation.url}).then((c) => {
277
- assert.equal(c.id, conversation.id);
278
- assert.equal(c.url, conversation.url);
279
- }));
280
-
281
- it('retrieves a single conversation by id', () =>
282
- webex.internal.conversation.get({id: conversation.id}).then((c) => {
283
- assert.equal(c.id, conversation.id);
284
- assert.equal(c.url, conversation.url);
285
- }));
286
-
287
- it('retrieves a 1:1 conversation by userId', () =>
288
- webex.internal.conversation.get({user: mccoy}).then((c) => {
289
- assert.equal(c.id, conversation.id);
290
- assert.equal(c.url, conversation.url);
291
- }));
292
-
293
- it('retrieves a 1:1 conversation with a deleted user', () =>
294
- webex.internal.conversation
295
- .get({user: scott})
296
- .then((c) => {
297
- assert.equal(c.id, conversation2.id);
298
- assert.equal(c.url, conversation2.url);
299
- })
300
- .then(() => testUsers.remove([scott]))
301
- // add retries to address CI propagation delay
302
- .then(() =>
303
- retry(() => assert.isRejected(webex.internal.conversation.get({user: scott})))
304
- )
305
- .then(() =>
306
- retry(() =>
307
- webex.internal.conversation.get({user: scott}, {includeConvWithDeletedUserUUID: true})
308
- )
309
- )
310
- .then((c) => {
311
- assert.equal(c.id, conversation2.id);
312
- assert.equal(c.url, conversation2.url);
313
- }));
314
-
315
- it('decrypts the contents of activities in the retrieved conversation', () =>
316
- webex.internal.conversation
317
- .post(conversation, {
318
- displayName: 'Test Message',
319
- })
320
- .then(() =>
321
- webex.internal.conversation.get({url: conversation.url}, {activitiesLimit: 50})
322
- )
323
- .then((c) => {
324
- const posts = c.activities.items.filter((activity) => activity.verb === 'post');
325
-
326
- assert.lengthOf(posts, 1);
327
- assert.equal(posts[0].object.displayName, 'Test Message');
328
- }));
329
- });
330
-
331
- describe('#list()', () => {
332
- let conversation1, conversation2;
333
-
334
- before('create conversations', () =>
335
- webex.internal.conversation
336
- .create({
337
- displayName: 'test 1',
338
- participants,
339
- })
340
- .then((c) => {
341
- conversation1 = c;
342
- })
343
- .then(() =>
344
- webex.internal.conversation.create({
345
- displayName: 'test 2',
346
- participants,
347
- })
348
- )
349
-
350
- .then((c) => {
351
- conversation2 = c;
352
- })
353
- );
354
-
355
- it('retrieves a set of conversations', () =>
356
- webex.internal.conversation
357
- .list({
358
- conversationsLimit: 2,
359
- })
360
- .then((conversations) => {
361
- assert.include(map(conversations, 'url'), conversation1.url);
362
- assert.include(map(conversations, 'url'), conversation2.url);
363
- }));
364
-
365
- it('retrieves a paginated set of conversations', () =>
366
- webex.internal.conversation
367
- .paginate({
368
- conversationsLimit: 1,
369
- personRefresh: false,
370
- paginate: true,
371
- })
372
- .then((response) => {
373
- const conversations = response.page.items;
374
-
375
- assert.lengthOf(conversations, 1);
376
- assert.equal(conversations[0].displayName, conversation2.displayName);
377
-
378
- return webex.internal.conversation.paginate({page: response.page});
379
- })
380
- .then((response) => {
381
- const conversations = response.page.items;
382
-
383
- assert.lengthOf(conversations, 1);
384
- assert.equal(conversations[0].displayName, conversation1.displayName);
385
- }));
386
-
387
- describe('with summary = true (ConversationsSummary)', () => {
388
- it('retrieves all conversations using conversationsSummary', () =>
389
- webex.internal.conversation
390
- .list({
391
- summary: true,
392
- })
393
- .then((conversations) => {
394
- assert.include(map(conversations, 'url'), conversation1.url);
395
- assert.include(map(conversations, 'url'), conversation2.url);
396
- }));
397
-
398
- it('retrieves a set of (1) conversations using conversationsLimit', () =>
399
- webex.internal.conversation
400
- .list({
401
- summary: true,
402
- conversationsLimit: 1,
403
- })
404
- .then((conversations) => {
405
- assert.lengthOf(conversations, 1);
406
- assert.include(map(conversations, 'url'), conversation2.url);
407
- assert.include(map(conversations, 'displayName'), conversation2.displayName);
408
- }));
409
- });
410
-
411
- describe('with deferDecrypt = true', () => {
412
- it('retrieves a non-decrypted set of conversations each with a bound decrypt method', () =>
413
- webex.internal.conversation
414
- .list({
415
- conversationsLimit: 2,
416
- deferDecrypt: true,
417
- })
418
- .then(([c1, c2]) => {
419
- assert.lengthOf(
420
- c1.displayName.split('.'),
421
- 5,
422
- '5 periods implies this is a jwt and not a decrypted string'
423
- );
424
- assert.notInclude(['test 1, test 2'], c1.displayName);
425
-
426
- assert.lengthOf(
427
- c2.displayName.split('.'),
428
- 5,
429
- '5 periods implies this is a jwt and not a decrypted string'
430
- );
431
- assert.notInclude(['test 1, test 2'], c2.displayName);
432
-
433
- return Promise.all([
434
- c1.decrypt().then(() => assert.notInclude(['test 1, test 2'], c1.displayName)),
435
- c2.decrypt().then(() => assert.notInclude(['test 1, test 2'], c2.displayName)),
436
- ]);
437
- }));
438
- });
439
-
440
- describe('with deferDecrypt && summary = true', () => {
441
- it('retrieves a non-decrypted set of conversations each with a bound decrypt method', () =>
442
- webex.internal.conversation
443
- .list({
444
- conversationsLimit: 2,
445
- deferDecrypt: true,
446
- summary: true,
447
- })
448
- .then(([c1, c2]) => {
449
- assert.lengthOf(
450
- c1.displayName.split('.'),
451
- 5,
452
- '5 periods implies this is a jwt and not a decrypted string'
453
- );
454
- assert.notInclude(['test 1, test 2'], c1.displayName);
455
-
456
- assert.lengthOf(
457
- c2.displayName.split('.'),
458
- 5,
459
- '5 periods implies this is a jwt and not a decrypted string'
460
- );
461
- assert.notInclude(['test 1, test 2'], c2.displayName);
462
-
463
- return Promise.all([
464
- c1.decrypt().then(() => assert.notInclude(['test 1, test 2'], c1.displayName)),
465
- c2.decrypt().then(() => assert.notInclude(['test 1, test 2'], c2.displayName)),
466
- ]);
467
- }));
468
- });
469
-
470
- describe('with conversation from remote clusters', () => {
471
- let conversation3, conversation4;
472
-
473
- before('create conversations in EU cluster', () =>
474
- Promise.all([
475
- suluEU.webex.internal.conversation
476
- .create({
477
- displayName: 'eu test 1',
478
- participants,
479
- })
480
- .then((c) => {
481
- conversation3 = c;
482
- }),
483
- suluEU.webex.internal.conversation
484
- .create({
485
- displayName: 'eu test 2',
486
- participants: [checkov.id, spock.id],
487
- })
488
- .then((c) => {
489
- conversation4 = c;
490
- }),
491
- ])
492
- );
493
-
494
- it('retrieves local + remote cluster conversations', () =>
495
- webex.internal.conversation.list().then((conversations) => {
496
- assert.include(map(conversations, 'url'), conversation1.url);
497
- assert.include(map(conversations, 'url'), conversation2.url);
498
- assert.include(map(conversations, 'url'), conversation3.url);
499
- assert.include(map(conversations, 'url'), conversation4.url);
500
- }));
501
-
502
- it('retrieves only remote cluter conversations if user does not have any local conversations', () =>
503
- checkov.webex.internal.conversation.list().then((conversations) => {
504
- assert.include(map(conversations, 'url'), conversation4.url);
505
- assert.lengthOf(conversations, 1);
506
- }));
507
- });
508
- });
509
-
510
- describe('#listLeft()', () => {
511
- let conversation;
512
-
513
- before('create conversation', () =>
514
- webex.internal.conversation.create({participants}).then((c) => {
515
- conversation = c;
516
- })
517
- );
518
-
519
- it('retrieves the conversations the current user has left', () =>
520
- webex.internal.conversation
521
- .listLeft()
522
- .then((c) => {
523
- assert.lengthOf(c, 0);
524
-
525
- return webex.internal.conversation.leave(conversation);
526
- })
527
- .then(() => webex.internal.conversation.listLeft())
528
- .then((c) => {
529
- assert.lengthOf(c, 1);
530
- assert.equal(c[0].id, conversation.id);
531
- }));
532
- });
533
-
534
- describe('#listActivities()', () => {
535
- let conversation;
536
-
537
- before('create conversation with activity', () =>
538
- webex.internal.conversation.create({participants}).then((c) => {
539
- conversation = c;
540
- assert.lengthOf(conversation.participants.items, 3);
541
-
542
- return webex.internal.conversation.post(conversation, {displayName: 'first message'});
543
- })
544
- );
545
-
546
- it('retrieves activities for the specified conversation', () =>
547
- webex.internal.conversation
548
- .listActivities({conversationUrl: conversation.url})
549
- .then((activities) => {
550
- assert.isArray(activities);
551
- assert.lengthOf(activities, 2);
552
- }));
553
- });
554
-
555
- describe('#listThreads()', () => {
556
- let webex2;
557
-
558
- before('connect mccoy to mercury', () => {
559
- webex2 = new WebexCore({
560
- credentials: {
561
- authorization: mccoy.token,
562
- },
563
- });
564
-
565
- return webex2.internal.mercury.connect();
566
- });
567
-
568
- after(() => webex2 && webex2.internal.mercury.disconnect());
569
-
570
- let conversation;
571
- let parent;
572
-
573
- before('create conversation', () =>
574
- webex.internal.conversation.create({participants}).then((c) => {
575
- conversation = c;
576
- assert.lengthOf(conversation.participants.items, 3);
577
-
578
- return webex2.internal.conversation
579
- .post(conversation, {displayName: 'first message'})
580
- .then((parentActivity) => {
581
- parent = parentActivity;
582
- });
583
- })
584
- );
585
-
586
- it('retrieves threads()', () =>
587
- webex2.internal.conversation
588
- .post(conversation, 'thread1', {
589
- parentActivityId: parent.id,
590
- activityType: 'reply',
591
- })
592
- .then(() => webex2.internal.conversation.listThreads())
593
- .then((thread) => {
594
- assert.equal(thread.length, 1);
595
- const firstThread = thread[0];
596
-
597
- assert.equal(firstThread.childType, 'reply');
598
- assert.equal(firstThread.parentActivityId, parent.id);
599
- assert.equal(firstThread.conversationId, conversation.id);
600
- assert.equal(firstThread.childActivities.length, 1);
601
-
602
- const childActivity = firstThread.childActivities[0];
603
-
604
- assert.equal(childActivity.objectType, 'activity');
605
- assert.equal(childActivity.object.displayName, 'thread1');
606
- }));
607
- });
608
-
609
- describe('#listMentions()', () => {
610
- let webex2;
611
-
612
- before('connect mccoy to mercury', () => {
613
- webex2 = new WebexCore({
614
- credentials: {
615
- authorization: mccoy.token,
616
- },
617
- });
618
-
619
- return webex2.internal.mercury.connect();
620
- });
621
-
622
- after(() => webex2 && webex2.internal.mercury.disconnect());
623
-
624
- let conversation;
625
-
626
- before('create conversation', () =>
627
- webex.internal.conversation.create({participants}).then((c) => {
628
- conversation = c;
629
- assert.lengthOf(conversation.participants.items, 3);
630
- })
631
- );
632
-
633
- it('retrieves activities in which the current user was mentioned', () =>
634
- webex2.internal.conversation
635
- .post(conversation, {
636
- displayName: 'Green blooded hobgloblin',
637
- content: `<webex-mention data-object-type="person" data-object-id="${spock.id}">Green blooded hobgloblin</webex-mention>`,
638
- mentions: {
639
- items: [
640
- {
641
- id: `${spock.id}`,
642
- objectType: 'person',
643
- },
644
- ],
645
- },
646
- })
647
- .then((activity) =>
648
- webex.internal.conversation
649
- .listMentions({sinceDate: Date.parse(activity.published) - 1})
650
- .then((mentions) => {
651
- assert.lengthOf(mentions, 1);
652
- assert.equal(mentions[0].url, activity.url);
653
- })
654
- ));
655
- });
656
-
657
- // TODO: add testing for bulk_activities_fetch() with clusters later
658
- describe('#bulkActivitiesFetch()', () => {
659
- let jenny, maria, dan, convo1, convo2, euConvo1;
660
- let webex3;
661
-
662
- before('create tests users and connect one to mercury', () =>
663
- testUsers.create({count: 4}).then((users) => {
664
- [jenny, maria, dan] = users;
665
-
666
- webex3 = new WebexCore({
667
- credentials: {
668
- authorization: jenny.token,
669
- },
670
- });
671
-
672
- return webex3.internal.mercury.connect();
673
- })
674
- );
675
-
676
- after(() => webex3 && webex3.internal.mercury.disconnect());
677
-
678
- before('create conversation 1', () =>
679
- webex3.internal.conversation.create({participants: [jenny, maria]}).then((c1) => {
680
- convo1 = c1;
681
- })
682
- );
683
-
684
- before('create conversation 2', () =>
685
- webex3.internal.conversation.create({participants: [jenny, dan]}).then((c2) => {
686
- convo2 = c2;
687
- })
688
- );
689
-
690
- before('create conversations in EU cluster', () =>
691
- suluEU.webex.internal.conversation
692
- .create({
693
- displayName: 'eu test 1',
694
- participants: [jenny, suluEU, dan],
695
- })
696
- .then((c) => {
697
- euConvo1 = c;
698
- })
699
- );
700
-
701
- before('add comments to convo1, and check post requests successfully went through', () =>
702
- webex3.internal.conversation
703
- .post(convo1, {displayName: 'BAGELS (O)'})
704
- .then((c1) => {
705
- assert.equal(c1.object.displayName, 'BAGELS (O)');
706
-
707
- return webex3.internal.conversation.post(convo1, {displayName: 'Cream Cheese'});
708
- })
709
- .then((c2) => {
710
- assert.equal(c2.object.displayName, 'Cream Cheese');
711
- })
712
- );
713
-
714
- before('add comments to convo2, and check post requests successfully went through', () =>
715
- webex3.internal.conversation
716
- .post(convo2, {displayName: 'Want to head to lunch soon?'})
717
- .then((c1) => {
718
- assert.equal(c1.object.displayName, 'Want to head to lunch soon?');
719
-
720
- return webex3.internal.conversation.post(convo2, {displayName: 'Sure :)'});
721
- })
722
- .then((c2) => {
723
- assert.equal(c2.object.displayName, 'Sure :)');
724
-
725
- return webex3.internal.conversation.post(convo2, {displayName: 'where?'});
726
- })
727
- .then((c3) => {
728
- assert.equal(c3.object.displayName, 'where?');
729
-
730
- return webex3.internal.conversation.post(convo2, {displayName: 'Meekong Bar!'});
731
- })
732
- .then((c4) => {
733
- assert.equal(c4.object.displayName, 'Meekong Bar!');
734
- })
735
- );
736
-
737
- before('add comments to euConvo1, and check post requests successfully went through', () =>
738
- suluEU.webex.internal.conversation.post(euConvo1, {displayName: 'Hello'}).then((c1) => {
739
- assert.equal(c1.object.displayName, 'Hello');
740
- })
741
- );
742
-
743
- it('retrieves activities from a single conversation', () =>
744
- webex3.internal.conversation
745
- .listActivities({conversationUrl: convo1.url})
746
- .then((convoActivities) => {
747
- const activityURLs = [];
748
- const expectedActivities = [];
749
-
750
- convoActivities.forEach((a) => {
751
- if (a.verb === 'post') {
752
- activityURLs.push(a.url);
753
- expectedActivities.push(a);
754
- }
755
- });
756
-
757
- return webex3.internal.conversation
758
- .bulkActivitiesFetch(activityURLs)
759
- .then((bulkFetchedActivities) => {
760
- assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
761
- assert.equal(
762
- bulkFetchedActivities[0].object.displayName,
763
- expectedActivities[0].object.displayName
764
- );
765
- assert.equal(
766
- bulkFetchedActivities[1].object.displayName,
767
- expectedActivities[1].object.displayName
768
- );
769
- });
770
- }));
771
-
772
- it('retrieves activities from multiple conversations', () => {
773
- const activityURLs = [];
774
- const expectedActivities = [];
775
-
776
- return webex3.internal.conversation
777
- .listActivities({conversationUrl: convo1.url})
778
- .then((convo1Activities) => {
779
- // gets all post activity urls from convo1
780
- convo1Activities.forEach((a1) => {
781
- if (a1.verb === 'post') {
782
- activityURLs.push(a1.url);
783
- expectedActivities.push(a1);
784
- }
785
- });
786
-
787
- return webex3.internal.conversation.listActivities({conversationUrl: convo2.url});
788
- })
789
- .then((convo2Activities) => {
790
- // gets activity urls of only comment 3 and 4 from convo2
791
- [3, 4].forEach((i) => {
792
- activityURLs.push(convo2Activities[i].url);
793
- expectedActivities.push(convo2Activities[i]);
794
- });
795
-
796
- return webex3.internal.conversation
797
- .bulkActivitiesFetch(activityURLs)
798
- .then((bulkFetchedActivities) => {
799
- assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
800
- assert.equal(
801
- bulkFetchedActivities[0].object.displayName,
802
- expectedActivities[0].object.displayName
803
- );
804
- assert.equal(
805
- bulkFetchedActivities[1].object.displayName,
806
- expectedActivities[1].object.displayName
807
- );
808
- assert.equal(
809
- bulkFetchedActivities[2].object.displayName,
810
- expectedActivities[2].object.displayName
811
- );
812
- assert.equal(
813
- bulkFetchedActivities[3].object.displayName,
814
- expectedActivities[3].object.displayName
815
- );
816
- });
817
- });
818
- });
819
-
820
- it('given a activity url that does not exist, should return []', () => {
821
- const mockURL =
822
- 'https://conversation-intb.ciscospark.com/conversation/api/v1/activities/6d8c7c90-a770-11e9-bcfb-6616ead99ac3';
823
-
824
- webex3.internal.conversation
825
- .bulkActivitiesFetch([mockURL])
826
- .then((bulkFetchedActivities) => {
827
- assert.equal(bulkFetchedActivities, []);
828
- });
829
- });
830
-
831
- it('retrieves activities from multiple conversations passing in base convo url', () => {
832
- const activityURLs = [];
833
- const expectedActivities = [];
834
-
835
- return webex3.internal.conversation
836
- .listActivities({conversationUrl: convo1.url})
837
- .then((convo1Activities) => {
838
- // gets all post activity urls from convo1
839
- convo1Activities.forEach((a1) => {
840
- if (a1.verb === 'post') {
841
- activityURLs.push(a1.url);
842
- expectedActivities.push(a1);
843
- }
844
- });
845
-
846
- return webex3.internal.conversation.listActivities({conversationUrl: convo2.url});
847
- })
848
- .then((convo2Activities) => {
849
- // gets activity urls of only comment 3 and 4 from convo2
850
- [3, 4].forEach((i) => {
851
- activityURLs.push(convo2Activities[i].url);
852
- expectedActivities.push(convo2Activities[i]);
853
- });
854
-
855
- return webex3.internal.conversation
856
- .bulkActivitiesFetch(activityURLs, undefined, {url: process.env.CONVERSATION_SERVICE})
857
- .then((bulkFetchedActivities) => {
858
- assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
859
- assert.equal(
860
- bulkFetchedActivities[0].object.displayName,
861
- expectedActivities[0].object.displayName
862
- );
863
- assert.equal(
864
- bulkFetchedActivities[1].object.displayName,
865
- expectedActivities[1].object.displayName
866
- );
867
- assert.equal(
868
- bulkFetchedActivities[2].object.displayName,
869
- expectedActivities[2].object.displayName
870
- );
871
- assert.equal(
872
- bulkFetchedActivities[3].object.displayName,
873
- expectedActivities[3].object.displayName
874
- );
875
- });
876
- });
877
- });
878
-
879
- it('retrieves activities from conversations passing in base convo url from another cluster', () => {
880
- const activityURLs = [];
881
- const expectedActivities = [];
882
-
883
- return webex3.internal.conversation
884
- .listActivities({conversationUrl: euConvo1.url})
885
- .then((euConvo1Activities) => {
886
- const convoUrlRegex = /(.*)\/activities/;
887
-
888
- activityURLs.push(euConvo1Activities[1].url);
889
- expectedActivities.push(euConvo1Activities[1]);
890
- const match = convoUrlRegex.exec(euConvo1Activities[1].url);
891
- const convoUrl = match[1];
892
-
893
- return webex3.internal.conversation.bulkActivitiesFetch(activityURLs, {url: convoUrl});
894
- })
895
- .then((bulkFetchedActivities) => {
896
- assert.lengthOf(bulkFetchedActivities, 1);
897
- assert.equal(
898
- bulkFetchedActivities[0].object.displayName,
899
- expectedActivities[0].object.displayName
900
- );
901
- });
902
- });
903
- });
904
-
905
- describe('#listParentActivityIds', () => {
906
- let conversation, parent;
907
-
908
- beforeEach('create conversation with activity', () =>
909
- webex.internal.conversation
910
- .create({participants})
911
- .then((c) => {
912
- conversation = c;
913
-
914
- return webex.internal.conversation.post(conversation, {displayName: 'first message'});
915
- })
916
- .then((parentAct) => {
917
- parent = parentAct;
918
- })
919
- );
920
-
921
- it('retrieves parent IDs for thread parents()', () =>
922
- webex.internal.conversation
923
- .post(
924
- conversation,
925
- {displayName: 'first thread reply'},
926
- {
927
- parentActivityId: parent.id,
928
- activityType: 'reply',
929
- }
930
- )
931
- .then(({parent: parentObj} = {}) => {
932
- assert.equal(parentObj.type, 'reply');
933
- assert.equal(parentObj.id, parent.id);
934
-
935
- return webex.internal.conversation.listParentActivityIds(conversation.url, {
936
- activityType: 'reply',
937
- });
938
- })
939
- .then(({reply}) => {
940
- assert.include(reply, parent.id);
941
- }));
942
-
943
- it('retrieves parent IDs for edits', () =>
944
- webex.internal.conversation
945
- .post(conversation, 'edited', {
946
- parent: {
947
- id: parent.id,
948
- type: 'edit',
949
- },
950
- })
951
- .then((edit) => {
952
- assert.equal(edit.parent.type, 'edit');
953
- assert.equal(edit.parent.id, parent.id);
954
-
955
- return webex.internal.conversation.listParentActivityIds(conversation.url, {
956
- activityType: 'edit',
957
- });
958
- })
959
- .then(({edit}) => {
960
- assert.include(edit, parent.id);
961
- }));
962
-
963
- it('retrieves parent IDs for reactions', () =>
964
- webex.internal.conversation
965
- .addReaction(conversation, 'heart', parent)
966
- .then((reaction) => {
967
- assert.equal(reaction.parent.type, 'reaction');
968
- assert.equal(reaction.parent.id, parent.id);
969
-
970
- return webex.internal.conversation.listParentActivityIds(conversation.url, {
971
- activityType: 'reaction',
972
- });
973
- })
974
- .then(({reaction}) => {
975
- assert.include(reaction, parent.id);
976
- }));
977
- });
978
-
979
- describe('#listChildActivitiesByParentId()', () => {
980
- let conversation, parent;
981
- let replies;
982
-
983
- before('create conversation with thread replies', () =>
984
- webex.internal.conversation
985
- .create({participants})
986
- .then((c) => {
987
- conversation = c;
988
-
989
- return webex.internal.conversation.post(conversation, {displayName: 'first message'});
990
- })
991
- .then((parentAct) => {
992
- parent = parentAct;
993
-
994
- const messages = ['thread 1', 'thread 2', 'thread 3'];
995
-
996
- return Promise.all(
997
- messages.map((msg) =>
998
- webex.internal.conversation.post(conversation, msg, {
999
- parentActivityId: parent.id,
1000
- activityType: 'reply',
1001
- })
1002
- )
1003
- );
1004
- })
1005
- .then((repliesArr) => {
1006
- replies = repliesArr;
1007
- })
1008
- );
1009
-
1010
- it('retrieves thread reply activities for a given parent', () =>
1011
- webex.internal.conversation
1012
- .listChildActivitiesByParentId(conversation.url, parent.id, 'reply')
1013
- .then((response) => {
1014
- const {items} = response.body;
1015
-
1016
- items.forEach((threadAct) => {
1017
- assert.include(
1018
- replies.map((reply) => reply.id),
1019
- threadAct.id
1020
- );
1021
- });
1022
- }));
1023
- });
1024
-
1025
- describe('#_listActivitiesThreadOrdered', () => {
1026
- let conversation, firstParentBatch, secondParentBatch, getOlder, jumpToActivity;
1027
-
1028
- const minActivities = 10;
1029
- const displayNames = [
1030
- 'first message',
1031
- 'second message',
1032
- 'third message',
1033
- 'fourth message',
1034
- 'fifth message',
1035
- 'sixth message',
1036
- 'seventh message',
1037
- 'eighth message',
1038
- 'ninth message',
1039
- ];
1040
-
1041
- const initializeGenerator = () =>
1042
- webex.internal.conversation.listActivitiesThreadOrdered({
1043
- conversationUrl: conversation.url,
1044
- minActivities,
1045
- });
1046
-
1047
- before(() =>
1048
- webex.internal.conversation
1049
- .create({participants})
1050
- .then((c) => {
1051
- conversation = c;
1052
-
1053
- return c;
1054
- })
1055
- .then((c) => Promise.all(displayNames.slice(0, 5).map(postMessage(webex, c))))
1056
- .then((parents) => {
1057
- firstParentBatch = parents;
1058
-
1059
- return Promise.all(createThreadObjs(parents).map(postReply(webex, conversation)));
1060
- })
1061
- .then(() => Promise.all(displayNames.slice(4).map(postMessage(webex, conversation))))
1062
- .then((parents) => {
1063
- secondParentBatch = parents;
1064
-
1065
- return Promise.all(createThreadObjs(parents).map(postReply(webex, conversation)));
1066
- })
1067
- );
1068
-
1069
- beforeEach(() => {
1070
- const funcs = initializeGenerator();
1071
-
1072
- getOlder = funcs.getOlder;
1073
- jumpToActivity = funcs.jumpToActivity;
1074
- });
1075
-
1076
- it('should return more than or exactly N minimum activities', () =>
1077
- getOlder().then(({value}) => {
1078
- assert.isAtLeast(value.length, minActivities);
1079
- }));
1080
-
1081
- it('should return edit activity ID as activity ID when an activity has been edited', () => {
1082
- const lastParent = secondParentBatch[secondParentBatch.length - 1];
1083
-
1084
- const message = {
1085
- displayName: 'edited',
1086
- content: 'edited',
1087
- };
1088
-
1089
- const editingActivity = Object.assign(
1090
- {
1091
- parent: {
1092
- id: lastParent.id,
1093
- type: 'edit',
1094
- },
1095
- },
1096
- {
1097
- object: message,
1098
- }
1099
- );
1100
-
1101
- return webex.internal.conversation
1102
- .post(conversation, message, editingActivity)
1103
- .then(() => getOlder())
1104
- .then(({value}) => {
1105
- const activities = value.map((act) => act.activity);
1106
- const editedAct = find(activities, (act) => act.editParent);
1107
-
1108
- assert.equal(editedAct.editParent.id, lastParent.id);
1109
- assert.notEqual(editedAct.id, lastParent.id);
1110
- });
1111
- });
1112
-
1113
- it('should return activities in thread order', () =>
1114
- getOlder().then((data) => {
1115
- const {value} = data;
1116
- const oldestAct = value[0].activity;
1117
- const newestAct = value[value.length - 1].activity;
1118
-
1119
- const oldestThreadIx = findIndex(value, ['activity.parent.type', 'reply']);
1120
- const oldestParent = value[oldestThreadIx - 1].activity;
1121
-
1122
- assert.isTrue(oldestAct.published < newestAct.published);
1123
-
1124
- assert.doesNotHaveAnyKeys(oldestParent, 'parentActivityId');
1125
- assert.isTrue(oldestParent.object.objectType === 'comment');
1126
- }));
1127
-
1128
- it('should return next batch when getOlder is called a second time', () => {
1129
- let firstBatch;
1130
-
1131
- return getOlder()
1132
- .then(({value}) => {
1133
- firstBatch = value;
1134
-
1135
- return getOlder();
1136
- })
1137
- .then(({value}) => {
1138
- const secondBatch = value;
1139
-
1140
- const oldestRootInFirstBatch = find(firstBatch, [
1141
- 'activity.object.objectType',
1142
- 'comment',
1143
- ]).activity;
1144
- const newestRootInSecondBatch = findLast(secondBatch, [
1145
- 'activity.object.objectType',
1146
- 'comment',
1147
- ]).activity;
1148
-
1149
- assert.isTrue(oldestRootInFirstBatch.published > newestRootInSecondBatch.published);
1150
- });
1151
- });
1152
-
1153
- it('should return done as true when no more activities can be fetched', () => {
1154
- const {getOlder: getOlderWithLargeMin} =
1155
- webex.internal.conversation.listActivitiesThreadOrdered({
1156
- conversationId: conversation.id,
1157
- minActivities: 50,
1158
- });
1159
-
1160
- return getOlderWithLargeMin().then(({done}) => {
1161
- assert.isTrue(done);
1162
- });
1163
- });
1164
-
1165
- describe('jumpToActivity()', () => {
1166
- let _listActivitiesThreadOrderedSpy;
1167
-
1168
- beforeEach(() => {
1169
- _listActivitiesThreadOrderedSpy = sinon.spy(
1170
- webex.internal.conversation,
1171
- '_listActivitiesThreadOrdered'
1172
- );
1173
- });
1174
-
1175
- afterEach(() => {
1176
- webex.internal.conversation._listActivitiesThreadOrdered.restore();
1177
- });
1178
-
1179
- it('should return searched-for activity with surrounding activities when jumpToActivity is called with an activity', () => {
1180
- const search = firstParentBatch[firstParentBatch.length - 1];
1181
-
1182
- return jumpToActivity(search).then(({value}) => {
1183
- const searchedForActivityIx = findIndex(value, ['id', search.id]);
1184
-
1185
- assert.isFalse(searchedForActivityIx === -1);
1186
- assert.isTrue(searchedForActivityIx > 0);
1187
- assert.isTrue(searchedForActivityIx < value.length);
1188
- });
1189
- });
1190
-
1191
- it('should return all activities in a space when jumping to an activity in a space with less activities than the asked-for activities limit', () =>
1192
- webex.internal.conversation
1193
- .create({participants: [scott.id]})
1194
- .then((c) =>
1195
- webex.internal.conversation
1196
- .post(c, {displayName: 'first message'})
1197
- .then((m) =>
1198
- webex.internal.conversation
1199
- .listActivities({conversationUrl: c.url})
1200
- .then((acts) => jumpToActivity(m).then(() => acts.length))
1201
- )
1202
- )
1203
- .then((actCount) => {
1204
- assert.isTrue(actCount < minActivities);
1205
- }));
1206
-
1207
- it('should return all activities in a space when jumping to an activity in a space with more activities than the asked-for activities limit', () =>
1208
- webex.internal.conversation
1209
- .create({participants: [scott.id]})
1210
- .then((c) => {
1211
- const $posts = [];
1212
-
1213
- // eslint-disable-next-line no-plusplus
1214
- for (let i = 0; i < 15; i++) {
1215
- $posts.push(webex.internal.conversation.post(c, {displayName: `message ${i}`}));
1216
- }
1217
-
1218
- return Promise.all($posts).then(() =>
1219
- webex.internal.conversation.post(c, {displayName: 'message last'})
1220
- );
1221
- })
1222
- .then((lastPost) => jumpToActivity(lastPost))
1223
- .then(({value: acts}) => {
1224
- assert.isAtLeast(acts.length, minActivities);
1225
-
1226
- const firstAct = acts[0].activity;
1227
-
1228
- assert.notEqual(firstAct.verb, 'create');
1229
- }));
1230
-
1231
- it('should re-initialize _listActivitiesThreadOrdered when jumpToActivity is called with a new URL', () => {
1232
- let conversation2, msg;
1233
-
1234
- return webex.internal.conversation
1235
- .create({participants: [scott.id]})
1236
- .then((c) => {
1237
- conversation2 = c;
1238
-
1239
- return webex.internal.conversation.post(conversation2, {
1240
- displayName: 'first message',
1241
- });
1242
- })
1243
- .then((m) => {
1244
- msg = m;
1245
-
1246
- return jumpToActivity(msg);
1247
- })
1248
- .then(() => {
1249
- assert.isTrue(_listActivitiesThreadOrderedSpy.args[0][0].url === conversation2.url);
1250
- });
1251
- });
1252
- });
1253
- });
1254
- });
1255
- });
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+
5
+ import '@webex/internal-plugin-conversation';
6
+
7
+ import WebexCore from '@webex/webex-core';
8
+ import {assert} from '@webex/test-helper-chai';
9
+ import sinon from 'sinon';
10
+ import testUsers from '@webex/test-helper-test-users';
11
+ import fh from '@webex/test-helper-file';
12
+ import makeLocalUrl from '@webex/test-helper-make-local-url';
13
+ import {map, find, findIndex, findLast} from 'lodash';
14
+ import retry from '@webex/test-helper-retry';
15
+
16
+ const postMessage = (webex, convo) => (msg) =>
17
+ webex.internal.conversation.post(convo, {displayName: msg});
18
+
19
+ const postReply = (webex, conversation) => (threadObj) =>
20
+ webex.internal.conversation.post(conversation, threadObj.displayName, {
21
+ parentActivityId: threadObj.parentActivityId,
22
+ activityType: 'reply',
23
+ });
24
+
25
+ const threadDisplayNames = ['thread 1', 'thread 2', 'thread 3'];
26
+ const createThreadObjs = (parents) => {
27
+ const threadObjects = [];
28
+
29
+ for (const msg of threadDisplayNames) {
30
+ for (const [ix, parentAct] of parents.entries()) {
31
+ // add threads to every other, plus randoms for variability
32
+ if (ix % 2 || Math.round(Math.random())) {
33
+ threadObjects.push({
34
+ displayName: `${parentAct.object.displayName} ${msg}`,
35
+ parentActivityId: parentAct.id,
36
+ });
37
+ }
38
+ }
39
+ }
40
+
41
+ return threadObjects;
42
+ };
43
+
44
+ describe('plugin-conversation', function () {
45
+ this.timeout(120000);
46
+
47
+ describe('when fetching conversations', () => {
48
+ let kirk, mccoy, participants, scott, webex, spock, suluEU, checkov;
49
+
50
+ before('create tests users and connect three to mercury', () =>
51
+ Promise.all([
52
+ testUsers.create({count: 5}),
53
+ testUsers.create({count: 1, config: {orgId: process.env.EU_PRIMARY_ORG_ID}}),
54
+ ]).then(([users, usersEU]) => {
55
+ [spock, mccoy, kirk, scott, checkov] = users;
56
+ [suluEU] = usersEU;
57
+ participants = [spock, mccoy, kirk];
58
+
59
+ spock.webex = new WebexCore({
60
+ credentials: {
61
+ supertoken: spock.token,
62
+ },
63
+ });
64
+
65
+ webex = spock.webex;
66
+
67
+ suluEU.webex = new WebexCore({
68
+ credentials: {
69
+ supertoken: suluEU.token,
70
+ },
71
+ });
72
+
73
+ checkov.webex = new WebexCore({
74
+ credentials: {
75
+ supertoken: checkov.token,
76
+ },
77
+ });
78
+
79
+ return Promise.all(
80
+ [suluEU, checkov, spock].map((user) =>
81
+ user.webex.internal.services
82
+ .waitForCatalog('postauth')
83
+ .then(() => user.webex.internal.mercury.connect())
84
+ )
85
+ );
86
+ })
87
+ );
88
+
89
+ after(() =>
90
+ Promise.all([suluEU, checkov, spock].map((user) => user.webex.internal.mercury.disconnect()))
91
+ );
92
+
93
+ describe('#download()', () => {
94
+ let sampleImageSmallOnePng = 'sample-image-small-one.png';
95
+
96
+ let conversation, conversationRequestSpy;
97
+
98
+ before('create conversation', () =>
99
+ webex.internal.conversation.create({participants}).then((c) => {
100
+ conversation = c;
101
+ })
102
+ );
103
+
104
+ before('fetch image fixture', () =>
105
+ fh.fetch(sampleImageSmallOnePng).then((res) => {
106
+ sampleImageSmallOnePng = res;
107
+ })
108
+ );
109
+
110
+ beforeEach(() => {
111
+ conversationRequestSpy = sinon.spy(webex.internal.conversation, 'request');
112
+ });
113
+
114
+ afterEach(() => conversationRequestSpy.restore());
115
+
116
+ it('rejects for invalid options argument', () =>
117
+ webex.internal.conversation
118
+ .share(conversation, [sampleImageSmallOnePng])
119
+ .then((activity) => {
120
+ const item = activity.object.files.items[0];
121
+
122
+ item.options = {
123
+ params: {
124
+ allow: 'invalidOption',
125
+ },
126
+ };
127
+
128
+ assert.isRejected(webex.internal.conversation.download(item));
129
+ }));
130
+
131
+ it('downloads and decrypts an encrypted file', () =>
132
+ webex.internal.conversation
133
+ .share(conversation, [sampleImageSmallOnePng])
134
+ .then((activity) => webex.internal.conversation.download(activity.object.files.items[0]))
135
+ .then((f) =>
136
+ fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
137
+ ));
138
+
139
+ it('emits download progress events for encrypted files', () =>
140
+ webex.internal.conversation
141
+ .share(conversation, [sampleImageSmallOnePng])
142
+ .then((activity) => {
143
+ const spy = sinon.spy();
144
+
145
+ return webex.internal.conversation
146
+ .download(activity.object.files.items[0])
147
+ .on('progress', spy)
148
+ .then(() => assert.called(spy));
149
+ }));
150
+
151
+ it('downloads and decrypts a file without a scr key', () =>
152
+ webex.internal.conversation
153
+ .download({
154
+ scr: {
155
+ loc: makeLocalUrl('/sample-image-small-one.png'),
156
+ },
157
+ })
158
+ .then((f) =>
159
+ fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
160
+ )
161
+ .then(() =>
162
+ conversationRequestSpy.returnValues[0].then((res) => {
163
+ assert.property(res.options.headers, 'cisco-no-http-redirect');
164
+ assert.property(res.options.headers, 'spark-user-agent');
165
+ assert.property(res.options.headers, 'trackingid');
166
+ })
167
+ ));
168
+
169
+ it('downloads and decrypts a non-encrypted file', () =>
170
+ webex.internal.conversation
171
+ .download({url: makeLocalUrl('/sample-image-small-one.png')})
172
+ .then((f) =>
173
+ fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
174
+ )
175
+ .then(() =>
176
+ conversationRequestSpy.returnValues[0].then((res) => {
177
+ assert.property(res.options.headers, 'cisco-no-http-redirect');
178
+ assert.property(res.options.headers, 'spark-user-agent');
179
+ assert.property(res.options.headers, 'trackingid');
180
+ })
181
+ ));
182
+
183
+ it('downloads non-encrypted file with specific options headers', () =>
184
+ webex.internal.conversation
185
+ .download(
186
+ {url: makeLocalUrl('/sample-image-small-one.png')},
187
+ {
188
+ headers: {
189
+ 'cisco-no-http-redirect': null,
190
+ 'spark-user-agent': null,
191
+ trackingid: null,
192
+ },
193
+ }
194
+ )
195
+ .then((f) =>
196
+ fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
197
+ )
198
+ .then(() =>
199
+ conversationRequestSpy.returnValues[0].then((res) => {
200
+ assert.isUndefined(res.options.headers['cisco-no-http-redirect']);
201
+ assert.isUndefined(res.options.headers['spark-user-agent']);
202
+ assert.isUndefined(res.options.headers.trackingid);
203
+ })
204
+ ));
205
+
206
+ it('emits download progress events for non-encrypted files', () => {
207
+ const spy = sinon.spy();
208
+
209
+ return webex.internal.conversation
210
+ .download({url: makeLocalUrl('/sample-image-small-one.png')})
211
+ .on('progress', spy)
212
+ .then((f) =>
213
+ fh.isMatchingFile(f, sampleImageSmallOnePng).then((result) => assert.isTrue(result))
214
+ )
215
+ .then(() => assert.called(spy));
216
+ });
217
+
218
+ describe('reads exif data and', () => {
219
+ let fileItem;
220
+ let sampleImagePortraitJpeg = 'Portrait_7.jpg';
221
+
222
+ before('fetch image fixture', () =>
223
+ fh.fetch(sampleImagePortraitJpeg).then((res) => {
224
+ sampleImagePortraitJpeg = res;
225
+ sampleImagePortraitJpeg.displayName = 'Portrait_7.jpg';
226
+ sampleImagePortraitJpeg.mimeType = 'image/jpeg';
227
+ })
228
+ );
229
+ it('does not add exif data', () =>
230
+ webex.internal.conversation
231
+ .share(conversation, [sampleImagePortraitJpeg])
232
+ .then((activity) => {
233
+ fileItem = activity.object.files.items[0];
234
+
235
+ return webex.internal.conversation.download(fileItem, {shouldNotAddExifData: true});
236
+ })
237
+ .then((f) => {
238
+ assert.equal(fileItem.orientation, undefined);
239
+
240
+ return fh.isMatchingFile(f, sampleImagePortraitJpeg);
241
+ })
242
+ .then((result) => assert.isTrue(result)));
243
+
244
+ it('adds exif data', () =>
245
+ webex.internal.conversation
246
+ .share(conversation, [sampleImagePortraitJpeg])
247
+ .then((activity) => {
248
+ fileItem = activity.object.files.items[0];
249
+
250
+ return webex.internal.conversation.download(fileItem);
251
+ })
252
+ .then((f) => {
253
+ assert.equal(fileItem.orientation, 7);
254
+
255
+ return fh.isMatchingFile(f, sampleImagePortraitJpeg);
256
+ })
257
+ .then((result) => assert.isTrue(result)));
258
+ });
259
+ });
260
+
261
+ describe('#get()', () => {
262
+ let conversation, conversation2;
263
+
264
+ before('create conversations', () =>
265
+ Promise.all([
266
+ webex.internal.conversation.create({participants: [mccoy.id]}).then((c) => {
267
+ conversation = c;
268
+ }),
269
+ webex.internal.conversation.create({participants: [scott.id]}).then((c) => {
270
+ conversation2 = c;
271
+ }),
272
+ ])
273
+ );
274
+
275
+ it('retrieves a single conversation by url', () =>
276
+ webex.internal.conversation.get({url: conversation.url}).then((c) => {
277
+ assert.equal(c.id, conversation.id);
278
+ assert.equal(c.url, conversation.url);
279
+ }));
280
+
281
+ it('retrieves a single conversation by id', () =>
282
+ webex.internal.conversation.get({id: conversation.id}).then((c) => {
283
+ assert.equal(c.id, conversation.id);
284
+ assert.equal(c.url, conversation.url);
285
+ }));
286
+
287
+ it('retrieves a 1:1 conversation by userId', () =>
288
+ webex.internal.conversation.get({user: mccoy}).then((c) => {
289
+ assert.equal(c.id, conversation.id);
290
+ assert.equal(c.url, conversation.url);
291
+ }));
292
+
293
+ it('retrieves a 1:1 conversation with a deleted user', () =>
294
+ webex.internal.conversation
295
+ .get({user: scott})
296
+ .then((c) => {
297
+ assert.equal(c.id, conversation2.id);
298
+ assert.equal(c.url, conversation2.url);
299
+ })
300
+ .then(() => testUsers.remove([scott]))
301
+ // add retries to address CI propagation delay
302
+ .then(() =>
303
+ retry(() => assert.isRejected(webex.internal.conversation.get({user: scott})))
304
+ )
305
+ .then(() =>
306
+ retry(() =>
307
+ webex.internal.conversation.get({user: scott}, {includeConvWithDeletedUserUUID: true})
308
+ )
309
+ )
310
+ .then((c) => {
311
+ assert.equal(c.id, conversation2.id);
312
+ assert.equal(c.url, conversation2.url);
313
+ }));
314
+
315
+ it('decrypts the contents of activities in the retrieved conversation', () =>
316
+ webex.internal.conversation
317
+ .post(conversation, {
318
+ displayName: 'Test Message',
319
+ })
320
+ .then(() =>
321
+ webex.internal.conversation.get({url: conversation.url}, {activitiesLimit: 50})
322
+ )
323
+ .then((c) => {
324
+ const posts = c.activities.items.filter((activity) => activity.verb === 'post');
325
+
326
+ assert.lengthOf(posts, 1);
327
+ assert.equal(posts[0].object.displayName, 'Test Message');
328
+ }));
329
+ });
330
+
331
+ describe('#list()', () => {
332
+ let conversation1, conversation2;
333
+
334
+ before('create conversations', () =>
335
+ webex.internal.conversation
336
+ .create({
337
+ displayName: 'test 1',
338
+ participants,
339
+ })
340
+ .then((c) => {
341
+ conversation1 = c;
342
+ })
343
+ .then(() =>
344
+ webex.internal.conversation.create({
345
+ displayName: 'test 2',
346
+ participants,
347
+ })
348
+ )
349
+
350
+ .then((c) => {
351
+ conversation2 = c;
352
+ })
353
+ );
354
+
355
+ it('retrieves a set of conversations', () =>
356
+ webex.internal.conversation
357
+ .list({
358
+ conversationsLimit: 2,
359
+ })
360
+ .then((conversations) => {
361
+ assert.include(map(conversations, 'url'), conversation1.url);
362
+ assert.include(map(conversations, 'url'), conversation2.url);
363
+ }));
364
+
365
+ it('retrieves a paginated set of conversations', () =>
366
+ webex.internal.conversation
367
+ .paginate({
368
+ conversationsLimit: 1,
369
+ personRefresh: false,
370
+ paginate: true,
371
+ })
372
+ .then((response) => {
373
+ const conversations = response.page.items;
374
+
375
+ assert.lengthOf(conversations, 1);
376
+ assert.equal(conversations[0].displayName, conversation2.displayName);
377
+
378
+ return webex.internal.conversation.paginate({page: response.page});
379
+ })
380
+ .then((response) => {
381
+ const conversations = response.page.items;
382
+
383
+ assert.lengthOf(conversations, 1);
384
+ assert.equal(conversations[0].displayName, conversation1.displayName);
385
+ }));
386
+
387
+ describe('with summary = true (ConversationsSummary)', () => {
388
+ it('retrieves all conversations using conversationsSummary', () =>
389
+ webex.internal.conversation
390
+ .list({
391
+ summary: true,
392
+ })
393
+ .then((conversations) => {
394
+ assert.include(map(conversations, 'url'), conversation1.url);
395
+ assert.include(map(conversations, 'url'), conversation2.url);
396
+ }));
397
+
398
+ it('retrieves a set of (1) conversations using conversationsLimit', () =>
399
+ webex.internal.conversation
400
+ .list({
401
+ summary: true,
402
+ conversationsLimit: 1,
403
+ })
404
+ .then((conversations) => {
405
+ assert.lengthOf(conversations, 1);
406
+ assert.include(map(conversations, 'url'), conversation2.url);
407
+ assert.include(map(conversations, 'displayName'), conversation2.displayName);
408
+ }));
409
+ });
410
+
411
+ describe('with deferDecrypt = true', () => {
412
+ it('retrieves a non-decrypted set of conversations each with a bound decrypt method', () =>
413
+ webex.internal.conversation
414
+ .list({
415
+ conversationsLimit: 2,
416
+ deferDecrypt: true,
417
+ })
418
+ .then(([c1, c2]) => {
419
+ assert.lengthOf(
420
+ c1.displayName.split('.'),
421
+ 5,
422
+ '5 periods implies this is a jwt and not a decrypted string'
423
+ );
424
+ assert.notInclude(['test 1, test 2'], c1.displayName);
425
+
426
+ assert.lengthOf(
427
+ c2.displayName.split('.'),
428
+ 5,
429
+ '5 periods implies this is a jwt and not a decrypted string'
430
+ );
431
+ assert.notInclude(['test 1, test 2'], c2.displayName);
432
+
433
+ return Promise.all([
434
+ c1.decrypt().then(() => assert.notInclude(['test 1, test 2'], c1.displayName)),
435
+ c2.decrypt().then(() => assert.notInclude(['test 1, test 2'], c2.displayName)),
436
+ ]);
437
+ }));
438
+ });
439
+
440
+ describe('with deferDecrypt && summary = true', () => {
441
+ it('retrieves a non-decrypted set of conversations each with a bound decrypt method', () =>
442
+ webex.internal.conversation
443
+ .list({
444
+ conversationsLimit: 2,
445
+ deferDecrypt: true,
446
+ summary: true,
447
+ })
448
+ .then(([c1, c2]) => {
449
+ assert.lengthOf(
450
+ c1.displayName.split('.'),
451
+ 5,
452
+ '5 periods implies this is a jwt and not a decrypted string'
453
+ );
454
+ assert.notInclude(['test 1, test 2'], c1.displayName);
455
+
456
+ assert.lengthOf(
457
+ c2.displayName.split('.'),
458
+ 5,
459
+ '5 periods implies this is a jwt and not a decrypted string'
460
+ );
461
+ assert.notInclude(['test 1, test 2'], c2.displayName);
462
+
463
+ return Promise.all([
464
+ c1.decrypt().then(() => assert.notInclude(['test 1, test 2'], c1.displayName)),
465
+ c2.decrypt().then(() => assert.notInclude(['test 1, test 2'], c2.displayName)),
466
+ ]);
467
+ }));
468
+ });
469
+
470
+ describe('with conversation from remote clusters', () => {
471
+ let conversation3, conversation4;
472
+
473
+ before('create conversations in EU cluster', () =>
474
+ Promise.all([
475
+ suluEU.webex.internal.conversation
476
+ .create({
477
+ displayName: 'eu test 1',
478
+ participants,
479
+ })
480
+ .then((c) => {
481
+ conversation3 = c;
482
+ }),
483
+ suluEU.webex.internal.conversation
484
+ .create({
485
+ displayName: 'eu test 2',
486
+ participants: [checkov.id, spock.id],
487
+ })
488
+ .then((c) => {
489
+ conversation4 = c;
490
+ }),
491
+ ])
492
+ );
493
+
494
+ it('retrieves local + remote cluster conversations', () =>
495
+ webex.internal.conversation.list().then((conversations) => {
496
+ assert.include(map(conversations, 'url'), conversation1.url);
497
+ assert.include(map(conversations, 'url'), conversation2.url);
498
+ assert.include(map(conversations, 'url'), conversation3.url);
499
+ assert.include(map(conversations, 'url'), conversation4.url);
500
+ }));
501
+
502
+ it('retrieves only remote cluter conversations if user does not have any local conversations', () =>
503
+ checkov.webex.internal.conversation.list().then((conversations) => {
504
+ assert.include(map(conversations, 'url'), conversation4.url);
505
+ assert.lengthOf(conversations, 1);
506
+ }));
507
+ });
508
+ });
509
+
510
+ describe('#listLeft()', () => {
511
+ let conversation;
512
+
513
+ before('create conversation', () =>
514
+ webex.internal.conversation.create({participants}).then((c) => {
515
+ conversation = c;
516
+ })
517
+ );
518
+
519
+ it('retrieves the conversations the current user has left', () =>
520
+ webex.internal.conversation
521
+ .listLeft()
522
+ .then((c) => {
523
+ assert.lengthOf(c, 0);
524
+
525
+ return webex.internal.conversation.leave(conversation);
526
+ })
527
+ .then(() => webex.internal.conversation.listLeft())
528
+ .then((c) => {
529
+ assert.lengthOf(c, 1);
530
+ assert.equal(c[0].id, conversation.id);
531
+ }));
532
+ });
533
+
534
+ describe('#listActivities()', () => {
535
+ let conversation;
536
+
537
+ before('create conversation with activity', () =>
538
+ webex.internal.conversation.create({participants}).then((c) => {
539
+ conversation = c;
540
+ assert.lengthOf(conversation.participants.items, 3);
541
+
542
+ return webex.internal.conversation.post(conversation, {displayName: 'first message'});
543
+ })
544
+ );
545
+
546
+ it('retrieves activities for the specified conversation', () =>
547
+ webex.internal.conversation
548
+ .listActivities({conversationUrl: conversation.url})
549
+ .then((activities) => {
550
+ assert.isArray(activities);
551
+ assert.lengthOf(activities, 2);
552
+ }));
553
+ });
554
+
555
+ describe('#listThreads()', () => {
556
+ let webex2;
557
+
558
+ before('connect mccoy to mercury', () => {
559
+ webex2 = new WebexCore({
560
+ credentials: {
561
+ authorization: mccoy.token,
562
+ },
563
+ });
564
+
565
+ return webex2.internal.mercury.connect();
566
+ });
567
+
568
+ after(() => webex2 && webex2.internal.mercury.disconnect());
569
+
570
+ let conversation;
571
+ let parent;
572
+
573
+ before('create conversation', () =>
574
+ webex.internal.conversation.create({participants}).then((c) => {
575
+ conversation = c;
576
+ assert.lengthOf(conversation.participants.items, 3);
577
+
578
+ return webex2.internal.conversation
579
+ .post(conversation, {displayName: 'first message'})
580
+ .then((parentActivity) => {
581
+ parent = parentActivity;
582
+ });
583
+ })
584
+ );
585
+
586
+ it('retrieves threads()', () =>
587
+ webex2.internal.conversation
588
+ .post(conversation, 'thread1', {
589
+ parentActivityId: parent.id,
590
+ activityType: 'reply',
591
+ })
592
+ .then(() => webex2.internal.conversation.listThreads())
593
+ .then((thread) => {
594
+ assert.equal(thread.length, 1);
595
+ const firstThread = thread[0];
596
+
597
+ assert.equal(firstThread.childType, 'reply');
598
+ assert.equal(firstThread.parentActivityId, parent.id);
599
+ assert.equal(firstThread.conversationId, conversation.id);
600
+ assert.equal(firstThread.childActivities.length, 1);
601
+
602
+ const childActivity = firstThread.childActivities[0];
603
+
604
+ assert.equal(childActivity.objectType, 'activity');
605
+ assert.equal(childActivity.object.displayName, 'thread1');
606
+ }));
607
+ });
608
+
609
+ describe('#listMentions()', () => {
610
+ let webex2;
611
+
612
+ before('connect mccoy to mercury', () => {
613
+ webex2 = new WebexCore({
614
+ credentials: {
615
+ authorization: mccoy.token,
616
+ },
617
+ });
618
+
619
+ return webex2.internal.mercury.connect();
620
+ });
621
+
622
+ after(() => webex2 && webex2.internal.mercury.disconnect());
623
+
624
+ let conversation;
625
+
626
+ before('create conversation', () =>
627
+ webex.internal.conversation.create({participants}).then((c) => {
628
+ conversation = c;
629
+ assert.lengthOf(conversation.participants.items, 3);
630
+ })
631
+ );
632
+
633
+ it('retrieves activities in which the current user was mentioned', () =>
634
+ webex2.internal.conversation
635
+ .post(conversation, {
636
+ displayName: 'Green blooded hobgloblin',
637
+ content: `<webex-mention data-object-type="person" data-object-id="${spock.id}">Green blooded hobgloblin</webex-mention>`,
638
+ mentions: {
639
+ items: [
640
+ {
641
+ id: `${spock.id}`,
642
+ objectType: 'person',
643
+ },
644
+ ],
645
+ },
646
+ })
647
+ .then((activity) =>
648
+ webex.internal.conversation
649
+ .listMentions({sinceDate: Date.parse(activity.published) - 1})
650
+ .then((mentions) => {
651
+ assert.lengthOf(mentions, 1);
652
+ assert.equal(mentions[0].url, activity.url);
653
+ })
654
+ ));
655
+ });
656
+
657
+ // TODO: add testing for bulk_activities_fetch() with clusters later
658
+ describe('#bulkActivitiesFetch()', () => {
659
+ let jenny, maria, dan, convo1, convo2, euConvo1;
660
+ let webex3;
661
+
662
+ before('create tests users and connect one to mercury', () =>
663
+ testUsers.create({count: 4}).then((users) => {
664
+ [jenny, maria, dan] = users;
665
+
666
+ webex3 = new WebexCore({
667
+ credentials: {
668
+ authorization: jenny.token,
669
+ },
670
+ });
671
+
672
+ return webex3.internal.mercury.connect();
673
+ })
674
+ );
675
+
676
+ after(() => webex3 && webex3.internal.mercury.disconnect());
677
+
678
+ before('create conversation 1', () =>
679
+ webex3.internal.conversation.create({participants: [jenny, maria]}).then((c1) => {
680
+ convo1 = c1;
681
+ })
682
+ );
683
+
684
+ before('create conversation 2', () =>
685
+ webex3.internal.conversation.create({participants: [jenny, dan]}).then((c2) => {
686
+ convo2 = c2;
687
+ })
688
+ );
689
+
690
+ before('create conversations in EU cluster', () =>
691
+ suluEU.webex.internal.conversation
692
+ .create({
693
+ displayName: 'eu test 1',
694
+ participants: [jenny, suluEU, dan],
695
+ })
696
+ .then((c) => {
697
+ euConvo1 = c;
698
+ })
699
+ );
700
+
701
+ before('add comments to convo1, and check post requests successfully went through', () =>
702
+ webex3.internal.conversation
703
+ .post(convo1, {displayName: 'BAGELS (O)'})
704
+ .then((c1) => {
705
+ assert.equal(c1.object.displayName, 'BAGELS (O)');
706
+
707
+ return webex3.internal.conversation.post(convo1, {displayName: 'Cream Cheese'});
708
+ })
709
+ .then((c2) => {
710
+ assert.equal(c2.object.displayName, 'Cream Cheese');
711
+ })
712
+ );
713
+
714
+ before('add comments to convo2, and check post requests successfully went through', () =>
715
+ webex3.internal.conversation
716
+ .post(convo2, {displayName: 'Want to head to lunch soon?'})
717
+ .then((c1) => {
718
+ assert.equal(c1.object.displayName, 'Want to head to lunch soon?');
719
+
720
+ return webex3.internal.conversation.post(convo2, {displayName: 'Sure :)'});
721
+ })
722
+ .then((c2) => {
723
+ assert.equal(c2.object.displayName, 'Sure :)');
724
+
725
+ return webex3.internal.conversation.post(convo2, {displayName: 'where?'});
726
+ })
727
+ .then((c3) => {
728
+ assert.equal(c3.object.displayName, 'where?');
729
+
730
+ return webex3.internal.conversation.post(convo2, {displayName: 'Meekong Bar!'});
731
+ })
732
+ .then((c4) => {
733
+ assert.equal(c4.object.displayName, 'Meekong Bar!');
734
+ })
735
+ );
736
+
737
+ before('add comments to euConvo1, and check post requests successfully went through', () =>
738
+ suluEU.webex.internal.conversation.post(euConvo1, {displayName: 'Hello'}).then((c1) => {
739
+ assert.equal(c1.object.displayName, 'Hello');
740
+ })
741
+ );
742
+
743
+ it('retrieves activities from a single conversation', () =>
744
+ webex3.internal.conversation
745
+ .listActivities({conversationUrl: convo1.url})
746
+ .then((convoActivities) => {
747
+ const activityURLs = [];
748
+ const expectedActivities = [];
749
+
750
+ convoActivities.forEach((a) => {
751
+ if (a.verb === 'post') {
752
+ activityURLs.push(a.url);
753
+ expectedActivities.push(a);
754
+ }
755
+ });
756
+
757
+ return webex3.internal.conversation
758
+ .bulkActivitiesFetch(activityURLs)
759
+ .then((bulkFetchedActivities) => {
760
+ assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
761
+ assert.equal(
762
+ bulkFetchedActivities[0].object.displayName,
763
+ expectedActivities[0].object.displayName
764
+ );
765
+ assert.equal(
766
+ bulkFetchedActivities[1].object.displayName,
767
+ expectedActivities[1].object.displayName
768
+ );
769
+ });
770
+ }));
771
+
772
+ it('retrieves activities from multiple conversations', () => {
773
+ const activityURLs = [];
774
+ const expectedActivities = [];
775
+
776
+ return webex3.internal.conversation
777
+ .listActivities({conversationUrl: convo1.url})
778
+ .then((convo1Activities) => {
779
+ // gets all post activity urls from convo1
780
+ convo1Activities.forEach((a1) => {
781
+ if (a1.verb === 'post') {
782
+ activityURLs.push(a1.url);
783
+ expectedActivities.push(a1);
784
+ }
785
+ });
786
+
787
+ return webex3.internal.conversation.listActivities({conversationUrl: convo2.url});
788
+ })
789
+ .then((convo2Activities) => {
790
+ // gets activity urls of only comment 3 and 4 from convo2
791
+ [3, 4].forEach((i) => {
792
+ activityURLs.push(convo2Activities[i].url);
793
+ expectedActivities.push(convo2Activities[i]);
794
+ });
795
+
796
+ return webex3.internal.conversation
797
+ .bulkActivitiesFetch(activityURLs)
798
+ .then((bulkFetchedActivities) => {
799
+ assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
800
+ assert.equal(
801
+ bulkFetchedActivities[0].object.displayName,
802
+ expectedActivities[0].object.displayName
803
+ );
804
+ assert.equal(
805
+ bulkFetchedActivities[1].object.displayName,
806
+ expectedActivities[1].object.displayName
807
+ );
808
+ assert.equal(
809
+ bulkFetchedActivities[2].object.displayName,
810
+ expectedActivities[2].object.displayName
811
+ );
812
+ assert.equal(
813
+ bulkFetchedActivities[3].object.displayName,
814
+ expectedActivities[3].object.displayName
815
+ );
816
+ });
817
+ });
818
+ });
819
+
820
+ it('given a activity url that does not exist, should return []', () => {
821
+ const mockURL =
822
+ 'https://conversation-intb.ciscospark.com/conversation/api/v1/activities/6d8c7c90-a770-11e9-bcfb-6616ead99ac3';
823
+
824
+ webex3.internal.conversation
825
+ .bulkActivitiesFetch([mockURL])
826
+ .then((bulkFetchedActivities) => {
827
+ assert.equal(bulkFetchedActivities, []);
828
+ });
829
+ });
830
+
831
+ it('retrieves activities from multiple conversations passing in base convo url', () => {
832
+ const activityURLs = [];
833
+ const expectedActivities = [];
834
+
835
+ return webex3.internal.conversation
836
+ .listActivities({conversationUrl: convo1.url})
837
+ .then((convo1Activities) => {
838
+ // gets all post activity urls from convo1
839
+ convo1Activities.forEach((a1) => {
840
+ if (a1.verb === 'post') {
841
+ activityURLs.push(a1.url);
842
+ expectedActivities.push(a1);
843
+ }
844
+ });
845
+
846
+ return webex3.internal.conversation.listActivities({conversationUrl: convo2.url});
847
+ })
848
+ .then((convo2Activities) => {
849
+ // gets activity urls of only comment 3 and 4 from convo2
850
+ [3, 4].forEach((i) => {
851
+ activityURLs.push(convo2Activities[i].url);
852
+ expectedActivities.push(convo2Activities[i]);
853
+ });
854
+
855
+ return webex3.internal.conversation
856
+ .bulkActivitiesFetch(activityURLs, undefined, {url: process.env.CONVERSATION_SERVICE})
857
+ .then((bulkFetchedActivities) => {
858
+ assert.lengthOf(bulkFetchedActivities, expectedActivities.length);
859
+ assert.equal(
860
+ bulkFetchedActivities[0].object.displayName,
861
+ expectedActivities[0].object.displayName
862
+ );
863
+ assert.equal(
864
+ bulkFetchedActivities[1].object.displayName,
865
+ expectedActivities[1].object.displayName
866
+ );
867
+ assert.equal(
868
+ bulkFetchedActivities[2].object.displayName,
869
+ expectedActivities[2].object.displayName
870
+ );
871
+ assert.equal(
872
+ bulkFetchedActivities[3].object.displayName,
873
+ expectedActivities[3].object.displayName
874
+ );
875
+ });
876
+ });
877
+ });
878
+
879
+ it('retrieves activities from conversations passing in base convo url from another cluster', () => {
880
+ const activityURLs = [];
881
+ const expectedActivities = [];
882
+
883
+ return webex3.internal.conversation
884
+ .listActivities({conversationUrl: euConvo1.url})
885
+ .then((euConvo1Activities) => {
886
+ const convoUrlRegex = /(.*)\/activities/;
887
+
888
+ activityURLs.push(euConvo1Activities[1].url);
889
+ expectedActivities.push(euConvo1Activities[1]);
890
+ const match = convoUrlRegex.exec(euConvo1Activities[1].url);
891
+ const convoUrl = match[1];
892
+
893
+ return webex3.internal.conversation.bulkActivitiesFetch(activityURLs, {url: convoUrl});
894
+ })
895
+ .then((bulkFetchedActivities) => {
896
+ assert.lengthOf(bulkFetchedActivities, 1);
897
+ assert.equal(
898
+ bulkFetchedActivities[0].object.displayName,
899
+ expectedActivities[0].object.displayName
900
+ );
901
+ });
902
+ });
903
+ });
904
+
905
+ describe('#listParentActivityIds', () => {
906
+ let conversation, parent;
907
+
908
+ beforeEach('create conversation with activity', () =>
909
+ webex.internal.conversation
910
+ .create({participants})
911
+ .then((c) => {
912
+ conversation = c;
913
+
914
+ return webex.internal.conversation.post(conversation, {displayName: 'first message'});
915
+ })
916
+ .then((parentAct) => {
917
+ parent = parentAct;
918
+ })
919
+ );
920
+
921
+ it('retrieves parent IDs for thread parents()', () =>
922
+ webex.internal.conversation
923
+ .post(
924
+ conversation,
925
+ {displayName: 'first thread reply'},
926
+ {
927
+ parentActivityId: parent.id,
928
+ activityType: 'reply',
929
+ }
930
+ )
931
+ .then(({parent: parentObj} = {}) => {
932
+ assert.equal(parentObj.type, 'reply');
933
+ assert.equal(parentObj.id, parent.id);
934
+
935
+ return webex.internal.conversation.listParentActivityIds(conversation.url, {
936
+ activityType: 'reply',
937
+ });
938
+ })
939
+ .then(({reply}) => {
940
+ assert.include(reply, parent.id);
941
+ }));
942
+
943
+ it('retrieves parent IDs for edits', () =>
944
+ webex.internal.conversation
945
+ .post(conversation, 'edited', {
946
+ parent: {
947
+ id: parent.id,
948
+ type: 'edit',
949
+ },
950
+ })
951
+ .then((edit) => {
952
+ assert.equal(edit.parent.type, 'edit');
953
+ assert.equal(edit.parent.id, parent.id);
954
+
955
+ return webex.internal.conversation.listParentActivityIds(conversation.url, {
956
+ activityType: 'edit',
957
+ });
958
+ })
959
+ .then(({edit}) => {
960
+ assert.include(edit, parent.id);
961
+ }));
962
+
963
+ it('retrieves parent IDs for reactions', () =>
964
+ webex.internal.conversation
965
+ .addReaction(conversation, 'heart', parent)
966
+ .then((reaction) => {
967
+ assert.equal(reaction.parent.type, 'reaction');
968
+ assert.equal(reaction.parent.id, parent.id);
969
+
970
+ return webex.internal.conversation.listParentActivityIds(conversation.url, {
971
+ activityType: 'reaction',
972
+ });
973
+ })
974
+ .then(({reaction}) => {
975
+ assert.include(reaction, parent.id);
976
+ }));
977
+ });
978
+
979
+ describe('#listChildActivitiesByParentId()', () => {
980
+ let conversation, parent;
981
+ let replies;
982
+
983
+ before('create conversation with thread replies', () =>
984
+ webex.internal.conversation
985
+ .create({participants})
986
+ .then((c) => {
987
+ conversation = c;
988
+
989
+ return webex.internal.conversation.post(conversation, {displayName: 'first message'});
990
+ })
991
+ .then((parentAct) => {
992
+ parent = parentAct;
993
+
994
+ const messages = ['thread 1', 'thread 2', 'thread 3'];
995
+
996
+ return Promise.all(
997
+ messages.map((msg) =>
998
+ webex.internal.conversation.post(conversation, msg, {
999
+ parentActivityId: parent.id,
1000
+ activityType: 'reply',
1001
+ })
1002
+ )
1003
+ );
1004
+ })
1005
+ .then((repliesArr) => {
1006
+ replies = repliesArr;
1007
+ })
1008
+ );
1009
+
1010
+ it('retrieves thread reply activities for a given parent', () =>
1011
+ webex.internal.conversation
1012
+ .listChildActivitiesByParentId(conversation.url, parent.id, 'reply')
1013
+ .then((response) => {
1014
+ const {items} = response.body;
1015
+
1016
+ items.forEach((threadAct) => {
1017
+ assert.include(
1018
+ replies.map((reply) => reply.id),
1019
+ threadAct.id
1020
+ );
1021
+ });
1022
+ }));
1023
+ });
1024
+
1025
+ describe('#_listActivitiesThreadOrdered', () => {
1026
+ let conversation, firstParentBatch, secondParentBatch, getOlder, jumpToActivity;
1027
+
1028
+ const minActivities = 10;
1029
+ const displayNames = [
1030
+ 'first message',
1031
+ 'second message',
1032
+ 'third message',
1033
+ 'fourth message',
1034
+ 'fifth message',
1035
+ 'sixth message',
1036
+ 'seventh message',
1037
+ 'eighth message',
1038
+ 'ninth message',
1039
+ ];
1040
+
1041
+ const initializeGenerator = () =>
1042
+ webex.internal.conversation.listActivitiesThreadOrdered({
1043
+ conversationUrl: conversation.url,
1044
+ minActivities,
1045
+ });
1046
+
1047
+ before(() =>
1048
+ webex.internal.conversation
1049
+ .create({participants})
1050
+ .then((c) => {
1051
+ conversation = c;
1052
+
1053
+ return c;
1054
+ })
1055
+ .then((c) => Promise.all(displayNames.slice(0, 5).map(postMessage(webex, c))))
1056
+ .then((parents) => {
1057
+ firstParentBatch = parents;
1058
+
1059
+ return Promise.all(createThreadObjs(parents).map(postReply(webex, conversation)));
1060
+ })
1061
+ .then(() => Promise.all(displayNames.slice(4).map(postMessage(webex, conversation))))
1062
+ .then((parents) => {
1063
+ secondParentBatch = parents;
1064
+
1065
+ return Promise.all(createThreadObjs(parents).map(postReply(webex, conversation)));
1066
+ })
1067
+ );
1068
+
1069
+ beforeEach(() => {
1070
+ const funcs = initializeGenerator();
1071
+
1072
+ getOlder = funcs.getOlder;
1073
+ jumpToActivity = funcs.jumpToActivity;
1074
+ });
1075
+
1076
+ it('should return more than or exactly N minimum activities', () =>
1077
+ getOlder().then(({value}) => {
1078
+ assert.isAtLeast(value.length, minActivities);
1079
+ }));
1080
+
1081
+ it('should return edit activity ID as activity ID when an activity has been edited', () => {
1082
+ const lastParent = secondParentBatch[secondParentBatch.length - 1];
1083
+
1084
+ const message = {
1085
+ displayName: 'edited',
1086
+ content: 'edited',
1087
+ };
1088
+
1089
+ const editingActivity = Object.assign(
1090
+ {
1091
+ parent: {
1092
+ id: lastParent.id,
1093
+ type: 'edit',
1094
+ },
1095
+ },
1096
+ {
1097
+ object: message,
1098
+ }
1099
+ );
1100
+
1101
+ return webex.internal.conversation
1102
+ .post(conversation, message, editingActivity)
1103
+ .then(() => getOlder())
1104
+ .then(({value}) => {
1105
+ const activities = value.map((act) => act.activity);
1106
+ const editedAct = find(activities, (act) => act.editParent);
1107
+
1108
+ assert.equal(editedAct.editParent.id, lastParent.id);
1109
+ assert.notEqual(editedAct.id, lastParent.id);
1110
+ });
1111
+ });
1112
+
1113
+ it('should return activities in thread order', () =>
1114
+ getOlder().then((data) => {
1115
+ const {value} = data;
1116
+ const oldestAct = value[0].activity;
1117
+ const newestAct = value[value.length - 1].activity;
1118
+
1119
+ const oldestThreadIx = findIndex(value, ['activity.parent.type', 'reply']);
1120
+ const oldestParent = value[oldestThreadIx - 1].activity;
1121
+
1122
+ assert.isTrue(oldestAct.published < newestAct.published);
1123
+
1124
+ assert.doesNotHaveAnyKeys(oldestParent, 'parentActivityId');
1125
+ assert.isTrue(oldestParent.object.objectType === 'comment');
1126
+ }));
1127
+
1128
+ it('should return next batch when getOlder is called a second time', () => {
1129
+ let firstBatch;
1130
+
1131
+ return getOlder()
1132
+ .then(({value}) => {
1133
+ firstBatch = value;
1134
+
1135
+ return getOlder();
1136
+ })
1137
+ .then(({value}) => {
1138
+ const secondBatch = value;
1139
+
1140
+ const oldestRootInFirstBatch = find(firstBatch, [
1141
+ 'activity.object.objectType',
1142
+ 'comment',
1143
+ ]).activity;
1144
+ const newestRootInSecondBatch = findLast(secondBatch, [
1145
+ 'activity.object.objectType',
1146
+ 'comment',
1147
+ ]).activity;
1148
+
1149
+ assert.isTrue(oldestRootInFirstBatch.published > newestRootInSecondBatch.published);
1150
+ });
1151
+ });
1152
+
1153
+ it('should return done as true when no more activities can be fetched', () => {
1154
+ const {getOlder: getOlderWithLargeMin} =
1155
+ webex.internal.conversation.listActivitiesThreadOrdered({
1156
+ conversationId: conversation.id,
1157
+ minActivities: 50,
1158
+ });
1159
+
1160
+ return getOlderWithLargeMin().then(({done}) => {
1161
+ assert.isTrue(done);
1162
+ });
1163
+ });
1164
+
1165
+ describe('jumpToActivity()', () => {
1166
+ let _listActivitiesThreadOrderedSpy;
1167
+
1168
+ beforeEach(() => {
1169
+ _listActivitiesThreadOrderedSpy = sinon.spy(
1170
+ webex.internal.conversation,
1171
+ '_listActivitiesThreadOrdered'
1172
+ );
1173
+ });
1174
+
1175
+ afterEach(() => {
1176
+ webex.internal.conversation._listActivitiesThreadOrdered.restore();
1177
+ });
1178
+
1179
+ it('should return searched-for activity with surrounding activities when jumpToActivity is called with an activity', () => {
1180
+ const search = firstParentBatch[firstParentBatch.length - 1];
1181
+
1182
+ return jumpToActivity(search).then(({value}) => {
1183
+ const searchedForActivityIx = findIndex(value, ['id', search.id]);
1184
+
1185
+ assert.isFalse(searchedForActivityIx === -1);
1186
+ assert.isTrue(searchedForActivityIx > 0);
1187
+ assert.isTrue(searchedForActivityIx < value.length);
1188
+ });
1189
+ });
1190
+
1191
+ it('should return all activities in a space when jumping to an activity in a space with less activities than the asked-for activities limit', () =>
1192
+ webex.internal.conversation
1193
+ .create({participants: [scott.id]})
1194
+ .then((c) =>
1195
+ webex.internal.conversation
1196
+ .post(c, {displayName: 'first message'})
1197
+ .then((m) =>
1198
+ webex.internal.conversation
1199
+ .listActivities({conversationUrl: c.url})
1200
+ .then((acts) => jumpToActivity(m).then(() => acts.length))
1201
+ )
1202
+ )
1203
+ .then((actCount) => {
1204
+ assert.isTrue(actCount < minActivities);
1205
+ }));
1206
+
1207
+ it('should return all activities in a space when jumping to an activity in a space with more activities than the asked-for activities limit', () =>
1208
+ webex.internal.conversation
1209
+ .create({participants: [scott.id]})
1210
+ .then((c) => {
1211
+ const $posts = [];
1212
+
1213
+ // eslint-disable-next-line no-plusplus
1214
+ for (let i = 0; i < 15; i++) {
1215
+ $posts.push(webex.internal.conversation.post(c, {displayName: `message ${i}`}));
1216
+ }
1217
+
1218
+ return Promise.all($posts).then(() =>
1219
+ webex.internal.conversation.post(c, {displayName: 'message last'})
1220
+ );
1221
+ })
1222
+ .then((lastPost) => jumpToActivity(lastPost))
1223
+ .then(({value: acts}) => {
1224
+ assert.isAtLeast(acts.length, minActivities);
1225
+
1226
+ const firstAct = acts[0].activity;
1227
+
1228
+ assert.notEqual(firstAct.verb, 'create');
1229
+ }));
1230
+
1231
+ it('should re-initialize _listActivitiesThreadOrdered when jumpToActivity is called with a new URL', () => {
1232
+ let conversation2, msg;
1233
+
1234
+ return webex.internal.conversation
1235
+ .create({participants: [scott.id]})
1236
+ .then((c) => {
1237
+ conversation2 = c;
1238
+
1239
+ return webex.internal.conversation.post(conversation2, {
1240
+ displayName: 'first message',
1241
+ });
1242
+ })
1243
+ .then((m) => {
1244
+ msg = m;
1245
+
1246
+ return jumpToActivity(msg);
1247
+ })
1248
+ .then(() => {
1249
+ assert.isTrue(_listActivitiesThreadOrderedSpy.args[0][0].url === conversation2.url);
1250
+ });
1251
+ });
1252
+ });
1253
+ });
1254
+ });
1255
+ });