database migration 1.0.4, store topic fetch etag/last-modified, provide these when...
[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 from deleted topic deletes topic', async function () {
409 communication.db.verificationGetById.restore();
410 verification.mode = 'unsubscribe';
411 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
412 communication.db.topicGetById.restore();
413 sinon.stub(communication.db, 'topicGetById').resolves({
414 ...topic,
415 isDeleted: true,
416 });
417
418 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
419
420 assert(communication.db.subscriptionDelete.called);
421 assert(communication.db.verificationComplete.called);
422 assert(communication.db.topicPendingDelete.called);
423 });
424
425 it('unsubscription denial succeeds', async function () {
426 communication.db.verificationGetById.restore();
427 verification.mode = 'unsubscribe';
428 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
429 communication.axios.restore();
430 sinon.stub(communication, 'axios').resolves({
431 status: 200,
432 statusText: 'OK',
433 headers: {
434 'content-type': 'text/plain',
435 },
436 data: 'not the challenge',
437 });
438
439 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
440
441 assert(!communication.db.subscriptionDelete.called);
442 assert(communication.db.verificationComplete.called);
443 });
444
445 it('does not handle strange mode', async function() {
446 communication.db.verificationGetById.restore();
447 verification.mode = 'flarp';
448 sinon.stub(communication.db, 'verificationGetById').resolves(verification);
449
450 try {
451 await communication.verificationProcess(dbCtx, callback, topicId, requestId);
452 assert.fail(noExpectedException);
453 } catch (e) {
454 assert(e instanceof Errors.InternalInconsistencyError);
455 }
456 });
457 }); // verificationProcess
458
459 describe('publisherValidate', function () {
460 let dbCtx, topic, verification;
461 beforeEach(function () {
462 dbCtx = {};
463 topic = {
464 url: 'https://example.com/topic/',
465 publisherValidationUrl: 'https://example.com/pub_valid/',
466 };
467 verification = {
468 callback: 'https://exmaple.com/callback/?id=123',
469 httpFrom: 'user@example.com',
470 httpRemoteAddr: '127.0.0.0',
471 };
472
473 sinon.stub(communication, 'axios').resolves({
474 status: 200,
475 statusText: 'OK',
476 headers: {
477 'content-type': 'application/json',
478 },
479 });
480
481 communication.db.verificationIncomplete.resolves();
482 communication.db.verificationUpdate.resolves();
483 communication.db.verificationValidated.resolves();
484 });
485
486 it('succeeds', async function () {
487 const result = await communication.publisherValidate(dbCtx, topic, verification);
488
489 assert(communication.db.verificationValidated.called);
490 assert.strictEqual(result, true);
491 });
492
493 it('succeeds with rejection', async function () {
494 communication.axios.restore();
495 sinon.stub(communication, 'axios').resolves({
496 status: 400,
497 statusText: 'Bad Request',
498 headers: {
499 'content-type': 'application/json',
500 },
501 });
502
503 const result = await communication.publisherValidate(dbCtx, topic, verification);
504
505 assert(communication.db.verificationValidated.called);
506 assert(communication.db.verificationUpdate.called);
507 assert.strictEqual(result, true);
508 });
509
510 it('defers on request server error', async function () {
511 communication.axios.restore();
512 sinon.stub(communication, 'axios').resolves({
513 status: 502,
514 statusText: 'Bad Gateway',
515 headers: {
516 'content-type': 'text/plain',
517 },
518 });
519
520 const result = await communication.publisherValidate(dbCtx, topic, verification);
521
522 assert.strictEqual(result, false);
523 });
524
525 it('handles request error', async function () {
526 communication.axios.restore();
527 sinon.stub(communication, 'axios').throws(new Error());
528
529 const result = await communication.publisherValidate(dbCtx, topic, verification);
530
531 assert.strictEqual(result, false);
532 });
533
534 }); // publisherValidate
535
536 describe('topicFetchProcess', function () {
537 let dbCtx, topic, requestId, topicId;
538
539 beforeEach(function () {
540 dbCtx = {};
541 topic = {
542 url: 'https://example.com/topic/',
543 isDeleted: false,
544 contentHashAlgorithm: 'sha512',
545 };
546 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
547 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
548
549 sinon.stub(communication, 'axios').resolves({
550 status: 200,
551 statusText: 'OK',
552 headers: {
553 'content-type': 'text/plain',
554 link: '<https://example.com/hub/>; rel="hub"',
555 'last-modified': 'Thu, 18 Nov 2021 20:34:35 GMT',
556 'etag': '"9c104-1673e-5d1161636d742"',
557 },
558 data: 'Jackdaws love my big sphinx of quartz.',
559 });
560
561 communication.db.topicGetById.resolves(topic);
562 });
563
564 it('requires topic exists', async function () {
565 communication.db.topicGetById.restore();
566 sinon.stub(communication.db, 'topicGetById').resolves();
567
568 try {
569 await communication.topicFetchProcess(dbCtx, topicId, requestId);
570 assert.fail(noExpectedException);
571 } catch (e) {
572 assert(e instanceof Errors.InternalInconsistencyError);
573 }
574 });
575
576 it ('skips deleted topic', async function () {
577 communication.db.topicGetById.restore();
578 topic.isDeleted = true;
579 sinon.stub(communication.db, 'topicGetById').resolves(topic);
580
581 await communication.topicFetchProcess(dbCtx, topicId, requestId);
582
583 assert(!communication.axios.called);
584 });
585
586 it('handles request error', async function () {
587 communication.axios.restore();
588 sinon.stub(communication, 'axios').throws(new Error());
589
590 await communication.topicFetchProcess(dbCtx, topicId, requestId);
591
592 assert(communication.db.topicFetchIncomplete.called);
593 });
594
595 it('handles 500 response', async function () {
596 communication.axios.restore();
597 sinon.stub(communication, 'axios').resolves({
598 status: 500,
599 statusText: 'Internal Server Error',
600 headers: {
601 'content-type': 'text/plain',
602 },
603 });
604
605 await communication.topicFetchProcess(dbCtx, topicId, requestId);
606
607 assert(communication.db.topicFetchIncomplete.called);
608 });
609
610 it('handles bad response', async function () {
611 communication.axios.restore();
612 sinon.stub(communication, 'axios').resolves({
613 status: 404,
614 statusText: 'Not Found',
615 headers: {
616 'content-type': 'text/plain',
617 },
618 });
619
620 await communication.topicFetchProcess(dbCtx, topicId, requestId);
621
622 assert(communication.db.topicFetchIncomplete.called);
623 });
624
625 it('recognizes unchanged content', async function () {
626 communication.db.topicGetById.restore();
627 topic.contentHash = 'a630999c61738f3e066d79a1b299a295c5d0598c173e0904d04a707d43988e3e81660bfc1b1779377f4ec26f837d1bb31fa2b860c9ad2d37495d83de32647fea';
628 sinon.stub(communication.db, 'topicGetById').resolves(topic);
629
630 await communication.topicFetchProcess(dbCtx, topicId, requestId);
631
632 assert(communication.db.topicFetchComplete.called);
633 assert(!communication.db.topicSetContent.called);
634 });
635
636 it('recognizes 304 response', async function () {
637 topic.httpLastModified = 'Thu, 18 Nov 2021 20:34:35 GMT';
638 topic.httpEtag = '"9c104-1673e-5d1161636d742"';
639 communication.db.topicGetById.resolves(topic);
640 communication.axios.resolves({
641 status: 304,
642 });
643
644 await communication.topicFetchProcess(dbCtx, topicId, requestId);
645
646 assert(communication.db.topicFetchComplete.called);
647 assert(!communication.db.topicSetContent.called);
648 });
649
650 it('updates content', async function () {
651 await communication.topicFetchProcess(dbCtx, topicId, requestId);
652
653 assert(communication.db.topicFetchComplete.called);
654 assert(communication.db.topicSetContent.called);
655 });
656
657 it('updates content with lax link enforcement', async function () {
658 communication.axios.restore();
659 sinon.stub(communication, 'axios').resolves({
660 status: 200,
661 statusText: 'OK',
662 headers: {
663 'content-type': 'text/plain',
664 link: '<https://example.com/other/hub/>; rel="hub"',
665 },
666 data: 'Jackdaws love my big sphinx of quartz.',
667 });
668
669 communication.options.communication.strictTopicHubLink = false;
670
671 await communication.topicFetchProcess(dbCtx, topicId, requestId);
672
673 assert(communication.db.topicFetchComplete.called);
674 assert(communication.db.topicSetContent.called);
675 });
676
677 it('deletes topic when hub relation unsatisfied', async function () {
678 communication.axios.restore();
679 sinon.stub(communication, 'axios').resolves({
680 status: 200,
681 statusText: 'OK',
682 headers: {
683 'content-type': 'text/plain',
684 link: '<https://example.com/other/hub/>; rel="hub"',
685 },
686 data: 'Jackdaws love my big sphinx of quartz.',
687 });
688
689 await communication.topicFetchProcess(dbCtx, topicId, requestId);
690
691 assert(communication.db.topicFetchComplete.called);
692 assert(communication.db.topicDeleted.called);
693 });
694 }); // topicFetchProcess
695
696 describe('subscriptionDeliveryProcess', function () {
697 let dbCtx, requestId, topic, topicId, subscription, subscriptionId;
698
699 beforeEach(function () {
700 dbCtx = {};
701 topic = {
702 url: 'https://example.com/topic/',
703 isDeleted: false,
704 contentHashAlgorithm: 'sha512',
705 content: 'Jackdaws love my big sphinx of quartz.',
706 };
707 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
708 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
709 subscriptionId = 'c5e6a3ac-dab8-11eb-b758-0025905f714a';
710 subscription = {
711 topicId,
712 callback: 'https://example.com/callback/123',
713 secret: 'superdupersecret',
714 signatureAlgorithm: 'sha512',
715 };
716
717 sinon.stub(communication, 'axios').resolves({
718 status: 200,
719 statusText: 'OK',
720 headers: {
721 'content-type': 'text/plain',
722 },
723 data: 'Jackdaws love my big sphinx of quartz.',
724 });
725
726 communication.db.topicGetContentById.resolves(topic);
727 communication.db.subscriptionGetById.resolves(subscription);
728 });
729
730 it('requires subscription to exist', async function () {
731 communication.db.subscriptionGetById.restore();
732 sinon.stub(communication.db, 'subscriptionGetById').resolves();
733 try {
734 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
735 assert.fail(noExpectedException);
736 } catch (e) {
737 assert(e instanceof Errors.InternalInconsistencyError);
738 }
739 });
740
741 it('requires topic to exist', async function () {
742 communication.db.topicGetContentById.restore();
743 sinon.stub(communication.db, 'topicGetContentById').resolves();
744 try {
745 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
746 assert.fail(noExpectedException);
747 } catch (e) {
748 assert(e instanceof Errors.InternalInconsistencyError);
749 }
750 });
751
752 it('succeeds', async function () {
753 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
754
755 assert(communication.db.subscriptionDeliveryComplete.called);
756 });
757
758 it('handles request error', async function () {
759 communication.axios.restore();
760 sinon.stub(communication, 'axios').throws();
761
762 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
763
764 assert(communication.db.subscriptionDeliveryIncomplete.called);
765 });
766
767 it('handles 5xx response', async function () {
768 communication.axios.restore();
769 sinon.stub(communication, 'axios').resolves({
770 status: 500,
771 statusText: 'Internal Server Error',
772 headers: {
773 'content-type': 'text/plain',
774 },
775 });
776
777 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
778
779 assert(communication.db.subscriptionDeliveryIncomplete.called);
780 });
781
782 it('handles 4xx response', async function () {
783 communication.axios.restore();
784 sinon.stub(communication, 'axios').resolves({
785 status: 404,
786 statusText: 'Not Found',
787 headers: {
788 'content-type': 'text/plain',
789 },
790 });
791
792 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
793
794 assert(communication.db.subscriptionDeliveryIncomplete.called);
795 });
796
797 it('handles 410 response', async function () {
798 communication.axios.restore();
799 sinon.stub(communication, 'axios').resolves({
800 status: 410,
801 statusText: 'Gone',
802 headers: {
803 'content-type': 'text/plain',
804 },
805 });
806
807 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
808
809 assert(communication.db.subscriptionDeliveryGone.called);
810 });
811
812 it('unsubscribes when topic is deleted', async function () {
813 topic.isDeleted = true;
814 communication.db.topicGetContentById.restore();
815 sinon.stub(communication.db, 'topicGetContentById').resolves(topic);
816
817 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
818
819 assert(communication.db.verificationInsert.called);
820 assert(communication.db.subscriptionDeliveryComplete.called);
821 });
822 }); // subscriptionDeliveryProcess
823
824 describe('topicFetchClaimAndProcessById', function () {
825 let dbCtx, topicId, requestId;
826 beforeEach(function () {
827 dbCtx = {};
828 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
829 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
830 sinon.stub(communication, 'topicFetchProcess');
831 });
832 it('covers claim', async function () {
833 communication.db.topicFetchClaimById.resolves({
834 changes: 1,
835 })
836 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
837 assert(communication.topicFetchProcess.called);
838 });
839 it('covers no claim', async function () {
840 communication.db.topicFetchClaimById.resolves({
841 changes: 0,
842 })
843 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
844 assert(!communication.topicFetchProcess.called);
845 });
846 }); // topicFetchClaimAndProcessById
847
848 describe('verificationClaimAndProcessById', function () {
849 let dbCtx, verificationId, requestId;
850 beforeEach(function () {
851 dbCtx = {};
852 verificationId = '28488311-6652-42ea-9839-7bbc42b246cb';
853 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
854 sinon.stub(communication, 'verificationProcess');
855 });
856 it('covers claim', async function () {
857 communication.db.verificationClaimById.resolves({
858 changes: 1,
859 })
860 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
861 assert(communication.verificationProcess.called);
862 });
863 it('covers no claim', async function () {
864 communication.db.verificationClaimById.resolves({
865 changes: 0,
866 })
867 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
868 assert(!communication.verificationProcess.called);
869 });
870 }); // verificationClaimAndProcessById
871
872 describe('workFeed', function () {
873 let stubCtx, wanted;
874 beforeEach(function () {
875 stubCtx = {};
876 sinon.stub(communication, 'topicFetchProcess');
877 sinon.stub(communication, 'verificationProcess');
878 sinon.stub(communication, 'subscriptionDeliveryProcess');
879 });
880 it('succeeds', async function () {
881 const topicIds = [ { id: '' }, { id: '' } ];
882 communication.db.topicFetchClaim.resolves(topicIds);
883 const verificationIds = [ { id: '' }, { id: '' } ];
884 communication.db.verificationClaim.resolves(verificationIds);
885 const subscriptionIds = [ { id: '' }, { id: '' } ];
886 communication.db.subscriptionDeliveryClaim.resolves(subscriptionIds);
887 const expectedLength = [topicIds, verificationIds, subscriptionIds].map((x) => x.length).reduce((a, b) => a + b, 0);
888 wanted = 10;
889
890 const result = await communication.workFeed(stubCtx, wanted);
891
892 assert.strictEqual(result.length, expectedLength);
893 });
894 it('covers no wanted work', async function () {
895 wanted = 0;
896 const result = await communication.workFeed(stubCtx, wanted);
897 assert.strictEqual(result.length, 0);
898 assert(!communication.db.topicFetchClaim.called);
899 assert(!communication.db.verificationClaim.called);
900 assert(!communication.db.subscriptionDeliveryClaim.called);
901 });
902 it('deals with failure', async function () {
903 const topicIds = [ { id: '' }, { id: '' } ];
904 communication.db.topicFetchClaim.resolves(topicIds);
905 communication.db.verificationClaim.throws();
906 const expectedLength = topicIds.length;
907 wanted = 10;
908
909 const result = await communication.workFeed(stubCtx, wanted);
910
911 assert.strictEqual(result.length, expectedLength);
912 });
913 }); // workFeed
914
915 }); // Communication