lint cleanup
[websub-hub] / test / src / communication.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
9 const Communication = require('../../src/communication');
10 const Config = require('../../config');
11 const Errors = require('../../src/errors');
12
13 const stubDb = require('../stub-db');
14 const stubLogger = require('../stub-logger');
15
16 const noExpectedException = 'did not get expected exception';
17
18 describe('Communication', function () {
19 let communication, options;
20
21 beforeEach(function () {
22 options = new Config('test');
23 communication = new Communication(stubLogger, stubDb, options);
24 stubDb._reset();
25 stubLogger._reset();
26 });
27 afterEach(function () {
28 sinon.restore();
29 });
30
31 it('instantiates', function () {
32 assert(communication);
33 });
34
35 it('covers config value', function () {
36 options.dingus.selfBaseUrl = undefined;
37 communication = new Communication(stubLogger, stubDb, options);
38 });
39
40 describe('_init', function () {
41 it('covers', async function () {
42 await communication._init();
43 await communication._init();
44 assert(communication.Got);
45 assert(communication.got);
46 });
47 });
48
49 describe('_onRetry', function () {
50 it('covers', function () {
51 const error = {};
52 const retryCount = 1;
53 communication._onRetry(error, retryCount);
54 assert(communication.logger.debug.called);
55 });
56 });
57
58 describe('userAgentString', function () {
59 it('has default behavior', function () {
60 const result = Communication.userAgentString();
61 assert(result);
62 assert(result.length > 30);
63 });
64 it('is settable', function () {
65 const result = Communication.userAgentString({
66 product: 'myhub',
67 version: '9.9.9',
68 implementation: 'custom',
69 });
70 assert(result);
71 assert.strictEqual(result, 'myhub/9.9.9 (custom)');
72 });
73 it('covers branches', function () {
74 const result = Communication.userAgentString({
75 product: 'myhub',
76 version: '9.9.9',
77 implementation: '',
78 });
79 assert(result);
80 assert.strictEqual(result, 'myhub/9.9.9');
81 });
82 });
83
84 describe('generateChallenge', function () {
85 it('generates a thing', async function () {
86 const result = await Communication.generateChallenge();
87 assert(result);
88 assert(result.length);
89 });
90 });
91
92 describe('signature', function () {
93 let message, secret, algorithm, expected;
94 beforeEach(function () {
95 message = 'Jackdaws love my big sphinx of quartz.';
96 secret = 'secretsecret';
97 algorithm = 'sha256';
98 expected = 'sha256=ee92148d9cd043cdfb8da7cf5ee1897abaafdb5ab840e85010abd4bf235fa31e';
99 });
100 it('signs a thing', function () {
101 const result = Communication.signature(message, secret, algorithm);
102 assert.strictEqual(result, expected);
103 });
104 });
105
106 describe('contentHash', function () {
107 let content, algorithm, expected;
108 beforeEach(function () {
109 content = 'Jived fox nymph grabs quick waltz.';
110 algorithm = 'sha256';
111 expected = '6e5e1a93bde78910b0d7c5fd8aba393294d4eca5d3fbf2bfd49100df3d5cc85d';
112 });
113 it('hashes', function () {
114 const result = Communication.contentHash(content, algorithm);
115 assert.strictEqual(result, expected);
116 });
117 });
118
119 describe('verificationProcess', function () {
120 const challenge = 'a_challenge';
121 let dbCtx, callback, requestId, topicId;
122 let topic, verification;
123 beforeEach(function () {
124 dbCtx = {};
125 callback = 'https://example.com/callback/?id=123';
126 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
127 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
128 topic = {
129 id: topicId,
130 url: 'https://example.com/blog/',
131 isActive: true,
132 isDeleted: false,
133 };
134 verification = {
135 callback,
136 mode: 'subscribe',
137 isPublisherValidated: true,
138 leaseSeconds: 864000,
139 };
140
141 sinon.stub(Communication, 'generateChallenge').resolves(challenge);
142 sinon.stub(communication, 'publisherValidate').resolves(true);
143 sinon.stub(communication, 'got').resolves({
144 statusCode: 200,
145 statusMessage: 'OK',
146 headers: {
147 'content-type': 'text/plain',
148 },
149 body: challenge,
150 });
151
152 communication.db.verificationGetById.resolves(verification);
153 communication.db.topicGetById.resolves(topic);
154 communication.db.verificationRelease.resolves({});
155 communication.db.verificationUpdate.resolves({});
156 communication.db.verificationIncomplete.resolves({});
157 communication.db.verificationComplete.resolves({});
158 });
159
160 it('errors on non-existent verification', async function () {
161 communication.db.verificationGetById.restore();
162 sinon.stub(communication.db, 'verificationGetById').resolves();
163
164 try {
165 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
166 assert.fail(noExpectedException);
167 } catch (e) {
168 assert(e instanceof Errors.InternalInconsistencyError);
169 }
170 });
171
172 it('errors on non-existent topic', async function () {
173 communication.db.topicGetById.restore();
174 sinon.stub(communication.db, 'topicGetById').resolves();
175
176 try {
177 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
178 assert.fail(noExpectedException);
179 } catch (e) {
180 assert(e instanceof Errors.InternalInconsistencyError);
181 }
182 });
183
184 it('skips inactive topic', async function () {
185 communication.db.topicGetById.restore();
186 topic.isActive = false;
187 sinon.stub(communication.db, 'topicGetById').resolves(topic);
188
189 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
190
191 assert(communication.db.verificationRelease.called);
192 assert(!communication.got.called);
193 });
194
195 it('denies subscription to deleted topic', async function () {
196 communication.db.topicGetById.restore();
197 topic.isDeleted = true;
198 sinon.stub(communication.db, 'topicGetById').resolves(topic);
199
200 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
201
202 assert(communication.db.verificationUpdate.called);
203 assert.strictEqual(verification.mode, 'denied');
204 });
205
206 it('checks publisher validation if needed', async function() {
207 communication.db.verificationGetById.restore();
208 verification.isPublisherValidated = false;
209 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
210 communication.db.topicGetById.restore();
211 topic.publisherValidationUrl = 'https://example.com/publisher/';
212 sinon.stub(communication.db, 'topicGetById').resolves(topic);
213
214 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
215
216 assert(communication.publisherValidate.called);
217 assert(communication.db.verificationComplete.called);
218 });
219
220 it('handles publisher validation failure', async function() {
221 communication.db.verificationGetById.restore();
222 verification.isPublisherValidated = false;
223 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
224 communication.db.topicGetById.restore();
225 topic.publisherValidationUrl = 'https://example.com/publisher/';
226 sinon.stub(communication.db, 'topicGetById').resolves(topic);
227 communication.publisherValidate.restore();
228 sinon.stub(communication, 'publisherValidate').resolves(false);
229
230 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
231
232 assert(communication.publisherValidate.called);
233 assert(communication.db.verificationIncomplete.called);
234 });
235
236 it('handles request error', async function () {
237 communication.got.restore();
238 sinon.stub(communication, 'got').rejects(new Error());
239
240 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
241
242 assert(communication.db.verificationIncomplete.called);
243 });
244
245 it('handles 500 response', async function () {
246 communication.got.restore();
247 sinon.stub(communication, 'got').resolves({
248 statusCode: 500,
249 });
250
251 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
252
253 assert(communication.db.verificationIncomplete.called);
254 });
255
256 it('handles non-200 response', async function () {
257 communication.got.restore();
258 sinon.stub(communication, 'got').resolves({
259 statusCode: 400,
260 });
261
262 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
263
264 assert(communication.db.verificationComplete.called);
265 });
266
267 it('subscription succeeds', async function () {
268 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
269
270 assert(communication.db.subscriptionUpsert.called);
271 assert(communication.db.verificationComplete.called);
272 });
273
274 it('unsubscription succeeds', async function () {
275 communication.db.verificationGetById.restore();
276 verification.mode = 'unsubscribe';
277 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
278
279 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
280
281 assert(communication.db.subscriptionDelete.called);
282 assert(communication.db.verificationComplete.called);
283 });
284
285 it('unsubscription from deleted topic deletes topic', async function () {
286 communication.db.verificationGetById.restore();
287 verification.mode = 'unsubscribe';
288 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
289 communication.db.topicGetById.restore();
290 sinon.stub(communication.db, 'topicGetById').resolves({
291 ...topic,
292 isDeleted: true,
293 });
294
295 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
296
297 assert(communication.db.subscriptionDelete.called);
298 assert(communication.db.verificationComplete.called);
299 assert(communication.db.topicPendingDelete.called);
300 });
301
302 it('unsubscription denial succeeds', async function () {
303 communication.db.verificationGetById.restore();
304 verification.mode = 'unsubscribe';
305 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
306 communication.got.restore();
307 sinon.stub(communication, 'got').resolves({
308 statusCode: 200,
309 statusMessage: 'OK',
310 headers: {
311 'content-type': 'text/plain',
312 },
313 body: 'not the challenge',
314 });
315
316 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
317
318 assert(!communication.db.subscriptionDelete.called);
319 assert(communication.db.verificationComplete.called);
320 });
321
322 it('does not handle strange mode', async function() {
323 communication.db.verificationGetById.restore();
324 verification.mode = 'flarp';
325 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
326
327 try {
328 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
329 assert.fail(noExpectedException);
330 } catch (e) {
331 assert(e instanceof Errors.InternalInconsistencyError);
332 }
333 });
334 }); // verificationProcess
335
336 describe('publisherValidate', function () {
337 let dbCtx, topic, verification;
338 beforeEach(function () {
339 dbCtx = {};
340 topic = {
341 url: 'https://example.com/topic/',
342 publisherValidationUrl: 'https://example.com/pub_valid/',
343 };
344 verification = {
345 callback: 'https://exmaple.com/callback/?id=123',
346 httpFrom: 'user@example.com',
347 httpRemoteAddr: '127.0.0.0',
348 };
349
350 sinon.stub(communication, 'got').resolves({
351 statusCode: 200,
352 statusMessage: 'OK',
353 headers: {
354 'content-type': 'application/json',
355 },
356 });
357
358 communication.db.verificationIncomplete.resolves();
359 communication.db.verificationUpdate.resolves();
360 communication.db.verificationValidated.resolves();
361 });
362
363 it('succeeds', async function () {
364 const result = await communication.publisherValidate(dbCtx, topic, verification);
365
366 assert(communication.db.verificationValidated.called);
367 assert.strictEqual(result, true);
368 });
369
370 it('succeeds with rejection', async function () {
371 communication.got.restore();
372 sinon.stub(communication, 'got').resolves({
373 statusCode: 400,
374 statusMessage: 'Bad Request',
375 headers: {
376 'content-type': 'application/json',
377 },
378 });
379
380 const result = await communication.publisherValidate(dbCtx, topic, verification);
381
382 assert(communication.db.verificationValidated.called);
383 assert(communication.db.verificationUpdate.called);
384 assert.strictEqual(result, true);
385 });
386
387 it('defers on request server error', async function () {
388 communication.got.restore();
389 sinon.stub(communication, 'got').resolves({
390 statusCode: 502,
391 statusMessage: 'Bad Gateway',
392 headers: {
393 'content-type': 'text/plain',
394 },
395 });
396
397 const result = await communication.publisherValidate(dbCtx, topic, verification);
398
399 assert.strictEqual(result, false);
400 });
401
402 it('handles request error', async function () {
403 communication.got.restore();
404 sinon.stub(communication, 'got').rejects(new Error());
405
406 const result = await communication.publisherValidate(dbCtx, topic, verification);
407
408 assert.strictEqual(result, false);
409 });
410
411 }); // publisherValidate
412
413 describe('topicFetchProcess', function () {
414 let dbCtx, topic, requestId, topicId;
415
416 beforeEach(function () {
417 dbCtx = {};
418 topic = {
419 url: 'https://example.com/topic/',
420 isDeleted: false,
421 contentHashAlgorithm: 'sha512',
422 };
423 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
424 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
425
426 sinon.stub(communication, 'got').resolves({
427 statusCode: 200,
428 statusMessage: 'OK',
429 headers: {
430 'content-type': 'text/plain',
431 link: '<https://example.com/hub/>; rel="hub"',
432 'last-modified': 'Thu, 18 Nov 2021 20:34:35 GMT',
433 'etag': '"9c104-1673e-5d1161636d742"',
434 },
435 body: 'Jackdaws love my big sphinx of quartz.',
436 });
437
438 communication.db.topicGetById.resolves(topic);
439 });
440
441 it('requires topic exists', async function () {
442 communication.db.topicGetById.restore();
443 sinon.stub(communication.db, 'topicGetById').resolves();
444
445 try {
446 await communication.topicFetchProcess(dbCtx, topicId, requestId);
447 assert.fail(noExpectedException);
448 } catch (e) {
449 assert(e instanceof Errors.InternalInconsistencyError);
450 }
451 });
452
453 it ('skips deleted topic', async function () {
454 communication.db.topicGetById.restore();
455 topic.isDeleted = true;
456 sinon.stub(communication.db, 'topicGetById').resolves(topic);
457
458 await communication.topicFetchProcess(dbCtx, topicId, requestId);
459
460 assert(!communication.got.called);
461 });
462
463 it('handles request error', async function () {
464 communication.got.restore();
465 sinon.stub(communication, 'got').rejects(new Error());
466
467 await communication.topicFetchProcess(dbCtx, topicId, requestId);
468
469 assert(communication.db.topicFetchIncomplete.called);
470 });
471
472 it('handles 500 response', async function () {
473 communication.got.restore();
474 sinon.stub(communication, 'got').resolves({
475 statusCode: 500,
476 statusMessage: 'Internal Server Error',
477 headers: {
478 'content-type': 'text/plain',
479 },
480 });
481
482 await communication.topicFetchProcess(dbCtx, topicId, requestId);
483
484 assert(communication.db.topicFetchIncomplete.called);
485 });
486
487 it('handles bad response', async function () {
488 communication.got.restore();
489 sinon.stub(communication, 'got').resolves({
490 statusCode: 404,
491 statusMessage: 'Not Found',
492 headers: {
493 'content-type': 'text/plain',
494 },
495 });
496
497 await communication.topicFetchProcess(dbCtx, topicId, requestId);
498
499 assert(communication.db.topicFetchIncomplete.called);
500 });
501
502 it('recognizes unchanged content', async function () {
503 communication.db.topicGetById.restore();
504 topic.contentHash = 'a630999c61738f3e066d79a1b299a295c5d0598c173e0904d04a707d43988e3e81660bfc1b1779377f4ec26f837d1bb31fa2b860c9ad2d37495d83de32647fea';
505 sinon.stub(communication.db, 'topicGetById').resolves(topic);
506
507 await communication.topicFetchProcess(dbCtx, topicId, requestId);
508
509 assert(communication.db.topicFetchComplete.called);
510 assert(!communication.db.topicSetContent.called);
511 });
512
513 it('recognizes 304 response', async function () {
514 topic.httpLastModified = 'Thu, 18 Nov 2021 20:34:35 GMT';
515 topic.httpEtag = '"9c104-1673e-5d1161636d742"';
516 communication.db.topicGetById.resolves(topic);
517 communication.got.resolves({
518 statusCode: 304,
519 });
520
521 await communication.topicFetchProcess(dbCtx, topicId, requestId);
522
523 assert(communication.db.topicFetchComplete.called);
524 assert(!communication.db.topicSetContent.called);
525 });
526
527 it('updates content', async function () {
528 await communication.topicFetchProcess(dbCtx, topicId, requestId);
529
530 assert(communication.db.topicFetchComplete.called);
531 assert(communication.db.topicSetContent.called);
532 });
533
534 it('updates content with lax link enforcement', async function () {
535 communication.got.restore();
536 sinon.stub(communication, 'got').resolves({
537 statusCode: 200,
538 statusMessage: 'OK',
539 headers: {
540 'content-type': 'text/plain',
541 link: '<https://example.com/other/hub/>; rel="hub"',
542 },
543 body: 'Jackdaws love my big sphinx of quartz.',
544 });
545
546 communication.options.communication.strictTopicHubLink = false;
547
548 await communication.topicFetchProcess(dbCtx, topicId, requestId);
549
550 assert(communication.db.topicFetchComplete.called);
551 assert(communication.db.topicSetContent.called);
552 });
553
554 it('deletes topic when hub relation unsatisfied', async function () {
555 communication.got.restore();
556 sinon.stub(communication, 'got').resolves({
557 statusCode: 200,
558 statusMessage: 'OK',
559 headers: {
560 'content-type': 'text/plain',
561 link: '<https://example.com/other/hub/>; rel="hub"',
562 },
563 body: 'Jackdaws love my big sphinx of quartz.',
564 });
565
566 await communication.topicFetchProcess(dbCtx, topicId, requestId);
567
568 assert(communication.db.topicFetchComplete.called);
569 assert(communication.db.topicDeleted.called);
570 });
571 }); // topicFetchProcess
572
573 describe('subscriptionDeliveryProcess', function () {
574 let dbCtx, requestId, topic, topicId, subscription, subscriptionId;
575
576 beforeEach(function () {
577 dbCtx = {};
578 topic = {
579 url: 'https://example.com/topic/',
580 isDeleted: false,
581 contentHashAlgorithm: 'sha512',
582 content: 'Jackdaws love my big sphinx of quartz.',
583 };
584 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
585 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
586 subscriptionId = 'c5e6a3ac-dab8-11eb-b758-0025905f714a';
587 subscription = {
588 topicId,
589 callback: 'https://example.com/callback/123',
590 secret: 'superdupersecret',
591 signatureAlgorithm: 'sha512',
592 };
593
594 sinon.stub(communication, 'got').resolves({
595 statusCode: 200,
596 statusMessage: 'OK',
597 headers: {
598 'content-type': 'text/plain',
599 },
600 body: 'Jackdaws love my big sphinx of quartz.',
601 });
602
603 communication.db.topicGetContentById.resolves(topic);
604 communication.db.subscriptionGetById.resolves(subscription);
605 });
606
607 it('requires subscription to exist', async function () {
608 communication.db.subscriptionGetById.restore();
609 sinon.stub(communication.db, 'subscriptionGetById').resolves();
610 try {
611 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
612 assert.fail(noExpectedException);
613 } catch (e) {
614 assert(e instanceof Errors.InternalInconsistencyError);
615 }
616 });
617
618 it('requires topic to exist', async function () {
619 communication.db.topicGetContentById.restore();
620 sinon.stub(communication.db, 'topicGetContentById').resolves();
621 try {
622 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
623 assert.fail(noExpectedException);
624 } catch (e) {
625 assert(e instanceof Errors.InternalInconsistencyError);
626 }
627 });
628
629 it('succeeds', async function () {
630 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
631
632 assert(communication.db.subscriptionDeliveryComplete.called);
633 });
634
635 it('handles request error', async function () {
636 communication.got.restore();
637 sinon.stub(communication, 'got').throws();
638
639 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
640
641 assert(communication.db.subscriptionDeliveryIncomplete.called);
642 });
643
644 it('handles 5xx response', async function () {
645 communication.got.restore();
646 sinon.stub(communication, 'got').resolves({
647 statusCode: 500,
648 statusMessage: 'Internal Server Error',
649 headers: {
650 'content-type': 'text/plain',
651 },
652 });
653
654 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
655
656 assert(communication.db.subscriptionDeliveryIncomplete.called);
657 });
658
659 it('handles 4xx response', async function () {
660 communication.got.restore();
661 sinon.stub(communication, 'got').resolves({
662 statusCode: 404,
663 statusMessage: 'Not Found',
664 headers: {
665 'content-type': 'text/plain',
666 },
667 });
668
669 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
670
671 assert(communication.db.subscriptionDeliveryIncomplete.called);
672 });
673
674 it('handles 410 response', async function () {
675 communication.got.restore();
676 sinon.stub(communication, 'got').resolves({
677 statusCode: 410,
678 statusMessage: 'Gone',
679 headers: {
680 'content-type': 'text/plain',
681 },
682 });
683
684 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
685
686 assert(communication.db.subscriptionDeliveryGone.called);
687 });
688
689 it('unsubscribes when topic is deleted', async function () {
690 topic.isDeleted = true;
691 communication.db.topicGetContentById.restore();
692 sinon.stub(communication.db, 'topicGetContentById').resolves(topic);
693
694 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
695
696 assert(communication.db.verificationInsert.called);
697 assert(communication.db.subscriptionDeliveryComplete.called);
698 });
699 }); // subscriptionDeliveryProcess
700
701 describe('topicFetchClaimAndProcessById', function () {
702 let dbCtx, topicId, requestId;
703 beforeEach(function () {
704 dbCtx = {};
705 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
706 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
707 sinon.stub(communication, 'topicFetchProcess');
708 });
709 it('covers claim', async function () {
710 communication.db.topicFetchClaimById.resolves({
711 changes: 1,
712 });
713 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
714 assert(communication.topicFetchProcess.called);
715 });
716 it('covers no claim', async function () {
717 communication.db.topicFetchClaimById.resolves({
718 changes: 0,
719 });
720 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
721 assert(!communication.topicFetchProcess.called);
722 });
723 }); // topicFetchClaimAndProcessById
724
725 describe('verificationClaimAndProcessById', function () {
726 let dbCtx, verificationId, requestId;
727 beforeEach(function () {
728 dbCtx = {};
729 verificationId = '28488311-6652-42ea-9839-7bbc42b246cb';
730 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
731 sinon.stub(communication, 'verificationProcess');
732 });
733 it('covers claim', async function () {
734 communication.db.verificationClaimById.resolves({
735 changes: 1,
736 });
737 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
738 assert(communication.verificationProcess.called);
739 });
740 it('covers no claim', async function () {
741 communication.db.verificationClaimById.resolves({
742 changes: 0,
743 });
744 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
745 assert(!communication.verificationProcess.called);
746 });
747 }); // verificationClaimAndProcessById
748
749 describe('workFeed', function () {
750 let stubCtx, wanted;
751 beforeEach(function () {
752 stubCtx = {};
753 sinon.stub(communication, 'topicFetchProcess');
754 sinon.stub(communication, 'verificationProcess');
755 sinon.stub(communication, 'subscriptionDeliveryProcess');
756 });
757 it('succeeds', async function () {
758 const topicIds = [ { id: '' }, { id: '' } ];
759 communication.db.topicFetchClaim.resolves(topicIds);
760 const verificationIds = [ { id: '' }, { id: '' } ];
761 communication.db.verificationClaim.resolves(verificationIds);
762 const subscriptionIds = [ { id: '' }, { id: '' } ];
763 communication.db.subscriptionDeliveryClaim.resolves(subscriptionIds);
764 const expectedLength = [topicIds, verificationIds, subscriptionIds].map((x) => x.length).reduce((a, b) => a + b, 0);
765 wanted = 10;
766
767 const result = await communication.workFeed(stubCtx, wanted);
768
769 assert.strictEqual(result.length, expectedLength);
770 });
771 it('covers no wanted work', async function () {
772 wanted = 0;
773 const result = await communication.workFeed(stubCtx, wanted);
774 assert.strictEqual(result.length, 0);
775 assert(!communication.db.topicFetchClaim.called);
776 assert(!communication.db.verificationClaim.called);
777 assert(!communication.db.subscriptionDeliveryClaim.called);
778 });
779 it('deals with failure', async function () {
780 const topicIds = [ { id: '' }, { id: '' } ];
781 communication.db.topicFetchClaim.resolves(topicIds);
782 communication.db.verificationClaim.throws();
783 const expectedLength = topicIds.length;
784 wanted = 10;
785
786 const result = await communication.workFeed(stubCtx, wanted);
787
788 assert.strictEqual(result.length, expectedLength);
789 });
790 }); // workFeed
791
792 }); // Communication