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