960a0f994b27a572236a92131638b326c59f4ece
[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 },
556 data: 'Jackdaws love my big sphinx of quartz.',
557 });
558
559 communication.db.topicGetById.resolves(topic);
560 });
561
562 it('requires topic exists', async function () {
563 communication.db.topicGetById.restore();
564 sinon.stub(communication.db, 'topicGetById').resolves();
565
566 try {
567 await communication.topicFetchProcess(dbCtx, topicId, requestId);
568 assert.fail(noExpectedException);
569 } catch (e) {
570 assert(e instanceof Errors.InternalInconsistencyError);
571 }
572 });
573
574 it ('skips deleted topic', async function () {
575 communication.db.topicGetById.restore();
576 topic.isDeleted = true;
577 sinon.stub(communication.db, 'topicGetById').resolves(topic);
578
579 await communication.topicFetchProcess(dbCtx, topicId, requestId);
580
581 assert(!communication.axios.called);
582 });
583
584 it('handles request error', async function () {
585 communication.axios.restore();
586 sinon.stub(communication, 'axios').throws(new Error());
587
588 await communication.topicFetchProcess(dbCtx, topicId, requestId);
589
590 assert(communication.db.topicFetchIncomplete.called);
591 });
592
593 it('handles 500 response', async function () {
594 communication.axios.restore();
595 sinon.stub(communication, 'axios').resolves({
596 status: 500,
597 statusText: 'Internal Server Error',
598 headers: {
599 'content-type': 'text/plain',
600 },
601 });
602
603 await communication.topicFetchProcess(dbCtx, topicId, requestId);
604
605 assert(communication.db.topicFetchIncomplete.called);
606 });
607
608 it('handles bad response', async function () {
609 communication.axios.restore();
610 sinon.stub(communication, 'axios').resolves({
611 status: 404,
612 statusText: 'Not Found',
613 headers: {
614 'content-type': 'text/plain',
615 },
616 });
617
618 await communication.topicFetchProcess(dbCtx, topicId, requestId);
619
620 assert(communication.db.topicFetchIncomplete.called);
621 });
622
623 it('recognizes unchanged content', async function () {
624 communication.db.topicGetById.restore();
625 topic.contentHash = 'a630999c61738f3e066d79a1b299a295c5d0598c173e0904d04a707d43988e3e81660bfc1b1779377f4ec26f837d1bb31fa2b860c9ad2d37495d83de32647fea';
626 sinon.stub(communication.db, 'topicGetById').resolves(topic);
627
628 await communication.topicFetchProcess(dbCtx, topicId, requestId);
629
630 assert(communication.db.topicFetchComplete.called);
631 assert(!communication.db.topicSetContent.called);
632 });
633
634 it('updates content', async function () {
635 await communication.topicFetchProcess(dbCtx, topicId, requestId);
636
637 assert(communication.db.topicFetchComplete.called);
638 assert(communication.db.topicSetContent.called);
639 });
640
641 it('updates content with lax link enforcement', async function () {
642 communication.axios.restore();
643 sinon.stub(communication, 'axios').resolves({
644 status: 200,
645 statusText: 'OK',
646 headers: {
647 'content-type': 'text/plain',
648 link: '<https://example.com/other/hub/>; rel="hub"',
649 },
650 data: 'Jackdaws love my big sphinx of quartz.',
651 });
652
653 communication.options.communication.strictTopicHubLink = false;
654
655 await communication.topicFetchProcess(dbCtx, topicId, requestId);
656
657 assert(communication.db.topicFetchComplete.called);
658 assert(communication.db.topicSetContent.called);
659 });
660
661 it('deletes topic when hub relation unsatisfied', async function () {
662 communication.axios.restore();
663 sinon.stub(communication, 'axios').resolves({
664 status: 200,
665 statusText: 'OK',
666 headers: {
667 'content-type': 'text/plain',
668 link: '<https://example.com/other/hub/>; rel="hub"',
669 },
670 data: 'Jackdaws love my big sphinx of quartz.',
671 });
672
673 await communication.topicFetchProcess(dbCtx, topicId, requestId);
674
675 assert(communication.db.topicFetchComplete.called);
676 assert(communication.db.topicDeleted.called);
677 });
678 }); // topicFetchProcess
679
680 describe('subscriptionDeliveryProcess', function () {
681 let dbCtx, requestId, topic, topicId, subscription, subscriptionId;
682
683 beforeEach(function () {
684 dbCtx = {};
685 topic = {
686 url: 'https://example.com/topic/',
687 isDeleted: false,
688 contentHashAlgorithm: 'sha512',
689 content: 'Jackdaws love my big sphinx of quartz.',
690 };
691 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
692 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
693 subscriptionId = 'c5e6a3ac-dab8-11eb-b758-0025905f714a';
694 subscription = {
695 topicId,
696 callback: 'https://example.com/callback/123',
697 secret: 'superdupersecret',
698 signatureAlgorithm: 'sha512',
699 };
700
701 sinon.stub(communication, 'axios').resolves({
702 status: 200,
703 statusText: 'OK',
704 headers: {
705 'content-type': 'text/plain',
706 },
707 data: 'Jackdaws love my big sphinx of quartz.',
708 });
709
710 communication.db.topicGetContentById.resolves(topic);
711 communication.db.subscriptionGetById.resolves(subscription);
712 });
713
714 it('requires subscription to exist', async function () {
715 communication.db.subscriptionGetById.restore();
716 sinon.stub(communication.db, 'subscriptionGetById').resolves();
717 try {
718 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
719 assert.fail(noExpectedException);
720 } catch (e) {
721 assert(e instanceof Errors.InternalInconsistencyError);
722 }
723 });
724
725 it('requires topic to exist', async function () {
726 communication.db.topicGetContentById.restore();
727 sinon.stub(communication.db, 'topicGetContentById').resolves();
728 try {
729 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
730 assert.fail(noExpectedException);
731 } catch (e) {
732 assert(e instanceof Errors.InternalInconsistencyError);
733 }
734 });
735
736 it('succeeds', async function () {
737 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
738
739 assert(communication.db.subscriptionDeliveryComplete.called);
740 });
741
742 it('handles request error', async function () {
743 communication.axios.restore();
744 sinon.stub(communication, 'axios').throws();
745
746 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
747
748 assert(communication.db.subscriptionDeliveryIncomplete.called);
749 });
750
751 it('handles 5xx response', async function () {
752 communication.axios.restore();
753 sinon.stub(communication, 'axios').resolves({
754 status: 500,
755 statusText: 'Internal Server Error',
756 headers: {
757 'content-type': 'text/plain',
758 },
759 });
760
761 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
762
763 assert(communication.db.subscriptionDeliveryIncomplete.called);
764 });
765
766 it('handles 4xx response', async function () {
767 communication.axios.restore();
768 sinon.stub(communication, 'axios').resolves({
769 status: 404,
770 statusText: 'Not Found',
771 headers: {
772 'content-type': 'text/plain',
773 },
774 });
775
776 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
777
778 assert(communication.db.subscriptionDeliveryIncomplete.called);
779 });
780
781 it('handles 410 response', async function () {
782 communication.axios.restore();
783 sinon.stub(communication, 'axios').resolves({
784 status: 410,
785 statusText: 'Gone',
786 headers: {
787 'content-type': 'text/plain',
788 },
789 });
790
791 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
792
793 assert(communication.db.subscriptionDeliveryGone.called);
794 });
795
796 it('unsubscribes when topic is deleted', async function () {
797 topic.isDeleted = true;
798 communication.db.topicGetContentById.restore();
799 sinon.stub(communication.db, 'topicGetContentById').resolves(topic);
800
801 await communication.subscriptionDeliveryProcess(dbCtx, subscriptionId, requestId);
802
803 assert(communication.db.verificationInsert.called);
804 assert(communication.db.subscriptionDeliveryComplete.called);
805 });
806 }); // subscriptionDeliveryProcess
807
808 describe('topicFetchClaimAndProcessById', function () {
809 let dbCtx, topicId, requestId;
810 beforeEach(function () {
811 dbCtx = {};
812 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
813 topicId = '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
814 sinon.stub(communication, 'topicFetchProcess');
815 });
816 it('covers claim', async function () {
817 communication.db.topicFetchClaimById.resolves({
818 changes: 1,
819 })
820 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
821 assert(communication.topicFetchProcess.called);
822 });
823 it('covers no claim', async function () {
824 communication.db.topicFetchClaimById.resolves({
825 changes: 0,
826 })
827 await communication.topicFetchClaimAndProcessById(dbCtx, topicId, requestId);
828 assert(!communication.topicFetchProcess.called);
829 });
830 }); // topicFetchClaimAndProcessById
831
832 describe('verificationClaimAndProcessById', function () {
833 let dbCtx, verificationId, requestId;
834 beforeEach(function () {
835 dbCtx = {};
836 verificationId = '28488311-6652-42ea-9839-7bbc42b246cb';
837 requestId = '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
838 sinon.stub(communication, 'verificationProcess');
839 });
840 it('covers claim', async function () {
841 communication.db.verificationClaimById.resolves({
842 changes: 1,
843 })
844 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
845 assert(communication.verificationProcess.called);
846 });
847 it('covers no claim', async function () {
848 communication.db.verificationClaimById.resolves({
849 changes: 0,
850 })
851 await communication.verificationClaimAndProcessById(dbCtx, verificationId, requestId);
852 assert(!communication.verificationProcess.called);
853 });
854 }); // verificationClaimAndProcessById
855
856 describe('workFeed', function () {
857 let stubCtx, wanted;
858 beforeEach(function () {
859 stubCtx = {};
860 sinon.stub(communication, 'topicFetchProcess');
861 sinon.stub(communication, 'verificationProcess');
862 sinon.stub(communication, 'subscriptionDeliveryProcess');
863 });
864 it('succeeds', async function () {
865 const topicIds = [ { id: '' }, { id: '' } ];
866 communication.db.topicFetchClaim.resolves(topicIds);
867 const verificationIds = [ { id: '' }, { id: '' } ];
868 communication.db.verificationClaim.resolves(verificationIds);
869 const subscriptionIds = [ { id: '' }, { id: '' } ];
870 communication.db.subscriptionDeliveryClaim.resolves(subscriptionIds);
871 const expectedLength = [topicIds, verificationIds, subscriptionIds].map((x) => x.length).reduce((a, b) => a + b, 0);
872 wanted = 10;
873
874 const result = await communication.workFeed(stubCtx, wanted);
875
876 assert.strictEqual(result.length, expectedLength);
877 });
878 it('covers no wanted work', async function () {
879 wanted = 0;
880 const result = await communication.workFeed(stubCtx, wanted);
881 assert.strictEqual(result.length, 0);
882 assert(!communication.db.topicFetchClaim.called);
883 assert(!communication.db.verificationClaim.called);
884 assert(!communication.db.subscriptionDeliveryClaim.called);
885 });
886 it('deals with failure', async function () {
887 const topicIds = [ { id: '' }, { id: '' } ];
888 communication.db.topicFetchClaim.resolves(topicIds);
889 communication.db.verificationClaim.throws();
890 const expectedLength = topicIds.length;
891 wanted = 10;
892
893 const result = await communication.workFeed(stubCtx, wanted);
894
895 assert.strictEqual(result.length, expectedLength);
896 });
897 }); // workFeed
898
899 }); // Communication