@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,823 +1,823 @@
1
- /*!
2
- * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
- */
4
- /* eslint-disable no-underscore-dangle */
5
- import {assert} from '@webex/test-helper-chai';
6
- import MockWebex from '@webex/test-helper-mock-webex';
7
- import sinon from 'sinon';
8
- import Conversation from '@webex/internal-plugin-conversation';
9
-
10
- import {
11
- rootActivityManager,
12
- getLoopCounterFailsafe,
13
- noMoreActivitiesManager,
14
- bookendManager,
15
- activityManager,
16
- } from '../../../src/activity-thread-ordering';
17
- import {ACTIVITY_TYPES, getActivityType, OLDER, NEWER} from '../../../src/activities';
18
-
19
- describe('plugin-conversation', () => {
20
- describe('Conversation', () => {
21
- let webex;
22
-
23
- const convoUrl = 'https://conv-test.wbx2.com/conversation';
24
-
25
- beforeEach(() => {
26
- webex = new MockWebex({
27
- children: {
28
- conversation: Conversation,
29
- },
30
- });
31
-
32
- webex.internal.services = {};
33
- webex.internal.services.get = sinon.stub().returns(Promise.resolve(convoUrl));
34
- webex.internal.services.getServiceUrlFromClusterId = sinon.stub().returns(convoUrl);
35
- });
36
-
37
- describe('processInmeetingchatEvent()', () => {
38
- beforeEach(() => {
39
- webex.transform = sinon.stub().callsFake((obj) => Promise.resolve(obj));
40
- });
41
-
42
- it('calls transform with correct arguments', async () => {
43
- const event = {name: 'test-event'};
44
-
45
- await webex.internal.conversation.processInmeetingchatEvent(event);
46
-
47
- assert.calledWith(webex.transform, 'inbound', event);
48
- });
49
-
50
- it('calls transform and returns event', async () => {
51
- const event = {name: 'test-event'};
52
- const returnValue = await webex.internal.conversation.processInmeetingchatEvent(event);
53
-
54
- assert.equal(event, returnValue);
55
- });
56
- });
57
-
58
- describe('#_inferConversationUrl', () => {
59
- const testConvo = {test: 'convo'};
60
-
61
- it('Returns given convo if no id', () =>
62
- webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
63
- assert.notCalled(webex.internal.feature.getFeature);
64
- assert.notCalled(webex.internal.services.get);
65
- assert.equal(convo.test, 'convo');
66
- }));
67
-
68
- describe('HA is disabled', () => {
69
- beforeEach(() => {
70
- webex.internal.feature.getFeature = sinon.stub().returns(Promise.resolve(false));
71
- testConvo.id = 'id1';
72
- });
73
- it('returns unmodified convo if URL is defined', () => {
74
- testConvo.url = 'http://example.com';
75
-
76
- return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
77
- assert.called(webex.internal.feature.getFeature);
78
- assert.notCalled(webex.internal.services.get);
79
- assert.equal(convo.url, 'http://example.com');
80
- });
81
- });
82
- it('builds URL if not defined', () => {
83
- delete testConvo.url;
84
-
85
- return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
86
- assert.called(webex.internal.feature.getFeature);
87
- assert.called(webex.internal.services.get);
88
- assert.equal(convo.url, `${convoUrl}/conversations/id1`);
89
- });
90
- });
91
- });
92
- describe('HA is enabled', () => {
93
- beforeEach(() => {
94
- webex.internal.feature.getFeature = sinon.stub().returns(Promise.resolve(true));
95
- testConvo.id = 'id1';
96
- });
97
- it('builds URL if already defined', () => {
98
- testConvo.url = 'https://example.com';
99
-
100
- return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
101
- assert.called(webex.internal.feature.getFeature);
102
- assert.called(webex.internal.services.get);
103
- assert.equal(convo.url, `${convoUrl}/conversations/id1`);
104
- });
105
- });
106
- it('builds URL if not defined', () => {
107
- delete testConvo.url;
108
-
109
- return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
110
- assert.called(webex.internal.feature.getFeature);
111
- assert.called(webex.internal.services.get);
112
- assert.equal(convo.url, `${convoUrl}/conversations/id1`);
113
- });
114
- });
115
- });
116
- });
117
-
118
- describe('getConvoUrl', () => {
119
- it('should not return a promise', () => {
120
- try {
121
- webex.internal.conversation.getConvoUrl({url: 'convoUrl'}).then();
122
- } catch (error) {
123
- assert.equal(
124
- error.message,
125
- 'webex.internal.conversation.getConvoUrl(...).then is not a function'
126
- );
127
- }
128
- });
129
-
130
- it('should return the url if a url is provided', () => {
131
- const url = webex.internal.conversation.getConvoUrl({url: convoUrl});
132
-
133
- assert.equal(url, convoUrl);
134
- });
135
- });
136
-
137
- describe('#getUrlFromClusterId', () => {
138
- it('should convert a "us" cluster to WEBEX_CONVERSATION_DEFAULT_CLUSTER cluster', async () => {
139
- await webex.internal.conversation.getUrlFromClusterId({cluster: 'us'});
140
-
141
- sinon.assert.calledWith(webex.internal.services.getServiceUrlFromClusterId, {cluster: 'us'});
142
- });
143
-
144
- it('should add the cluster service when missing', async () => {
145
- await webex.internal.conversation.getUrlFromClusterId({cluster: 'urn:TEAM:us-west-2_r'});
146
-
147
- sinon.assert.calledWith(webex.internal.services.getServiceUrlFromClusterId, {cluster: 'urn:TEAM:us-west-2_r'});
148
- });
149
- });
150
-
151
- describe('paginate', () => {
152
- it('should throw an error if a page is passed with no links', () => {
153
- webex.internal.conversation.paginate({page: {}}).catch((error) => {
154
- assert.equal(error.message, 'No link to follow for the provided page');
155
- });
156
- });
157
- });
158
-
159
- describe('#getReactionSummaryByParentId()', () => {
160
- beforeEach(() => {
161
- webex.request = sinon.stub().returns(
162
- Promise.resolve({
163
- body: {
164
- children: [
165
- {type: 'reactionSelfSummary'},
166
- {type: 'reactionSelfSummary'},
167
- {type: 'reactionSummary'},
168
- {type: 'notAReaction'},
169
- ],
170
- },
171
- })
172
- );
173
- });
174
-
175
- it('should call request', () =>
176
- webex.internal.conversation.getReactionSummaryByParentId(convoUrl, 'test-id').then(() => {
177
- assert.called(webex.request);
178
- }));
179
-
180
- it('should not retrieve non reaction summary objects', () =>
181
- webex.internal.conversation
182
- .getReactionSummaryByParentId(convoUrl, 'test-id')
183
- .then((result) => {
184
- assert.equal(result.length, 3);
185
- assert.notInclude(result, {type: 'notAReaction'});
186
- }));
187
- });
188
-
189
- describe('#listAllChildActivitiesByParentId()', () => {
190
- const options = {
191
- conversationUrl: convoUrl,
192
- activityParentId: '123-abc',
193
- query: {activityType: 'reply'},
194
- };
195
- let dayCount = 0;
196
- const createActivityItemBatch = (itemCount) => {
197
- const counter = [...Array(itemCount).keys()];
198
-
199
- dayCount += 1;
200
-
201
- return counter.map((n) => ({
202
- published: new Date(2020, 0, 1, dayCount, n),
203
- }));
204
- };
205
-
206
- const createMockChildResponse = (itemsToReturn = 10) => {
207
- const response = {
208
- body: {
209
- items: createActivityItemBatch(itemsToReturn),
210
- },
211
- headers: {
212
- link: '<https://www.cisco.com>; rel=next',
213
- },
214
- };
215
-
216
- return response;
217
- };
218
-
219
- beforeEach(() => {
220
- webex.request = sinon
221
- .stub()
222
- .onCall(0)
223
- .returns(Promise.resolve(createMockChildResponse()))
224
- .onCall(1)
225
- .returns(Promise.resolve(createMockChildResponse()))
226
- .onCall(2)
227
- .returns(Promise.resolve(createMockChildResponse(3)))
228
- .returns(
229
- Promise.resolve(
230
- Promise.resolve({
231
- body: {
232
- items: [],
233
- },
234
- headers: {},
235
- })
236
- )
237
- );
238
- });
239
- it('retrieves correct children count', () =>
240
- webex.internal.conversation.listAllChildActivitiesByParentId(options).then((res) => {
241
- assert.equal(res.length, 23);
242
- }));
243
-
244
- it('calls #listChildActivitiesByParentId() to initiate the request', () => {
245
- const spy = sinon.spy(webex.internal.conversation, 'listChildActivitiesByParentId');
246
-
247
- return webex.internal.conversation.listAllChildActivitiesByParentId(options).then(() => {
248
- assert(spy.calledOnce);
249
- });
250
- });
251
-
252
- it('returns children in ascending published order', () =>
253
- webex.internal.conversation.listAllChildActivitiesByParentId(options).then((res) => {
254
- const firstMessageOlderThanLastMessage = res[0].published < res[res.length - 1].published;
255
-
256
- assert.isTrue(firstMessageOlderThanLastMessage, 'activities out of order');
257
- }));
258
- });
259
-
260
- describe('#listActivitiesThreadOrdered()', () => {
261
- it('should throw an error when called without a conversationUrl option', (done) => {
262
- try {
263
- webex.internal.conversation.listActivitiesThreadOrdered({});
264
- } catch (e) {
265
- assert.equal(e.message, 'must provide a conversation URL or conversation ID');
266
- done();
267
- }
268
- });
269
-
270
- it('calls #_listActivitiesThreadOrdered()', () => {
271
- const spy = sinon.spy(webex.internal.conversation, '_listActivitiesThreadOrdered');
272
-
273
- webex.internal.conversation.listActivitiesThreadOrdered({
274
- conversationUrl: convoUrl,
275
- includeChildren: true,
276
- });
277
- sinon.assert.calledWith(spy, {url: convoUrl, includeChildren: true});
278
- });
279
-
280
- it('returns expected wrapped functions', () => {
281
- const functions = webex.internal.conversation.listActivitiesThreadOrdered({
282
- conversationUrl: convoUrl,
283
- });
284
-
285
- assert.hasAllKeys(functions, ['jumpToActivity', 'getOlder', 'getNewer']);
286
- });
287
-
288
- describe('returned functions', () => {
289
- const returnedVal = [{}, {}, {}, {}];
290
-
291
- beforeEach(() => {
292
- webex.internal.conversation._listActivitiesThreadOrdered = sinon.stub().returns({
293
- next() {
294
- return new Promise((resolve) =>
295
- resolve({
296
- value: returnedVal,
297
- next() {},
298
- })
299
- );
300
- },
301
- });
302
- });
303
-
304
- it('getOlder() should implement the iterator protocol', () => {
305
- const {getOlder} = webex.internal.conversation.listActivitiesThreadOrdered({
306
- conversationUrl: convoUrl,
307
- });
308
-
309
- return getOlder().then((result) => {
310
- assert.hasAllKeys(result, ['done', 'value']);
311
- });
312
- });
313
-
314
- it('getNewer() should implement the iterator protocol', () => {
315
- const {getNewer} = webex.internal.conversation.listActivitiesThreadOrdered({
316
- conversationUrl: convoUrl,
317
- });
318
-
319
- return getNewer().then((result) => {
320
- assert.hasAllKeys(result, ['done', 'value']);
321
- });
322
- });
323
-
324
- describe('jumpToActivity()', () => {
325
- it('should throw an error if search object is missing', () => {
326
- const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
327
- conversationUrl: convoUrl,
328
- });
329
-
330
- jumpToActivity().catch((e) => {
331
- assert.equal(
332
- e.message,
333
- 'Search must be an activity object from conversation service'
334
- );
335
- });
336
- });
337
-
338
- it('should throw an error if activity.target.url is missing', () => {
339
- const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
340
- conversationUrl: convoUrl,
341
- });
342
-
343
- jumpToActivity({target: null}).catch((e) => {
344
- assert.equal(e.message, 'Search object must have a target url!');
345
- });
346
- });
347
-
348
- it('should implement the iterator protocol', () => {
349
- const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
350
- conversationUrl: convoUrl,
351
- });
352
-
353
- return jumpToActivity({target: {url: 'newUrl'}}).then((result) => {
354
- assert.hasAllKeys(result, ['done', 'value']);
355
- });
356
- });
357
- });
358
- });
359
- });
360
-
361
- describe('activity-thread-ordering helpers', () => {
362
- const createSimpleActivityByType = (type) => ({
363
- parent: {
364
- type,
365
- },
366
- });
367
-
368
- describe('getActivityType', () => {
369
- it('should handle common activity types', () => {
370
- const reply = createSimpleActivityByType('reply');
371
- const reaction = {type: 'reactionSummary'};
372
- const deleteAct = {verb: 'delete'};
373
- const root = createSimpleActivityByType();
374
-
375
- assert.equal(getActivityType(reply), 'REPLY');
376
- assert.equal(getActivityType(reaction), 'REACTION');
377
- assert.equal(getActivityType(deleteAct), 'DELETE');
378
- assert.equal(getActivityType(root), 'ROOT');
379
- });
380
- });
381
-
382
- describe('rootActivityManager', () => {
383
- it('should return expected exposed functions', () => {
384
- const functions = rootActivityManager();
385
-
386
- assert.hasAllKeys(functions, ['addNewRoot', 'getRootActivityHash']);
387
- });
388
-
389
- it('should interface with internal storage object', () => {
390
- const {addNewRoot, getRootActivityHash} = rootActivityManager();
391
-
392
- const initialRootHash = getRootActivityHash();
393
-
394
- assert.isEmpty(initialRootHash);
395
-
396
- addNewRoot({id: '123'});
397
-
398
- assert.isNotEmpty(initialRootHash);
399
- });
400
- });
401
-
402
- describe('getLoopCounterFailsafe', () => {
403
- it('should throw after looping 100 times', () => {
404
- let externalCount = 1;
405
- const incrementLoop = getLoopCounterFailsafe();
406
-
407
- try {
408
- while (externalCount < 150) {
409
- externalCount += 1;
410
- incrementLoop();
411
- }
412
- } catch (e) {
413
- assert.equal(e.message, 'max fetches reached');
414
- }
415
- });
416
- });
417
-
418
- describe('noMoreActivitiesManager', () => {
419
- let getNoMoreActs, checkAndSetNoMoreActs, checkAndSetNoNewerActs, checkAndSetNoOlderActs;
420
- const createAct = {verb: 'create'};
421
-
422
- beforeEach(() => {
423
- const funcs = noMoreActivitiesManager();
424
-
425
- getNoMoreActs = funcs.getNoMoreActs;
426
- checkAndSetNoMoreActs = funcs.checkAndSetNoMoreActs;
427
- checkAndSetNoNewerActs = funcs.checkAndSetNoNewerActs;
428
- checkAndSetNoOlderActs = funcs.checkAndSetNoOlderActs;
429
- });
430
-
431
- it('should return expected exposed functions', () => {
432
- const functions = noMoreActivitiesManager();
433
-
434
- assert.hasAllKeys(functions, [
435
- 'getNoMoreActs',
436
- 'checkAndSetNoMoreActs',
437
- 'checkAndSetNoNewerActs',
438
- 'checkAndSetNoOlderActs',
439
- ]);
440
- });
441
-
442
- it('should set no more activities when no newer activities exist', () => {
443
- checkAndSetNoOlderActs({});
444
- checkAndSetNoNewerActs([]);
445
- checkAndSetNoMoreActs(NEWER);
446
-
447
- assert.isTrue(getNoMoreActs());
448
- });
449
-
450
- it('should set no more activities when no older activities exist', () => {
451
- checkAndSetNoOlderActs(createAct);
452
- checkAndSetNoNewerActs([]);
453
- checkAndSetNoMoreActs(OLDER);
454
-
455
- assert.isTrue(getNoMoreActs());
456
- });
457
-
458
- it('should not set no more activities when newer activities exist', () => {
459
- checkAndSetNoNewerActs([1, 2, 3]);
460
- checkAndSetNoOlderActs(createAct);
461
- checkAndSetNoMoreActs(NEWER);
462
-
463
- assert.isFalse(getNoMoreActs());
464
- });
465
-
466
- it('should not set no more activities when older activities exist', () => {
467
- checkAndSetNoNewerActs([]);
468
- checkAndSetNoOlderActs({});
469
- checkAndSetNoMoreActs(OLDER);
470
-
471
- assert.isFalse(getNoMoreActs());
472
- });
473
- });
474
-
475
- describe('bookendManager', () => {
476
- let setBookends, getNewestAct, getOldestAct;
477
- const createDateISO = (min) => new Date(1, 1, 1, 1, min).toISOString();
478
-
479
- beforeEach(() => {
480
- const funcs = bookendManager();
481
-
482
- setBookends = funcs.setBookends;
483
- getNewestAct = funcs.getNewestAct;
484
- getOldestAct = funcs.getOldestAct;
485
- });
486
-
487
- it('should return expected exposed functions', () => {
488
- const functions = bookendManager();
489
-
490
- assert.hasAllKeys(functions, ['setBookends', 'getNewestAct', 'getOldestAct']);
491
- });
492
-
493
- it('should set the oldest and newest activity in a batch', () => {
494
- const acts = [
495
- {published: createDateISO(1), order: 0},
496
- {published: createDateISO(2), order: 1},
497
- ];
498
-
499
- setBookends(acts);
500
- assert.equal(getOldestAct().order, 0);
501
- assert.equal(getNewestAct().order, 1);
502
- });
503
-
504
- it('should bookends when newer and older activities exists', () => {
505
- const acts = [
506
- {published: createDateISO(5), order: 2},
507
- {published: createDateISO(6), order: 3},
508
- ];
509
-
510
- setBookends(acts);
511
-
512
- const nextActs = [
513
- {published: createDateISO(1), order: 1},
514
- {published: createDateISO(9), order: 4},
515
- ];
516
-
517
- setBookends(nextActs);
518
-
519
- assert.equal(getOldestAct().order, 1);
520
- assert.equal(getNewestAct().order, 4);
521
- });
522
-
523
- it('should not update oldest activity when only newer activities exist', () => {
524
- const acts = [
525
- {published: createDateISO(1), order: 1},
526
- {published: createDateISO(5), order: 2},
527
- ];
528
-
529
- setBookends(acts);
530
-
531
- const nextActs = [
532
- {published: createDateISO(6), order: 3},
533
- {published: createDateISO(9), order: 4},
534
- ];
535
-
536
- setBookends(nextActs);
537
-
538
- assert.equal(getOldestAct().order, 1);
539
- assert.equal(getNewestAct().order, 4);
540
- });
541
- });
542
-
543
- describe('activityManager', () => {
544
- let getActivityHandlerByKey, getActivityByTypeAndParentId;
545
- const parentId = 'parentId';
546
- const parentId2 = 'parentId2';
547
-
548
- beforeEach(() => {
549
- const funcs = activityManager();
550
-
551
- getActivityHandlerByKey = funcs.getActivityHandlerByKey;
552
- getActivityByTypeAndParentId = funcs.getActivityByTypeAndParentId;
553
- });
554
-
555
- it('should return expected exposed functions', () => {
556
- const functions = activityManager();
557
-
558
- assert.hasAllKeys(functions, ['getActivityHandlerByKey', 'getActivityByTypeAndParentId']);
559
- });
560
-
561
- it('should handle new replies', () => {
562
- const replyAct = {
563
- id: '1',
564
- activityType: 'reply',
565
- parent: {
566
- id: parentId,
567
- },
568
- };
569
- const replyHandler = getActivityHandlerByKey(ACTIVITY_TYPES.REPLY);
570
-
571
- replyHandler(replyAct);
572
-
573
- const replyMap = getActivityByTypeAndParentId(ACTIVITY_TYPES.REPLY, parentId);
574
-
575
- assert.equal(replyMap.get('1'), replyAct);
576
-
577
- const secondReplyAct = {
578
- id: '2',
579
- activityType: 'reply',
580
- parent: {
581
- id: parentId,
582
- },
583
- };
584
-
585
- replyHandler(secondReplyAct);
586
-
587
- assert.equal(replyMap.get('2'), secondReplyAct);
588
-
589
- const thirdReply = {
590
- id: '3',
591
- activityType: 'reply',
592
- parent: {
593
- id: parentId2,
594
- },
595
- };
596
-
597
- replyHandler(thirdReply);
598
-
599
- const replyMap2 = getActivityByTypeAndParentId(ACTIVITY_TYPES.REPLY, parentId2);
600
-
601
- assert.equal(replyMap2.get('3'), thirdReply);
602
- });
603
-
604
- it('should handle edits', () => {
605
- const editHandler = getActivityHandlerByKey(ACTIVITY_TYPES.EDIT);
606
-
607
- const editAct = {
608
- id: 'editId1',
609
- parent: {
610
- id: parentId,
611
- type: 'edit',
612
- },
613
- published: new Date(1, 1, 1, 1, 1).toISOString(),
614
- };
615
- const tombstoneEdit = {
616
- ...editAct,
617
- verb: ACTIVITY_TYPES.TOMBSTONE,
618
- };
619
- const newerEdit = {
620
- ...editAct,
621
- published: new Date(1, 1, 1, 1, 3).toISOString(),
622
- };
623
-
624
- editHandler(editAct);
625
-
626
- assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), editAct);
627
-
628
- editHandler(tombstoneEdit);
629
-
630
- assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), editAct);
631
-
632
- editHandler(newerEdit);
633
-
634
- assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), newerEdit);
635
- });
636
-
637
- it('should handle reactions', () => {
638
- const reactionHandler = getActivityHandlerByKey(ACTIVITY_TYPES.REACTION);
639
- const reaction = {
640
- id: 'reaction1',
641
- published: new Date(1, 1, 1, 1, 1).toISOString(),
642
- type: 'reactionSummary',
643
- parent: {
644
- id: parentId,
645
- },
646
- };
647
-
648
- const newerReaction = {
649
- ...reaction,
650
- published: new Date(1, 1, 1, 1, 3).toISOString(),
651
- };
652
-
653
- reactionHandler(reaction);
654
-
655
- assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.REACTION, parentId), reaction);
656
-
657
- reactionHandler(newerReaction);
658
-
659
- assert.equal(
660
- getActivityByTypeAndParentId(ACTIVITY_TYPES.REACTION, parentId),
661
- newerReaction
662
- );
663
- });
664
- });
665
- });
666
-
667
- describe('#_createParsedServerActivity()', () => {
668
- const activities = {
669
- root1: {
670
- id: 'root1',
671
- },
672
- reply1: {
673
- id: 'reply1',
674
- activityType: 'reply',
675
- parent: {
676
- id: 1,
677
- type: 'reply',
678
- },
679
- },
680
- };
681
-
682
- it('should add editParent field to valid edit activity', () => {
683
- const activity = {
684
- id: 'edit1',
685
- parent: {
686
- id: 'root1',
687
- type: 'edit',
688
- },
689
- };
690
-
691
- const parsedActivity = webex.internal.conversation._createParsedServerActivity(
692
- activity,
693
- activities
694
- );
695
-
696
- assert.containsAllKeys(parsedActivity, ['editParent']);
697
- });
698
-
699
- it('should add replyParent field to valid reply activity', () => {
700
- const activity = {
701
- id: 'reply2',
702
- activityType: 'reply',
703
- parent: {
704
- id: 'root1',
705
- type: 'reply',
706
- },
707
- };
708
-
709
- const parsedActivity = webex.internal.conversation._createParsedServerActivity(
710
- activity,
711
- activities
712
- );
713
-
714
- assert.containsAllKeys(parsedActivity, ['replyParent']);
715
- });
716
-
717
- it('should add replyParent and editParent to valid edited reply activity', () => {
718
- const activity = {
719
- id: 'replyEdit1',
720
- parent: {
721
- id: 'reply1',
722
- type: 'edit',
723
- },
724
- };
725
-
726
- const parsedActivity = webex.internal.conversation._createParsedServerActivity(
727
- activity,
728
- activities
729
- );
730
-
731
- assert.containsAllKeys(parsedActivity, ['replyParent', 'editParent']);
732
- });
733
-
734
- it('should throw when passed in activity has no parent in activity hash', () => {
735
- const activity = {
736
- id: 'throwAct1',
737
- parent: {
738
- id: 'root2',
739
- },
740
- };
741
-
742
- try {
743
- assert.throws(
744
- webex.internal.conversation._createParsedServerActivity(activity, activities)
745
- );
746
- } catch (e) {
747
- // swallow error
748
- }
749
- });
750
- });
751
-
752
- describe('delete()', () => {
753
- const testConvo = {
754
- id: 'id1',
755
- url: 'https://example.com',
756
- };
757
-
758
- it('should reject if provided param is not an object', () => {
759
- const request = webex.internal.conversation.delete(testConvo, 'hello');
760
-
761
- return request
762
- .then(() => {
763
- assert.equal(true, false, 'should have rejected');
764
- })
765
- .catch(() => {
766
- assert.equal(true, true, 'object is not type object, rejects as expected');
767
- });
768
- });
769
- it('deletes a non-meeting container activity', () => {
770
- webex.internal.conversation.prepare = sinon
771
- .stub()
772
- .callsFake((activity, request) => Promise.resolve({activity, request}));
773
- webex.internal.conversation.submit = sinon.stub().callsFake((p) => Promise.resolve(p));
774
-
775
- // fix this to look like below
776
- const request = webex.internal.conversation.delete(
777
- testConvo,
778
- {
779
- id: 'activity-id-1',
780
- url: 'https://example.com/activity1',
781
- object: {objectType: 'activity'},
782
- },
783
- {
784
- object: {objectType: 'activity'},
785
- }
786
- );
787
-
788
- return request.then(({request}) => {
789
- assert.isUndefined(request.kmsMessage);
790
- });
791
- });
792
- it('deletes a meeting container activity', () => {
793
- webex.internal.conversation.prepare = sinon
794
- .stub()
795
- .callsFake((activity, request) => Promise.resolve({activity, request}));
796
- webex.internal.conversation.submit = sinon.stub().callsFake((p) => Promise.resolve(p));
797
-
798
- const request = webex.internal.conversation.delete(
799
- testConvo,
800
- {
801
- id: 'activity-id-2',
802
- url: 'https://example.com/activity2',
803
- object: {
804
- kmsResourceObjectUrl: 'kms://example',
805
- objectType: 'meetingContainer',
806
- },
807
- },
808
- {
809
- object: {
810
- objectType: 'meetingContainer',
811
- },
812
- }
813
- );
814
-
815
- return request.then(({request}) => {
816
- assert.equal(request.kmsMessage.method, 'delete');
817
- assert.equal(request.kmsMessage.uri, '<KRO>/authorizations?authId=');
818
- assert.equal(request.target.kmsResourceObjectUrl, 'kms://example');
819
- });
820
- });
821
- });
822
- });
823
- });
1
+ /*!
2
+ * Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
3
+ */
4
+ /* eslint-disable no-underscore-dangle */
5
+ import {assert} from '@webex/test-helper-chai';
6
+ import MockWebex from '@webex/test-helper-mock-webex';
7
+ import sinon from 'sinon';
8
+ import Conversation from '@webex/internal-plugin-conversation';
9
+
10
+ import {
11
+ rootActivityManager,
12
+ getLoopCounterFailsafe,
13
+ noMoreActivitiesManager,
14
+ bookendManager,
15
+ activityManager,
16
+ } from '../../../src/activity-thread-ordering';
17
+ import {ACTIVITY_TYPES, getActivityType, OLDER, NEWER} from '../../../src/activities';
18
+
19
+ describe('plugin-conversation', () => {
20
+ describe('Conversation', () => {
21
+ let webex;
22
+
23
+ const convoUrl = 'https://conv-test.wbx2.com/conversation';
24
+
25
+ beforeEach(() => {
26
+ webex = new MockWebex({
27
+ children: {
28
+ conversation: Conversation,
29
+ },
30
+ });
31
+
32
+ webex.internal.services = {};
33
+ webex.internal.services.get = sinon.stub().returns(Promise.resolve(convoUrl));
34
+ webex.internal.services.getServiceUrlFromClusterId = sinon.stub().returns(convoUrl);
35
+ });
36
+
37
+ describe('processInmeetingchatEvent()', () => {
38
+ beforeEach(() => {
39
+ webex.transform = sinon.stub().callsFake((obj) => Promise.resolve(obj));
40
+ });
41
+
42
+ it('calls transform with correct arguments', async () => {
43
+ const event = {name: 'test-event'};
44
+
45
+ await webex.internal.conversation.processInmeetingchatEvent(event);
46
+
47
+ assert.calledWith(webex.transform, 'inbound', event);
48
+ });
49
+
50
+ it('calls transform and returns event', async () => {
51
+ const event = {name: 'test-event'};
52
+ const returnValue = await webex.internal.conversation.processInmeetingchatEvent(event);
53
+
54
+ assert.equal(event, returnValue);
55
+ });
56
+ });
57
+
58
+ describe('#_inferConversationUrl', () => {
59
+ const testConvo = {test: 'convo'};
60
+
61
+ it('Returns given convo if no id', () =>
62
+ webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
63
+ assert.notCalled(webex.internal.feature.getFeature);
64
+ assert.notCalled(webex.internal.services.get);
65
+ assert.equal(convo.test, 'convo');
66
+ }));
67
+
68
+ describe('HA is disabled', () => {
69
+ beforeEach(() => {
70
+ webex.internal.feature.getFeature = sinon.stub().returns(Promise.resolve(false));
71
+ testConvo.id = 'id1';
72
+ });
73
+ it('returns unmodified convo if URL is defined', () => {
74
+ testConvo.url = 'http://example.com';
75
+
76
+ return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
77
+ assert.called(webex.internal.feature.getFeature);
78
+ assert.notCalled(webex.internal.services.get);
79
+ assert.equal(convo.url, 'http://example.com');
80
+ });
81
+ });
82
+ it('builds URL if not defined', () => {
83
+ delete testConvo.url;
84
+
85
+ return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
86
+ assert.called(webex.internal.feature.getFeature);
87
+ assert.called(webex.internal.services.get);
88
+ assert.equal(convo.url, `${convoUrl}/conversations/id1`);
89
+ });
90
+ });
91
+ });
92
+ describe('HA is enabled', () => {
93
+ beforeEach(() => {
94
+ webex.internal.feature.getFeature = sinon.stub().returns(Promise.resolve(true));
95
+ testConvo.id = 'id1';
96
+ });
97
+ it('builds URL if already defined', () => {
98
+ testConvo.url = 'https://example.com';
99
+
100
+ return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
101
+ assert.called(webex.internal.feature.getFeature);
102
+ assert.called(webex.internal.services.get);
103
+ assert.equal(convo.url, `${convoUrl}/conversations/id1`);
104
+ });
105
+ });
106
+ it('builds URL if not defined', () => {
107
+ delete testConvo.url;
108
+
109
+ return webex.internal.conversation._inferConversationUrl(testConvo).then((convo) => {
110
+ assert.called(webex.internal.feature.getFeature);
111
+ assert.called(webex.internal.services.get);
112
+ assert.equal(convo.url, `${convoUrl}/conversations/id1`);
113
+ });
114
+ });
115
+ });
116
+ });
117
+
118
+ describe('getConvoUrl', () => {
119
+ it('should not return a promise', () => {
120
+ try {
121
+ webex.internal.conversation.getConvoUrl({url: 'convoUrl'}).then();
122
+ } catch (error) {
123
+ assert.equal(
124
+ error.message,
125
+ 'webex.internal.conversation.getConvoUrl(...).then is not a function'
126
+ );
127
+ }
128
+ });
129
+
130
+ it('should return the url if a url is provided', () => {
131
+ const url = webex.internal.conversation.getConvoUrl({url: convoUrl});
132
+
133
+ assert.equal(url, convoUrl);
134
+ });
135
+ });
136
+
137
+ describe('#getUrlFromClusterId', () => {
138
+ it('should convert a "us" cluster to WEBEX_CONVERSATION_DEFAULT_CLUSTER cluster', async () => {
139
+ await webex.internal.conversation.getUrlFromClusterId({cluster: 'us'});
140
+
141
+ sinon.assert.calledWith(webex.internal.services.getServiceUrlFromClusterId, {cluster: 'us'});
142
+ });
143
+
144
+ it('should add the cluster service when missing', async () => {
145
+ await webex.internal.conversation.getUrlFromClusterId({cluster: 'urn:TEAM:us-west-2_r'});
146
+
147
+ sinon.assert.calledWith(webex.internal.services.getServiceUrlFromClusterId, {cluster: 'urn:TEAM:us-west-2_r'});
148
+ });
149
+ });
150
+
151
+ describe('paginate', () => {
152
+ it('should throw an error if a page is passed with no links', () => {
153
+ webex.internal.conversation.paginate({page: {}}).catch((error) => {
154
+ assert.equal(error.message, 'No link to follow for the provided page');
155
+ });
156
+ });
157
+ });
158
+
159
+ describe('#getReactionSummaryByParentId()', () => {
160
+ beforeEach(() => {
161
+ webex.request = sinon.stub().returns(
162
+ Promise.resolve({
163
+ body: {
164
+ children: [
165
+ {type: 'reactionSelfSummary'},
166
+ {type: 'reactionSelfSummary'},
167
+ {type: 'reactionSummary'},
168
+ {type: 'notAReaction'},
169
+ ],
170
+ },
171
+ })
172
+ );
173
+ });
174
+
175
+ it('should call request', () =>
176
+ webex.internal.conversation.getReactionSummaryByParentId(convoUrl, 'test-id').then(() => {
177
+ assert.called(webex.request);
178
+ }));
179
+
180
+ it('should not retrieve non reaction summary objects', () =>
181
+ webex.internal.conversation
182
+ .getReactionSummaryByParentId(convoUrl, 'test-id')
183
+ .then((result) => {
184
+ assert.equal(result.length, 3);
185
+ assert.notInclude(result, {type: 'notAReaction'});
186
+ }));
187
+ });
188
+
189
+ describe('#listAllChildActivitiesByParentId()', () => {
190
+ const options = {
191
+ conversationUrl: convoUrl,
192
+ activityParentId: '123-abc',
193
+ query: {activityType: 'reply'},
194
+ };
195
+ let dayCount = 0;
196
+ const createActivityItemBatch = (itemCount) => {
197
+ const counter = [...Array(itemCount).keys()];
198
+
199
+ dayCount += 1;
200
+
201
+ return counter.map((n) => ({
202
+ published: new Date(2020, 0, 1, dayCount, n),
203
+ }));
204
+ };
205
+
206
+ const createMockChildResponse = (itemsToReturn = 10) => {
207
+ const response = {
208
+ body: {
209
+ items: createActivityItemBatch(itemsToReturn),
210
+ },
211
+ headers: {
212
+ link: '<https://www.cisco.com>; rel=next',
213
+ },
214
+ };
215
+
216
+ return response;
217
+ };
218
+
219
+ beforeEach(() => {
220
+ webex.request = sinon
221
+ .stub()
222
+ .onCall(0)
223
+ .returns(Promise.resolve(createMockChildResponse()))
224
+ .onCall(1)
225
+ .returns(Promise.resolve(createMockChildResponse()))
226
+ .onCall(2)
227
+ .returns(Promise.resolve(createMockChildResponse(3)))
228
+ .returns(
229
+ Promise.resolve(
230
+ Promise.resolve({
231
+ body: {
232
+ items: [],
233
+ },
234
+ headers: {},
235
+ })
236
+ )
237
+ );
238
+ });
239
+ it('retrieves correct children count', () =>
240
+ webex.internal.conversation.listAllChildActivitiesByParentId(options).then((res) => {
241
+ assert.equal(res.length, 23);
242
+ }));
243
+
244
+ it('calls #listChildActivitiesByParentId() to initiate the request', () => {
245
+ const spy = sinon.spy(webex.internal.conversation, 'listChildActivitiesByParentId');
246
+
247
+ return webex.internal.conversation.listAllChildActivitiesByParentId(options).then(() => {
248
+ assert(spy.calledOnce);
249
+ });
250
+ });
251
+
252
+ it('returns children in ascending published order', () =>
253
+ webex.internal.conversation.listAllChildActivitiesByParentId(options).then((res) => {
254
+ const firstMessageOlderThanLastMessage = res[0].published < res[res.length - 1].published;
255
+
256
+ assert.isTrue(firstMessageOlderThanLastMessage, 'activities out of order');
257
+ }));
258
+ });
259
+
260
+ describe('#listActivitiesThreadOrdered()', () => {
261
+ it('should throw an error when called without a conversationUrl option', (done) => {
262
+ try {
263
+ webex.internal.conversation.listActivitiesThreadOrdered({});
264
+ } catch (e) {
265
+ assert.equal(e.message, 'must provide a conversation URL or conversation ID');
266
+ done();
267
+ }
268
+ });
269
+
270
+ it('calls #_listActivitiesThreadOrdered()', () => {
271
+ const spy = sinon.spy(webex.internal.conversation, '_listActivitiesThreadOrdered');
272
+
273
+ webex.internal.conversation.listActivitiesThreadOrdered({
274
+ conversationUrl: convoUrl,
275
+ includeChildren: true,
276
+ });
277
+ sinon.assert.calledWith(spy, {url: convoUrl, includeChildren: true});
278
+ });
279
+
280
+ it('returns expected wrapped functions', () => {
281
+ const functions = webex.internal.conversation.listActivitiesThreadOrdered({
282
+ conversationUrl: convoUrl,
283
+ });
284
+
285
+ assert.hasAllKeys(functions, ['jumpToActivity', 'getOlder', 'getNewer']);
286
+ });
287
+
288
+ describe('returned functions', () => {
289
+ const returnedVal = [{}, {}, {}, {}];
290
+
291
+ beforeEach(() => {
292
+ webex.internal.conversation._listActivitiesThreadOrdered = sinon.stub().returns({
293
+ next() {
294
+ return new Promise((resolve) =>
295
+ resolve({
296
+ value: returnedVal,
297
+ next() {},
298
+ })
299
+ );
300
+ },
301
+ });
302
+ });
303
+
304
+ it('getOlder() should implement the iterator protocol', () => {
305
+ const {getOlder} = webex.internal.conversation.listActivitiesThreadOrdered({
306
+ conversationUrl: convoUrl,
307
+ });
308
+
309
+ return getOlder().then((result) => {
310
+ assert.hasAllKeys(result, ['done', 'value']);
311
+ });
312
+ });
313
+
314
+ it('getNewer() should implement the iterator protocol', () => {
315
+ const {getNewer} = webex.internal.conversation.listActivitiesThreadOrdered({
316
+ conversationUrl: convoUrl,
317
+ });
318
+
319
+ return getNewer().then((result) => {
320
+ assert.hasAllKeys(result, ['done', 'value']);
321
+ });
322
+ });
323
+
324
+ describe('jumpToActivity()', () => {
325
+ it('should throw an error if search object is missing', () => {
326
+ const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
327
+ conversationUrl: convoUrl,
328
+ });
329
+
330
+ jumpToActivity().catch((e) => {
331
+ assert.equal(
332
+ e.message,
333
+ 'Search must be an activity object from conversation service'
334
+ );
335
+ });
336
+ });
337
+
338
+ it('should throw an error if activity.target.url is missing', () => {
339
+ const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
340
+ conversationUrl: convoUrl,
341
+ });
342
+
343
+ jumpToActivity({target: null}).catch((e) => {
344
+ assert.equal(e.message, 'Search object must have a target url!');
345
+ });
346
+ });
347
+
348
+ it('should implement the iterator protocol', () => {
349
+ const {jumpToActivity} = webex.internal.conversation.listActivitiesThreadOrdered({
350
+ conversationUrl: convoUrl,
351
+ });
352
+
353
+ return jumpToActivity({target: {url: 'newUrl'}}).then((result) => {
354
+ assert.hasAllKeys(result, ['done', 'value']);
355
+ });
356
+ });
357
+ });
358
+ });
359
+ });
360
+
361
+ describe('activity-thread-ordering helpers', () => {
362
+ const createSimpleActivityByType = (type) => ({
363
+ parent: {
364
+ type,
365
+ },
366
+ });
367
+
368
+ describe('getActivityType', () => {
369
+ it('should handle common activity types', () => {
370
+ const reply = createSimpleActivityByType('reply');
371
+ const reaction = {type: 'reactionSummary'};
372
+ const deleteAct = {verb: 'delete'};
373
+ const root = createSimpleActivityByType();
374
+
375
+ assert.equal(getActivityType(reply), 'REPLY');
376
+ assert.equal(getActivityType(reaction), 'REACTION');
377
+ assert.equal(getActivityType(deleteAct), 'DELETE');
378
+ assert.equal(getActivityType(root), 'ROOT');
379
+ });
380
+ });
381
+
382
+ describe('rootActivityManager', () => {
383
+ it('should return expected exposed functions', () => {
384
+ const functions = rootActivityManager();
385
+
386
+ assert.hasAllKeys(functions, ['addNewRoot', 'getRootActivityHash']);
387
+ });
388
+
389
+ it('should interface with internal storage object', () => {
390
+ const {addNewRoot, getRootActivityHash} = rootActivityManager();
391
+
392
+ const initialRootHash = getRootActivityHash();
393
+
394
+ assert.isEmpty(initialRootHash);
395
+
396
+ addNewRoot({id: '123'});
397
+
398
+ assert.isNotEmpty(initialRootHash);
399
+ });
400
+ });
401
+
402
+ describe('getLoopCounterFailsafe', () => {
403
+ it('should throw after looping 100 times', () => {
404
+ let externalCount = 1;
405
+ const incrementLoop = getLoopCounterFailsafe();
406
+
407
+ try {
408
+ while (externalCount < 150) {
409
+ externalCount += 1;
410
+ incrementLoop();
411
+ }
412
+ } catch (e) {
413
+ assert.equal(e.message, 'max fetches reached');
414
+ }
415
+ });
416
+ });
417
+
418
+ describe('noMoreActivitiesManager', () => {
419
+ let getNoMoreActs, checkAndSetNoMoreActs, checkAndSetNoNewerActs, checkAndSetNoOlderActs;
420
+ const createAct = {verb: 'create'};
421
+
422
+ beforeEach(() => {
423
+ const funcs = noMoreActivitiesManager();
424
+
425
+ getNoMoreActs = funcs.getNoMoreActs;
426
+ checkAndSetNoMoreActs = funcs.checkAndSetNoMoreActs;
427
+ checkAndSetNoNewerActs = funcs.checkAndSetNoNewerActs;
428
+ checkAndSetNoOlderActs = funcs.checkAndSetNoOlderActs;
429
+ });
430
+
431
+ it('should return expected exposed functions', () => {
432
+ const functions = noMoreActivitiesManager();
433
+
434
+ assert.hasAllKeys(functions, [
435
+ 'getNoMoreActs',
436
+ 'checkAndSetNoMoreActs',
437
+ 'checkAndSetNoNewerActs',
438
+ 'checkAndSetNoOlderActs',
439
+ ]);
440
+ });
441
+
442
+ it('should set no more activities when no newer activities exist', () => {
443
+ checkAndSetNoOlderActs({});
444
+ checkAndSetNoNewerActs([]);
445
+ checkAndSetNoMoreActs(NEWER);
446
+
447
+ assert.isTrue(getNoMoreActs());
448
+ });
449
+
450
+ it('should set no more activities when no older activities exist', () => {
451
+ checkAndSetNoOlderActs(createAct);
452
+ checkAndSetNoNewerActs([]);
453
+ checkAndSetNoMoreActs(OLDER);
454
+
455
+ assert.isTrue(getNoMoreActs());
456
+ });
457
+
458
+ it('should not set no more activities when newer activities exist', () => {
459
+ checkAndSetNoNewerActs([1, 2, 3]);
460
+ checkAndSetNoOlderActs(createAct);
461
+ checkAndSetNoMoreActs(NEWER);
462
+
463
+ assert.isFalse(getNoMoreActs());
464
+ });
465
+
466
+ it('should not set no more activities when older activities exist', () => {
467
+ checkAndSetNoNewerActs([]);
468
+ checkAndSetNoOlderActs({});
469
+ checkAndSetNoMoreActs(OLDER);
470
+
471
+ assert.isFalse(getNoMoreActs());
472
+ });
473
+ });
474
+
475
+ describe('bookendManager', () => {
476
+ let setBookends, getNewestAct, getOldestAct;
477
+ const createDateISO = (min) => new Date(1, 1, 1, 1, min).toISOString();
478
+
479
+ beforeEach(() => {
480
+ const funcs = bookendManager();
481
+
482
+ setBookends = funcs.setBookends;
483
+ getNewestAct = funcs.getNewestAct;
484
+ getOldestAct = funcs.getOldestAct;
485
+ });
486
+
487
+ it('should return expected exposed functions', () => {
488
+ const functions = bookendManager();
489
+
490
+ assert.hasAllKeys(functions, ['setBookends', 'getNewestAct', 'getOldestAct']);
491
+ });
492
+
493
+ it('should set the oldest and newest activity in a batch', () => {
494
+ const acts = [
495
+ {published: createDateISO(1), order: 0},
496
+ {published: createDateISO(2), order: 1},
497
+ ];
498
+
499
+ setBookends(acts);
500
+ assert.equal(getOldestAct().order, 0);
501
+ assert.equal(getNewestAct().order, 1);
502
+ });
503
+
504
+ it('should bookends when newer and older activities exists', () => {
505
+ const acts = [
506
+ {published: createDateISO(5), order: 2},
507
+ {published: createDateISO(6), order: 3},
508
+ ];
509
+
510
+ setBookends(acts);
511
+
512
+ const nextActs = [
513
+ {published: createDateISO(1), order: 1},
514
+ {published: createDateISO(9), order: 4},
515
+ ];
516
+
517
+ setBookends(nextActs);
518
+
519
+ assert.equal(getOldestAct().order, 1);
520
+ assert.equal(getNewestAct().order, 4);
521
+ });
522
+
523
+ it('should not update oldest activity when only newer activities exist', () => {
524
+ const acts = [
525
+ {published: createDateISO(1), order: 1},
526
+ {published: createDateISO(5), order: 2},
527
+ ];
528
+
529
+ setBookends(acts);
530
+
531
+ const nextActs = [
532
+ {published: createDateISO(6), order: 3},
533
+ {published: createDateISO(9), order: 4},
534
+ ];
535
+
536
+ setBookends(nextActs);
537
+
538
+ assert.equal(getOldestAct().order, 1);
539
+ assert.equal(getNewestAct().order, 4);
540
+ });
541
+ });
542
+
543
+ describe('activityManager', () => {
544
+ let getActivityHandlerByKey, getActivityByTypeAndParentId;
545
+ const parentId = 'parentId';
546
+ const parentId2 = 'parentId2';
547
+
548
+ beforeEach(() => {
549
+ const funcs = activityManager();
550
+
551
+ getActivityHandlerByKey = funcs.getActivityHandlerByKey;
552
+ getActivityByTypeAndParentId = funcs.getActivityByTypeAndParentId;
553
+ });
554
+
555
+ it('should return expected exposed functions', () => {
556
+ const functions = activityManager();
557
+
558
+ assert.hasAllKeys(functions, ['getActivityHandlerByKey', 'getActivityByTypeAndParentId']);
559
+ });
560
+
561
+ it('should handle new replies', () => {
562
+ const replyAct = {
563
+ id: '1',
564
+ activityType: 'reply',
565
+ parent: {
566
+ id: parentId,
567
+ },
568
+ };
569
+ const replyHandler = getActivityHandlerByKey(ACTIVITY_TYPES.REPLY);
570
+
571
+ replyHandler(replyAct);
572
+
573
+ const replyMap = getActivityByTypeAndParentId(ACTIVITY_TYPES.REPLY, parentId);
574
+
575
+ assert.equal(replyMap.get('1'), replyAct);
576
+
577
+ const secondReplyAct = {
578
+ id: '2',
579
+ activityType: 'reply',
580
+ parent: {
581
+ id: parentId,
582
+ },
583
+ };
584
+
585
+ replyHandler(secondReplyAct);
586
+
587
+ assert.equal(replyMap.get('2'), secondReplyAct);
588
+
589
+ const thirdReply = {
590
+ id: '3',
591
+ activityType: 'reply',
592
+ parent: {
593
+ id: parentId2,
594
+ },
595
+ };
596
+
597
+ replyHandler(thirdReply);
598
+
599
+ const replyMap2 = getActivityByTypeAndParentId(ACTIVITY_TYPES.REPLY, parentId2);
600
+
601
+ assert.equal(replyMap2.get('3'), thirdReply);
602
+ });
603
+
604
+ it('should handle edits', () => {
605
+ const editHandler = getActivityHandlerByKey(ACTIVITY_TYPES.EDIT);
606
+
607
+ const editAct = {
608
+ id: 'editId1',
609
+ parent: {
610
+ id: parentId,
611
+ type: 'edit',
612
+ },
613
+ published: new Date(1, 1, 1, 1, 1).toISOString(),
614
+ };
615
+ const tombstoneEdit = {
616
+ ...editAct,
617
+ verb: ACTIVITY_TYPES.TOMBSTONE,
618
+ };
619
+ const newerEdit = {
620
+ ...editAct,
621
+ published: new Date(1, 1, 1, 1, 3).toISOString(),
622
+ };
623
+
624
+ editHandler(editAct);
625
+
626
+ assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), editAct);
627
+
628
+ editHandler(tombstoneEdit);
629
+
630
+ assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), editAct);
631
+
632
+ editHandler(newerEdit);
633
+
634
+ assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.EDIT, parentId), newerEdit);
635
+ });
636
+
637
+ it('should handle reactions', () => {
638
+ const reactionHandler = getActivityHandlerByKey(ACTIVITY_TYPES.REACTION);
639
+ const reaction = {
640
+ id: 'reaction1',
641
+ published: new Date(1, 1, 1, 1, 1).toISOString(),
642
+ type: 'reactionSummary',
643
+ parent: {
644
+ id: parentId,
645
+ },
646
+ };
647
+
648
+ const newerReaction = {
649
+ ...reaction,
650
+ published: new Date(1, 1, 1, 1, 3).toISOString(),
651
+ };
652
+
653
+ reactionHandler(reaction);
654
+
655
+ assert.equal(getActivityByTypeAndParentId(ACTIVITY_TYPES.REACTION, parentId), reaction);
656
+
657
+ reactionHandler(newerReaction);
658
+
659
+ assert.equal(
660
+ getActivityByTypeAndParentId(ACTIVITY_TYPES.REACTION, parentId),
661
+ newerReaction
662
+ );
663
+ });
664
+ });
665
+ });
666
+
667
+ describe('#_createParsedServerActivity()', () => {
668
+ const activities = {
669
+ root1: {
670
+ id: 'root1',
671
+ },
672
+ reply1: {
673
+ id: 'reply1',
674
+ activityType: 'reply',
675
+ parent: {
676
+ id: 1,
677
+ type: 'reply',
678
+ },
679
+ },
680
+ };
681
+
682
+ it('should add editParent field to valid edit activity', () => {
683
+ const activity = {
684
+ id: 'edit1',
685
+ parent: {
686
+ id: 'root1',
687
+ type: 'edit',
688
+ },
689
+ };
690
+
691
+ const parsedActivity = webex.internal.conversation._createParsedServerActivity(
692
+ activity,
693
+ activities
694
+ );
695
+
696
+ assert.containsAllKeys(parsedActivity, ['editParent']);
697
+ });
698
+
699
+ it('should add replyParent field to valid reply activity', () => {
700
+ const activity = {
701
+ id: 'reply2',
702
+ activityType: 'reply',
703
+ parent: {
704
+ id: 'root1',
705
+ type: 'reply',
706
+ },
707
+ };
708
+
709
+ const parsedActivity = webex.internal.conversation._createParsedServerActivity(
710
+ activity,
711
+ activities
712
+ );
713
+
714
+ assert.containsAllKeys(parsedActivity, ['replyParent']);
715
+ });
716
+
717
+ it('should add replyParent and editParent to valid edited reply activity', () => {
718
+ const activity = {
719
+ id: 'replyEdit1',
720
+ parent: {
721
+ id: 'reply1',
722
+ type: 'edit',
723
+ },
724
+ };
725
+
726
+ const parsedActivity = webex.internal.conversation._createParsedServerActivity(
727
+ activity,
728
+ activities
729
+ );
730
+
731
+ assert.containsAllKeys(parsedActivity, ['replyParent', 'editParent']);
732
+ });
733
+
734
+ it('should throw when passed in activity has no parent in activity hash', () => {
735
+ const activity = {
736
+ id: 'throwAct1',
737
+ parent: {
738
+ id: 'root2',
739
+ },
740
+ };
741
+
742
+ try {
743
+ assert.throws(
744
+ webex.internal.conversation._createParsedServerActivity(activity, activities)
745
+ );
746
+ } catch (e) {
747
+ // swallow error
748
+ }
749
+ });
750
+ });
751
+
752
+ describe('delete()', () => {
753
+ const testConvo = {
754
+ id: 'id1',
755
+ url: 'https://example.com',
756
+ };
757
+
758
+ it('should reject if provided param is not an object', () => {
759
+ const request = webex.internal.conversation.delete(testConvo, 'hello');
760
+
761
+ return request
762
+ .then(() => {
763
+ assert.equal(true, false, 'should have rejected');
764
+ })
765
+ .catch(() => {
766
+ assert.equal(true, true, 'object is not type object, rejects as expected');
767
+ });
768
+ });
769
+ it('deletes a non-meeting container activity', () => {
770
+ webex.internal.conversation.prepare = sinon
771
+ .stub()
772
+ .callsFake((activity, request) => Promise.resolve({activity, request}));
773
+ webex.internal.conversation.submit = sinon.stub().callsFake((p) => Promise.resolve(p));
774
+
775
+ // fix this to look like below
776
+ const request = webex.internal.conversation.delete(
777
+ testConvo,
778
+ {
779
+ id: 'activity-id-1',
780
+ url: 'https://example.com/activity1',
781
+ object: {objectType: 'activity'},
782
+ },
783
+ {
784
+ object: {objectType: 'activity'},
785
+ }
786
+ );
787
+
788
+ return request.then(({request}) => {
789
+ assert.isUndefined(request.kmsMessage);
790
+ });
791
+ });
792
+ it('deletes a meeting container activity', () => {
793
+ webex.internal.conversation.prepare = sinon
794
+ .stub()
795
+ .callsFake((activity, request) => Promise.resolve({activity, request}));
796
+ webex.internal.conversation.submit = sinon.stub().callsFake((p) => Promise.resolve(p));
797
+
798
+ const request = webex.internal.conversation.delete(
799
+ testConvo,
800
+ {
801
+ id: 'activity-id-2',
802
+ url: 'https://example.com/activity2',
803
+ object: {
804
+ kmsResourceObjectUrl: 'kms://example',
805
+ objectType: 'meetingContainer',
806
+ },
807
+ },
808
+ {
809
+ object: {
810
+ objectType: 'meetingContainer',
811
+ },
812
+ }
813
+ );
814
+
815
+ return request.then(({request}) => {
816
+ assert.equal(request.kmsMessage.method, 'delete');
817
+ assert.equal(request.kmsMessage.uri, '<KRO>/authorizations?authId=');
818
+ assert.equal(request.target.kmsResourceObjectUrl, 'kms://example');
819
+ });
820
+ });
821
+ });
822
+ });
823
+ });