3 const assert
= require('node:assert');
4 const sinon
= require('sinon');
6 const Communication
= require('../../src/communication');
7 const Config
= require('../../config');
8 const Errors
= require('../../src/errors');
10 const stubDb
= require('../stub-db');
11 const stubLogger
= require('../stub-logger');
13 const noExpectedException
= 'did not get expected exception';
15 describe('Communication', function () {
16 let communication
, options
;
18 beforeEach(function () {
19 options
= new Config('test');
20 communication
= new Communication(stubLogger
, stubDb
, options
);
24 afterEach(function () {
28 it('instantiates', function () {
29 assert(communication
);
32 it('covers config value', function () {
33 options
.dingus
.selfBaseUrl
= undefined;
34 communication
= new Communication(stubLogger
, stubDb
, options
);
37 describe('_init', function () {
38 it('covers', async
function () {
39 await communication
._init();
40 await communication
._init();
41 assert(communication
.Got
);
42 assert(communication
.got
);
46 describe('_onRetry', function () {
47 it('covers', function () {
50 communication
._onRetry(error
, retryCount
);
51 assert(communication
.logger
.debug
.called
);
55 describe('userAgentString', function () {
56 it('has default behavior', function () {
57 const result
= Communication
.userAgentString();
59 assert(result
.length
> 30);
61 it('is settable', function () {
62 const result
= Communication
.userAgentString({
65 implementation: 'custom',
68 assert
.strictEqual(result
, 'myhub/9.9.9 (custom)');
70 it('covers branches', function () {
71 const result
= Communication
.userAgentString({
77 assert
.strictEqual(result
, 'myhub/9.9.9');
81 describe('generateChallenge', function () {
82 it('generates a thing', async
function () {
83 const result
= await Communication
.generateChallenge();
85 assert(result
.length
);
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';
95 expected
= 'sha256=ee92148d9cd043cdfb8da7cf5ee1897abaafdb5ab840e85010abd4bf235fa31e';
97 it('signs a thing', function () {
98 const result
= Communication
.signature(message
, secret
, algorithm
);
99 assert
.strictEqual(result
, expected
);
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';
110 it('hashes', function () {
111 const result
= Communication
.contentHash(content
, algorithm
);
112 assert
.strictEqual(result
, expected
);
116 describe('verificationProcess', function () {
117 const challenge
= 'a_challenge';
118 let dbCtx
, callback
, requestId
, topicId
;
119 let topic
, verification
;
120 beforeEach(function () {
122 callback
= 'https://example.com/callback/?id=123';
123 requestId
= '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
124 topicId
= '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
127 url: 'https://example.com/blog/',
134 isPublisherValidated: true,
135 leaseSeconds: 864000,
138 sinon
.stub(Communication
, 'generateChallenge').resolves(challenge
);
139 sinon
.stub(communication
, 'publisherValidate').resolves(true);
140 sinon
.stub(communication
, 'got').resolves({
144 'content-type': 'text/plain',
149 communication
.db
.verificationGetById
.resolves(verification
);
150 communication
.db
.topicGetById
.resolves(topic
);
151 communication
.db
.verificationRelease
.resolves({});
152 communication
.db
.verificationUpdate
.resolves({});
153 communication
.db
.verificationIncomplete
.resolves({});
154 communication
.db
.verificationComplete
.resolves({});
157 it('errors on non-existent verification', async
function () {
158 communication
.db
.verificationGetById
.restore();
159 sinon
.stub(communication
.db
, 'verificationGetById').resolves();
162 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
163 assert
.fail(noExpectedException
);
165 assert(e
instanceof Errors
.InternalInconsistencyError
);
169 it('errors on non-existent topic', async
function () {
170 communication
.db
.topicGetById
.restore();
171 sinon
.stub(communication
.db
, 'topicGetById').resolves();
174 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
175 assert
.fail(noExpectedException
);
177 assert(e
instanceof Errors
.InternalInconsistencyError
);
181 it('skips inactive topic', async
function () {
182 communication
.db
.topicGetById
.restore();
183 topic
.isActive
= false;
184 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
186 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
188 assert(communication
.db
.verificationRelease
.called
);
189 assert(!communication
.got
.called
);
192 it('denies subscription to deleted topic', async
function () {
193 communication
.db
.topicGetById
.restore();
194 topic
.isDeleted
= true;
195 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
197 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
199 assert(communication
.db
.verificationUpdate
.called
);
200 assert
.strictEqual(verification
.mode
, 'denied');
203 it('checks publisher validation if needed', async
function() {
204 communication
.db
.verificationGetById
.restore();
205 verification
.isPublisherValidated
= false;
206 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
207 communication
.db
.topicGetById
.restore();
208 topic
.publisherValidationUrl
= 'https://example.com/publisher/';
209 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
211 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
213 assert(communication
.publisherValidate
.called
);
214 assert(communication
.db
.verificationComplete
.called
);
217 it('handles publisher validation failure', async
function() {
218 communication
.db
.verificationGetById
.restore();
219 verification
.isPublisherValidated
= false;
220 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
221 communication
.db
.topicGetById
.restore();
222 topic
.publisherValidationUrl
= 'https://example.com/publisher/';
223 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
224 communication
.publisherValidate
.restore();
225 sinon
.stub(communication
, 'publisherValidate').resolves(false);
227 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
229 assert(communication
.publisherValidate
.called
);
230 assert(communication
.db
.verificationIncomplete
.called
);
233 it('handles request error', async
function () {
234 communication
.got
.restore();
235 sinon
.stub(communication
, 'got').rejects(new Error());
237 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
239 assert(communication
.db
.verificationIncomplete
.called
);
242 it('handles 500 response', async
function () {
243 communication
.got
.restore();
244 sinon
.stub(communication
, 'got').resolves({
248 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
250 assert(communication
.db
.verificationIncomplete
.called
);
253 it('handles non-200 response', async
function () {
254 communication
.got
.restore();
255 sinon
.stub(communication
, 'got').resolves({
259 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
261 assert(communication
.db
.verificationComplete
.called
);
264 it('subscription succeeds', async
function () {
265 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
267 assert(communication
.db
.subscriptionUpsert
.called
);
268 assert(communication
.db
.verificationComplete
.called
);
271 it('unsubscription succeeds', async
function () {
272 communication
.db
.verificationGetById
.restore();
273 verification
.mode
= 'unsubscribe';
274 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
276 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
278 assert(communication
.db
.subscriptionDelete
.called
);
279 assert(communication
.db
.verificationComplete
.called
);
282 it('unsubscription from deleted topic deletes topic', async
function () {
283 communication
.db
.verificationGetById
.restore();
284 verification
.mode
= 'unsubscribe';
285 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
286 communication
.db
.topicGetById
.restore();
287 sinon
.stub(communication
.db
, 'topicGetById').resolves({
292 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
294 assert(communication
.db
.subscriptionDelete
.called
);
295 assert(communication
.db
.verificationComplete
.called
);
296 assert(communication
.db
.topicPendingDelete
.called
);
299 it('unsubscription denial succeeds', async
function () {
300 communication
.db
.verificationGetById
.restore();
301 verification
.mode
= 'unsubscribe';
302 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
303 communication
.got
.restore();
304 sinon
.stub(communication
, 'got').resolves({
308 'content-type': 'text/plain',
310 body: 'not the challenge',
313 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
315 assert(!communication
.db
.subscriptionDelete
.called
);
316 assert(communication
.db
.verificationComplete
.called
);
319 it('does not handle strange mode', async
function() {
320 communication
.db
.verificationGetById
.restore();
321 verification
.mode
= 'flarp';
322 sinon
.stub(communication
.db
, 'verificationGetById').resolves(verification
);
325 await communication
.verificationProcess(dbCtx
, callback
, topicId
, requestId
);
326 assert
.fail(noExpectedException
);
328 assert(e
instanceof Errors
.InternalInconsistencyError
);
331 }); // verificationProcess
333 describe('publisherValidate', function () {
334 let dbCtx
, topic
, verification
;
335 beforeEach(function () {
338 url: 'https://example.com/topic/',
339 publisherValidationUrl: 'https://example.com/pub_valid/',
342 callback: 'https://exmaple.com/callback/?id=123',
343 httpFrom: 'user@example.com',
344 httpRemoteAddr: '127.0.0.0',
347 sinon
.stub(communication
, 'got').resolves({
351 'content-type': 'application/json',
355 communication
.db
.verificationIncomplete
.resolves();
356 communication
.db
.verificationUpdate
.resolves();
357 communication
.db
.verificationValidated
.resolves();
360 it('succeeds', async
function () {
361 const result
= await communication
.publisherValidate(dbCtx
, topic
, verification
);
363 assert(communication
.db
.verificationValidated
.called
);
364 assert
.strictEqual(result
, true);
367 it('succeeds with rejection', async
function () {
368 communication
.got
.restore();
369 sinon
.stub(communication
, 'got').resolves({
371 statusMessage: 'Bad Request',
373 'content-type': 'application/json',
377 const result
= await communication
.publisherValidate(dbCtx
, topic
, verification
);
379 assert(communication
.db
.verificationValidated
.called
);
380 assert(communication
.db
.verificationUpdate
.called
);
381 assert
.strictEqual(result
, true);
384 it('defers on request server error', async
function () {
385 communication
.got
.restore();
386 sinon
.stub(communication
, 'got').resolves({
388 statusMessage: 'Bad Gateway',
390 'content-type': 'text/plain',
394 const result
= await communication
.publisherValidate(dbCtx
, topic
, verification
);
396 assert
.strictEqual(result
, false);
399 it('handles request error', async
function () {
400 communication
.got
.restore();
401 sinon
.stub(communication
, 'got').rejects(new Error());
403 const result
= await communication
.publisherValidate(dbCtx
, topic
, verification
);
405 assert
.strictEqual(result
, false);
408 }); // publisherValidate
410 describe('topicFetchProcess', function () {
411 let dbCtx
, topic
, requestId
, topicId
;
413 beforeEach(function () {
416 url: 'https://example.com/topic/',
418 contentHashAlgorithm: 'sha512',
420 requestId
= '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
421 topicId
= '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
423 sinon
.stub(communication
, 'got').resolves({
427 'content-type': 'text/plain',
428 link: '<https://example.com/hub/>; rel="hub"',
429 'last-modified': 'Thu, 18 Nov 2021 20:34:35 GMT',
430 'etag': '"9c104-1673e-5d1161636d742"',
432 body: 'Jackdaws love my big sphinx of quartz.',
435 communication
.db
.topicGetById
.resolves(topic
);
438 it('requires topic exists', async
function () {
439 communication
.db
.topicGetById
.restore();
440 sinon
.stub(communication
.db
, 'topicGetById').resolves();
443 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
444 assert
.fail(noExpectedException
);
446 assert(e
instanceof Errors
.InternalInconsistencyError
);
450 it ('skips deleted topic', async
function () {
451 communication
.db
.topicGetById
.restore();
452 topic
.isDeleted
= true;
453 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
455 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
457 assert(!communication
.got
.called
);
460 it('handles request error', async
function () {
461 communication
.got
.restore();
462 sinon
.stub(communication
, 'got').rejects(new Error());
464 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
466 assert(communication
.db
.topicFetchIncomplete
.called
);
469 it('handles 500 response', async
function () {
470 communication
.got
.restore();
471 sinon
.stub(communication
, 'got').resolves({
473 statusMessage: 'Internal Server Error',
475 'content-type': 'text/plain',
479 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
481 assert(communication
.db
.topicFetchIncomplete
.called
);
484 it('handles bad response', async
function () {
485 communication
.got
.restore();
486 sinon
.stub(communication
, 'got').resolves({
488 statusMessage: 'Not Found',
490 'content-type': 'text/plain',
494 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
496 assert(communication
.db
.topicFetchIncomplete
.called
);
499 it('recognizes unchanged content', async
function () {
500 communication
.db
.topicGetById
.restore();
501 topic
.contentHash
= 'a630999c61738f3e066d79a1b299a295c5d0598c173e0904d04a707d43988e3e81660bfc1b1779377f4ec26f837d1bb31fa2b860c9ad2d37495d83de32647fea';
502 sinon
.stub(communication
.db
, 'topicGetById').resolves(topic
);
504 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
506 assert(communication
.db
.topicFetchComplete
.called
);
507 assert(!communication
.db
.topicSetContent
.called
);
510 it('recognizes 304 response', async
function () {
511 topic
.httpLastModified
= 'Thu, 18 Nov 2021 20:34:35 GMT';
512 topic
.httpEtag
= '"9c104-1673e-5d1161636d742"';
513 communication
.db
.topicGetById
.resolves(topic
);
514 communication
.got
.resolves({
518 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
520 assert(communication
.db
.topicFetchComplete
.called
);
521 assert(!communication
.db
.topicSetContent
.called
);
524 it('updates content', async
function () {
525 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
527 assert(communication
.db
.topicFetchComplete
.called
);
528 assert(communication
.db
.topicSetContent
.called
);
531 it('updates content with lax link enforcement', async
function () {
532 communication
.got
.restore();
533 sinon
.stub(communication
, 'got').resolves({
537 'content-type': 'text/plain',
538 link: '<https://example.com/other/hub/>; rel="hub"',
540 body: 'Jackdaws love my big sphinx of quartz.',
543 communication
.options
.communication
.strictTopicHubLink
= false;
545 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
547 assert(communication
.db
.topicFetchComplete
.called
);
548 assert(communication
.db
.topicSetContent
.called
);
551 it('deletes topic when hub relation unsatisfied', async
function () {
552 communication
.got
.restore();
553 sinon
.stub(communication
, 'got').resolves({
557 'content-type': 'text/plain',
558 link: '<https://example.com/other/hub/>; rel="hub"',
560 body: 'Jackdaws love my big sphinx of quartz.',
563 await communication
.topicFetchProcess(dbCtx
, topicId
, requestId
);
565 assert(communication
.db
.topicFetchComplete
.called
);
566 assert(communication
.db
.topicDeleted
.called
);
568 }); // topicFetchProcess
570 describe('subscriptionDeliveryProcess', function () {
571 let dbCtx
, requestId
, topic
, topicId
, subscription
, subscriptionId
;
573 beforeEach(function () {
576 url: 'https://example.com/topic/',
578 contentHashAlgorithm: 'sha512',
579 content: 'Jackdaws love my big sphinx of quartz.',
581 requestId
= '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
582 topicId
= '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
583 subscriptionId
= 'c5e6a3ac-dab8-11eb-b758-0025905f714a';
586 callback: 'https://example.com/callback/123',
587 secret: 'superdupersecret',
588 signatureAlgorithm: 'sha512',
591 sinon
.stub(communication
, 'got').resolves({
595 'content-type': 'text/plain',
597 body: 'Jackdaws love my big sphinx of quartz.',
600 communication
.db
.topicGetContentById
.resolves(topic
);
601 communication
.db
.subscriptionGetById
.resolves(subscription
);
604 it('requires subscription to exist', async
function () {
605 communication
.db
.subscriptionGetById
.restore();
606 sinon
.stub(communication
.db
, 'subscriptionGetById').resolves();
608 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
609 assert
.fail(noExpectedException
);
611 assert(e
instanceof Errors
.InternalInconsistencyError
);
615 it('requires topic to exist', async
function () {
616 communication
.db
.topicGetContentById
.restore();
617 sinon
.stub(communication
.db
, 'topicGetContentById').resolves();
619 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
620 assert
.fail(noExpectedException
);
622 assert(e
instanceof Errors
.InternalInconsistencyError
);
626 it('succeeds', async
function () {
627 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
629 assert(communication
.db
.subscriptionDeliveryComplete
.called
);
632 it('handles request error', async
function () {
633 communication
.got
.restore();
634 sinon
.stub(communication
, 'got').throws();
636 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
638 assert(communication
.db
.subscriptionDeliveryIncomplete
.called
);
641 it('handles 5xx response', async
function () {
642 communication
.got
.restore();
643 sinon
.stub(communication
, 'got').resolves({
645 statusMessage: 'Internal Server Error',
647 'content-type': 'text/plain',
651 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
653 assert(communication
.db
.subscriptionDeliveryIncomplete
.called
);
656 it('handles 4xx response', async
function () {
657 communication
.got
.restore();
658 sinon
.stub(communication
, 'got').resolves({
660 statusMessage: 'Not Found',
662 'content-type': 'text/plain',
666 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
668 assert(communication
.db
.subscriptionDeliveryIncomplete
.called
);
671 it('handles 410 response', async
function () {
672 communication
.got
.restore();
673 sinon
.stub(communication
, 'got').resolves({
675 statusMessage: 'Gone',
677 'content-type': 'text/plain',
681 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
683 assert(communication
.db
.subscriptionDeliveryGone
.called
);
686 it('unsubscribes when topic is deleted', async
function () {
687 topic
.isDeleted
= true;
688 communication
.db
.topicGetContentById
.restore();
689 sinon
.stub(communication
.db
, 'topicGetContentById').resolves(topic
);
691 await communication
.subscriptionDeliveryProcess(dbCtx
, subscriptionId
, requestId
);
693 assert(communication
.db
.verificationInsert
.called
);
694 assert(communication
.db
.subscriptionDeliveryComplete
.called
);
696 }); // subscriptionDeliveryProcess
698 describe('topicFetchClaimAndProcessById', function () {
699 let dbCtx
, topicId
, requestId
;
700 beforeEach(function () {
702 requestId
= '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
703 topicId
= '234ec6fb-f1cd-4ac3-8ea9-29ed42ae0e21';
704 sinon
.stub(communication
, 'topicFetchProcess');
706 it('covers claim', async
function () {
707 communication
.db
.topicFetchClaimById
.resolves({
710 await communication
.topicFetchClaimAndProcessById(dbCtx
, topicId
, requestId
);
711 assert(communication
.topicFetchProcess
.called
);
713 it('covers no claim', async
function () {
714 communication
.db
.topicFetchClaimById
.resolves({
717 await communication
.topicFetchClaimAndProcessById(dbCtx
, topicId
, requestId
);
718 assert(!communication
.topicFetchProcess
.called
);
720 }); // topicFetchClaimAndProcessById
722 describe('verificationClaimAndProcessById', function () {
723 let dbCtx
, verificationId
, requestId
;
724 beforeEach(function () {
726 verificationId
= '28488311-6652-42ea-9839-7bbc42b246cb';
727 requestId
= '7d37ea20-4ef7-417e-a08d-c0ba71269ab1';
728 sinon
.stub(communication
, 'verificationProcess');
730 it('covers claim', async
function () {
731 communication
.db
.verificationClaimById
.resolves({
734 await communication
.verificationClaimAndProcessById(dbCtx
, verificationId
, requestId
);
735 assert(communication
.verificationProcess
.called
);
737 it('covers no claim', async
function () {
738 communication
.db
.verificationClaimById
.resolves({
741 await communication
.verificationClaimAndProcessById(dbCtx
, verificationId
, requestId
);
742 assert(!communication
.verificationProcess
.called
);
744 }); // verificationClaimAndProcessById
746 describe('workFeed', function () {
748 beforeEach(function () {
750 sinon
.stub(communication
, 'topicFetchProcess');
751 sinon
.stub(communication
, 'verificationProcess');
752 sinon
.stub(communication
, 'subscriptionDeliveryProcess');
754 it('succeeds', async
function () {
755 const topicIds
= [ { id: '' }, { id: '' } ];
756 communication
.db
.topicFetchClaim
.resolves(topicIds
);
757 const verificationIds
= [ { id: '' }, { id: '' } ];
758 communication
.db
.verificationClaim
.resolves(verificationIds
);
759 const subscriptionIds
= [ { id: '' }, { id: '' } ];
760 communication
.db
.subscriptionDeliveryClaim
.resolves(subscriptionIds
);
761 const expectedLength
= [topicIds
, verificationIds
, subscriptionIds
].map((x
) => x
.length
).reduce((a
, b
) => a
+ b
, 0);
764 const result
= await communication
.workFeed(stubCtx
, wanted
);
766 assert
.strictEqual(result
.length
, expectedLength
);
768 it('covers no wanted work', async
function () {
770 const result
= await communication
.workFeed(stubCtx
, wanted
);
771 assert
.strictEqual(result
.length
, 0);
772 assert(!communication
.db
.topicFetchClaim
.called
);
773 assert(!communication
.db
.verificationClaim
.called
);
774 assert(!communication
.db
.subscriptionDeliveryClaim
.called
);
776 it('deals with failure', async
function () {
777 const topicIds
= [ { id: '' }, { id: '' } ];
778 communication
.db
.topicFetchClaim
.resolves(topicIds
);
779 communication
.db
.verificationClaim
.throws();
780 const expectedLength
= topicIds
.length
;
783 const result
= await communication
.workFeed(stubCtx
, wanted
);
785 assert
.strictEqual(result
.length
, expectedLength
);