2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
6 const assert
= require('assert');
7 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
9 const Manager
= require('../../src/manager');
10 const Config
= require('../../config');
11 const common
= require('../../src/common');
12 const Errors
= require('../../src/errors');
13 const DBErrors
= require('../../src/db/errors');
15 const stubDb
= require('../stub-db');
16 const stubLogger
= require('../stub-logger');
17 const testData
= require('../test-data/manager');
19 const noExpectedException
= 'did not get expected exception';
21 describe('Manager', function () {
25 beforeEach(function () {
26 options
= new Config('test');
28 getHeader : sinon
.stub(),
32 setHeader: sinon
.stub(),
38 manager
= new Manager(stubLogger
, stubDb
, options
);
39 sinon
.stub(manager
.communication
, 'verificationProcess');
40 sinon
.stub(manager
.communication
, 'topicFetchProcess');
41 sinon
.stub(manager
.communication
, 'topicFetchClaimAndProcessById');
45 afterEach(function () {
49 it('instantiates', function () {
53 describe('getRoot', function () {
54 beforeEach(function () {
55 sinon
.stub(common
, 'isClientCached');
58 it('normal response', async
function () {
59 common
.isClientCached
.returns(false);
60 await manager
.getRoot(req
, res
, ctx
);
61 assert(res
.end
.called
);
65 describe('getHealthcheck', function () {
66 it('normal response', async
function () {
67 await manager
.getHealthcheck(res
, ctx
);
68 assert(res
.end
.called
);
72 describe('getInfo', function () {
73 it('requires query param', async
function() {
76 await manager
.getInfo(res
, ctx
);
77 assert
.fail(noExpectedException
);
79 assert
.strictEqual(e
.statusCode
, 400);
82 it('requires parsable query param', async
function() {
83 ctx
.queryParams
= { topic: 'not a url' };
85 await manager
.getInfo(res
, ctx
);
86 assert
.fail(noExpectedException
);
88 assert
.strictEqual(e
.statusCode
, 400);
91 it('does not find unhandled topic', async
function() {
92 ctx
.queryParams
= { topic: 'https://example.com/blog/' };
94 await manager
.getInfo(res
, ctx
);
95 assert
.fail(noExpectedException
);
97 assert
.strictEqual(e
.statusCode
, 404);
100 it('returns a count', async
function() {
101 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
103 topic: 'https://example.com/blog/',
105 await manager
.getInfo(res
, ctx
);
106 assert(res
.end
.called
);
108 it('returns a count as json', async
function() {
109 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
110 ctx
.responseType
= 'application/json';
112 topic: 'https://example.com/blog/',
114 await manager
.getInfo(res
, ctx
);
115 assert(res
.end
.called
);
117 it('returns a count as json as override format', async
function() {
118 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
119 ctx
.responseType
= 'text/html';
121 topic: 'https://example.com/blog/',
124 await manager
.getInfo(res
, ctx
);
125 assert(res
.end
.called
);
126 assert(res
.setHeader
.called
);
128 it('returns an svg badge as override format', async
function() {
129 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
130 ctx
.responseType
= 'text/html';
132 topic: 'https://example.com/blog/',
135 await manager
.getInfo(res
, ctx
);
136 assert(res
.end
.called
);
137 assert(res
.setHeader
.called
);
141 describe('_historyBarCaption', function () {
142 it('covers today, none', function () {
143 const result
= Manager
._historyBarCaption(0, 0);
144 assert
.strictEqual(result
, 'today, no updates');
146 it('covers yesterday, singular', function () {
147 const result
= Manager
._historyBarCaption(1, 1);
148 assert
.strictEqual(result
, 'yesterday, 1 update');
150 it('covers older, plural', function () {
151 const result
= Manager
._historyBarCaption(7, 3);
152 assert
.strictEqual(result
, '7 days ago, 3 updates');
154 }); // _historyBarCaption
156 describe('getHistorySVG', function () {
157 beforeEach(function () {
158 manager
.db
.topicPublishHistory
.resolves([0, 1, 2, 1, 0, 1, 2, 0, 1]);
160 it('covers', async
function () {
161 await manager
.getHistorySVG(res
, ctx
);
162 assert(res
.end
.called
);
166 describe('getAdminOverview', function () {
167 beforeEach(function () {
168 manager
.db
.topicGetAll
.resolves([
170 id: '56c557ce-e667-11eb-bd80-0025905f714a',
172 url: 'https://example.com/',
173 leaseSecondsPreferred: 123,
175 leaseSecondsMax: 123456789,
176 publisherValidationUrl: null,
177 contentHashAlgorithm: 'hashy',
180 lastPublish: new Date(-Infinity
),
181 contentFetchNextAttempt: undefined,
182 contentFetchAttemptsSinceSuccess: 3,
183 contentUpdated: new Date(0),
190 it('covers', async
function () {
191 await manager
.getAdminOverview(res
, ctx
);
192 assert(res
.end
.called
);
194 it('covers non-matching profile', async
function () {
196 authenticatedProfile: 'https://different.example.com/profile',
198 await manager
.getAdminOverview(res
, ctx
);
199 assert
.deepStrictEqual(ctx
.topics
, []);
200 assert(res
.end
.called
);
202 }); // getAdminOverview
204 describe('getTopicDetails', function () {
205 beforeEach(function () {
206 ctx
.params
.topicId
= '56c557ce-e667-11eb-bd80-0025905f714a';
207 manager
.db
.topicGetById
.resolves({
208 id: '56c557ce-e667-11eb-bd80-0025905f714a',
210 url: 'https://example.com/topic',
211 leaseSecondsPreferred: 123,
213 leaseSecondsMax: 123456789,
214 publisherValidationUrl: null,
215 contentHashAlgorithm: 'hashy',
218 lastPublish: new Date(-Infinity
),
219 contentFetchNextAttempt: undefined,
220 contentFetchAttemptsSinceSuccess: 3,
221 contentUpdated: new Date(0),
226 manager
.db
.subscriptionsByTopicId
.resolves([{
229 topicId: '56c557ce-e667-11eb-bd80-0025905f714a',
231 verified: new Date(),
234 signatureAlgorithm: 'hmacy',
237 contentDelivered: new Date(),
238 deliveryAttemptsSinceSuccess: 0,
239 deliveryNextAttempt: new Date(-Infinity
),
241 manager
.db
.topicPublishHistory
.resolves([0, 1, 0, 1, 0]);
243 it('covers', async
function() {
244 await manager
.getTopicDetails(res
, ctx
);
245 assert(res
.end
.called
);
247 it('covers non-matching profile', async
function () {
249 authenticatedProfile: 'https://different.example.com/profile',
251 await manager
.getTopicDetails(res
, ctx
);
252 assert
.strictEqual(ctx
.topic
, null);
253 assert(res
.end
.called
);
255 it('covers matching profile', async
function () {
257 authenticatedProfile: 'https://example.com/',
259 await manager
.getTopicDetails(res
, ctx
);
261 assert(res
.end
.called
);
263 }); // getTopicDetails
265 describe('postRoot', function () {
266 let origProcessImmediately
;
267 beforeEach(function () {
268 origProcessImmediately
= manager
.options
.manager
.processImmediately
;
271 this.afterEach(function () {
272 manager
.options
.manager
.processImmediately
= origProcessImmediately
;
274 it('requires parameters', async
function () {
276 await manager
.postRoot(req
, res
, ctx
);
277 assert
.fail(noExpectedException
);
279 assert
.strictEqual(e
.message
, 'Bad Request');
282 it('accepts valid subscription', async
function () {
283 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
284 manager
.db
.topicGetByUrl
.resolves({
287 manager
.db
.verificationInsert
.resolves({
289 lastInsertRowid: undefined,
292 await manager
.postRoot(req
, res
, ctx
);
293 assert(manager
.db
.verificationInsert
.called
);
294 assert(res
.end
.called
);
296 it('accepts valid subscription without claiming work', async
function () {
297 manager
.options
.manager
.processImmediately
= false;
298 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
299 manager
.db
.topicGetByUrl
.resolves({
302 manager
.db
.verificationInsert
.resolves({
304 lastInsertRowid: undefined,
307 await manager
.postRoot(req
, res
, ctx
);
308 assert(manager
.db
.verificationInsert
.called
);
309 assert(!manager
.communication
.verificationProcess
.called
);
310 assert(res
.end
.called
);
312 it('accepts valid subscription, covers processVerification failure', async
function () {
313 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
314 manager
.communication
.verificationProcess
.rejects('failed');
315 manager
.db
.topicGetByUrl
.resolves({
318 manager
.db
.verificationInsert
.resolves({
320 lastInsertRowid: undefined,
323 await manager
.postRoot(req
, res
, ctx
);
324 assert(manager
.db
.verificationInsert
.called
);
325 assert(res
.end
.called
);
326 assert(manager
.logger
.error
.called
);
328 it('covers db.verificationInsert failure', async
function () {
329 const expectedException
= new Error('failure');
330 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
331 manager
.db
.topicGetByUrl
.resolves({
334 manager
.db
.verificationInsert
.rejects(expectedException
);
335 assert
.rejects(async () => {
336 await manager
.postRoot(req
, res
, ctx
);
337 }, expectedException
);
339 it('accepts valid unsubscription', async
function () {
340 ctx
= Object
.assign({}, testData
.validUnsubscribeCtx
);
341 manager
.db
.topicGetByUrl
.resolves({
344 manager
.db
.subscriptionGet
.resolves({
347 manager
.db
.verificationInsert
.resolves({
349 lastInsertRowid: undefined,
352 await manager
.postRoot(req
, res
, ctx
);
353 assert(res
.end
.called
);
355 it('accepts valid publish', async
function () {
356 ctx
= Object
.assign({}, testData
.validPublishCtx
);
357 manager
.db
.topicGetByUrl
.resolves({
360 manager
.db
.topicFetchRequested
.resolves({
362 lastInsertRowid: undefined,
365 await manager
.postRoot(req
, res
, ctx
);
366 assert(res
.end
.called
);
370 describe('_profileControlsTopic', function () {
371 let profileUrlObj
, topicUrlObj
;
372 it('allows exact match', function () {
373 profileUrlObj
= new URL('https://profile.example.com/');
374 topicUrlObj
= new URL('https://profile.example.com/');
375 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
376 assert
.strictEqual(result
, true);
378 it('allows descendent-path match', function () {
379 profileUrlObj
= new URL('https://profile.example.com/');
380 topicUrlObj
= new URL('https://profile.example.com/feed/atom');
381 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
382 assert
.strictEqual(result
, true);
384 it('disallows non-descendent-path', function () {
385 profileUrlObj
= new URL('https://profile.example.com/itsame');
386 topicUrlObj
= new URL('https://profile.example.com/');
387 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
388 assert
.strictEqual(result
, false);
390 it('disallows non-matched host', function () {
391 profileUrlObj
= new URL('https://profile.example.com/itsame');
392 topicUrlObj
= new URL('https://elsewhere.example.com/itsame/feed');
393 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
394 assert
.strictEqual(result
, false);
396 }); // _profileControlsTopic
398 describe('_getRootData', function () {
399 it('extracts expected values', function () {
400 req
.getHeader
.returns('user@example.com');
401 ctx
= Object
.assign({}, testData
.validSubscribeCtx
)
402 const result
= Manager
._getRootData(req
, ctx
);
403 assert
.deepStrictEqual(result
, testData
.validRootData
);
407 describe('_validateRootData', function () {
408 // This only wraps the other _check functions, not bothering with coverage.
409 }); // _validateRootData
411 describe('_checkTopic', function () {
412 let dbCtx
, data
, warn
, err
;
415 leaseSecondsPreferred: 86400 * 10,
416 leaseSecondsMax: 86400 * 20,
417 leaseSecondsMin: 86400,
419 beforeEach(function () {
425 it('succeeds', async
function () {
427 topic: 'http://example.com/blog',
429 manager
.db
.topicGetByUrl
.resolves(topic
);
430 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
431 assert
.strictEqual(warn
.length
, 0, warn
);
432 assert
.strictEqual(err
.length
, 0, err
);
433 assert
.strictEqual(data
.topicId
, 111);
434 assert
.strictEqual(data
.leaseSeconds
, 864000);
436 it('errors on unknown topic', async
function () {
437 manager
.db
.topicGetByUrl
.resolves();
438 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
439 assert
.strictEqual(warn
.length
, 0, warn
);
440 assert
.strictEqual(err
.length
, 1, err
);
442 it('warns on lease under min range', async
function () {
444 topic: 'http://example.com/blog',
447 manager
.db
.topicGetByUrl
.resolves(topic
);
448 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
449 assert
.strictEqual(warn
.length
, 1, warn
);
450 assert
.strictEqual(err
.length
, 0, err
);
451 assert
.strictEqual(data
.topicId
, 111);
452 assert
.strictEqual(data
.leaseSeconds
, 86400);
454 it('warns on lease over max range', async
function () {
456 topic: 'http://example.com/blog',
457 leaseSeconds: 86400 * 100,
459 manager
.db
.topicGetByUrl
.resolves(topic
);
460 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
461 assert
.strictEqual(warn
.length
, 1, warn
);
462 assert
.strictEqual(err
.length
, 0, err
);
463 assert
.strictEqual(data
.topicId
, 111);
464 assert
.strictEqual(data
.leaseSeconds
, 86400 * 20);
466 it('sets publisher validation state when available', async
function () {
468 topic: 'http://example.com/blog',
470 manager
.db
.topicGetByUrl
.resolves(Object
.assign({}, topic
, {
471 publisherValidationUrl: 'http://example.com/validate',
473 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
474 assert
.strictEqual(warn
.length
, 0, warn
);
475 assert
.strictEqual(err
.length
, 0, err
);
476 assert
.strictEqual(data
.topicId
, 111);
477 assert
.strictEqual(data
.leaseSeconds
, 864000);
478 assert
.strictEqual(data
.isPublisherValidated
, false);
480 it('accepts new public subscribe topic', async
function () {
481 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves(topic
);
483 topic: 'http://example.com/blog',
485 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
486 assert
.strictEqual(warn
.length
, 0, 'unexpected warnings length');
487 assert
.strictEqual(err
.length
, 0, 'unexpected errors length');
488 assert
.strictEqual(data
.topicId
, 111, 'unexpected topic id');
490 it('does not accept new public subscribe for invalid topic', async
function () {
491 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves(topic
);
493 topic: 'not a topic',
495 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
496 assert
.strictEqual(warn
.length
, 0, 'unexpected warnings length');
497 assert
.strictEqual(err
.length
, 1, 'unexpected errors length');
501 describe('_checkCallbackAndSecrets', function () {
503 let origStrictSecrets
;
505 origStrictSecrets
= manager
.options
.manager
.strictSecrets
;
507 beforeEach(function () {
512 afterEach(function () {
513 manager
.options
.manager
.strictSecrets
= origStrictSecrets
;
515 it('succeeds', function () {
517 callback: 'https://example.com/callback',
521 manager
._checkCallbackAndSecrets(data
, warn
, err
);
522 assert
.strictEqual(warn
.length
, 0, warn
);
523 assert
.strictEqual(err
.length
, 0, err
);
525 it('errors with invalid callback', function () {
527 callback: 'not a url',
531 manager
._checkCallbackAndSecrets(data
, warn
, err
);
532 assert
.strictEqual(warn
.length
, 0, warn
);
533 assert
.strictEqual(err
.length
, 1, err
);
535 it('errors when secret too large', function () {
537 callback: 'https://example.com/callback',
538 secret: 'x'.repeat(256),
541 manager
._checkCallbackAndSecrets(data
, warn
, err
);
542 assert
.strictEqual(warn
.length
, 0, warn
);
543 assert
.strictEqual(err
.length
, 1, err
);
545 it('warns when callback is insecure', function () {
547 callback: 'http://example.com/callback',
550 manager
._checkCallbackAndSecrets(data
, warn
, err
);
551 assert
.strictEqual(warn
.length
, 1, warn
);
552 assert
.strictEqual(err
.length
, 0, err
);
554 it('warns when hub is insecure with secret', function () {
556 callback: 'https://example.com/callback',
560 manager
._checkCallbackAndSecrets(data
, warn
, err
);
561 assert
.strictEqual(warn
.length
, 1, warn
);
562 assert
.strictEqual(err
.length
, 0, err
);
564 it('errors when callback is insecure with secret and strict', function () {
565 manager
.options
.manager
.strictSecrets
= true;
567 callback: 'http://example.com/callback',
571 manager
._checkCallbackAndSecrets(data
, warn
, err
);
572 assert
.strictEqual(warn
.length
, 1, warn
);
573 assert
.strictEqual(err
.length
, 1, err
);
575 }); // _checkCallbackAndSecrets
577 describe('_checkMode', function () {
578 let dbCtx
, data
, warn
, err
;
579 beforeEach(function () {
585 it('subscribe succeeds', async
function () {
589 await manager
._checkMode(dbCtx
, data
, warn
, err
);
590 assert
.strictEqual(warn
.length
, 0);
591 assert
.strictEqual(err
.length
, 0);
593 it('unsubscribe succeeds', async
function () {
596 callback: 'http://example.com',
599 manager
.db
.subscriptionGet
.resolves({
600 expires: (Date
.now() / 1000) + 60,
602 await manager
._checkMode(dbCtx
, data
, warn
, err
);
603 assert
.strictEqual(warn
.length
, 0, warn
);
604 assert
.strictEqual(err
.length
, 0, err
);
606 it('unsubscribe requires valid data', async
function () {
609 callback: 'http://example.com',
612 manager
.db
.subscriptionGet
.resolves({
613 expires: (Date
.now() / 1000) - 60,
615 await manager
._checkMode(dbCtx
, data
, warn
, err
);
616 assert
.strictEqual(warn
.length
, 0, warn
);
617 assert
.strictEqual(err
.length
, 1, err
);
619 it('unsubscribe ignores expired subscription', async
function () {
622 callback: 'http://example.com',
625 manager
.db
.subscriptionGet
.resolves({
626 expires: (Date
.now() / 1000) - 60,
628 await manager
._checkMode(dbCtx
, data
, warn
, err
);
629 assert
.strictEqual(warn
.length
, 0, warn
);
630 assert
.strictEqual(err
.length
, 1, err
);
634 describe('_publishTopics', function () {
635 let dbCtx
, data
, requestId
;
636 beforeEach(function () {
641 it('succeeds', async
function () {
642 manager
.db
.topicGetByUrl
.resolves({
645 Object
.assign(data
, testData
.validPublishRootData
);
646 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
647 assert
.strictEqual(topicResults
.length
, 1);
648 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
649 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
650 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
652 it('fails bad url', async
function () {
653 Object
.assign(data
, testData
.validPublishRootData
, { topic: 'not_a_url' });
654 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
655 assert
.strictEqual(topicResults
.length
, 1);
656 assert
.strictEqual(topicResults
[0].err
.length
, 1, 'unexpected errors length');
657 assert
.strictEqual(topicResults
[0].warn
.length
, 0);
659 it('accepts new public publish topic', async
function () {
660 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
663 Object
.assign(data
, testData
.validPublishRootData
);
664 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
665 assert
.strictEqual(topicResults
.length
, 1);
666 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
667 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
668 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
670 it('does not publish deleted topic', async
function () {
671 manager
.db
.topicGetByUrl
.resolves({
675 Object
.assign(data
, testData
.validPublishRootData
);
676 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
677 assert
.strictEqual(topicResults
.length
, 1);
678 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
679 assert
.strictEqual(topicResults
[0].err
.length
, 1, 'unexpected errors length');
680 assert
.strictEqual(topicResults
[0].topicId
, undefined, 'unexpected topic id');
682 it('no topics', async
function() {
683 Object
.assign(data
, testData
.validPublishRootData
);
685 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
686 assert
.strictEqual(topicResults
.length
, 0);
688 it('multiple valid topics', async
function () {
689 manager
.db
.topicGetByUrl
.resolves({
692 Object
.assign(data
, testData
.validPublishRootData
);
693 data
.url
= ['https://example.com/first', 'https://example.com/second'];
694 data
.topic
= ['https://example.com/third'];
695 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
696 assert
.strictEqual(topicResults
.length
, 3);
697 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
698 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
699 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
700 assert
.strictEqual(topicResults
[1].warn
.length
, 0, 'unexpected warnings length');
701 assert
.strictEqual(topicResults
[1].err
.length
, 0, 'unexpected errors length');
702 assert
.strictEqual(topicResults
[1].topicId
, 222, 'unexpected topic id');
703 assert
.strictEqual(topicResults
[2].warn
.length
, 0, 'unexpected warnings length');
704 assert
.strictEqual(topicResults
[2].err
.length
, 0, 'unexpected errors length');
705 assert
.strictEqual(topicResults
[2].topicId
, 222, 'unexpected topic id');
707 it('mix of valid and invalid topics', async
function () {
708 manager
.db
.topicGetByUrl
.onCall(1).resolves().resolves({
711 Object
.assign(data
, testData
.validPublishRootData
);
712 data
.url
= ['https://example.com/first', 'not a url'];
713 data
.topic
= ['https://example.com/third'];
714 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
715 assert
.strictEqual(topicResults
.length
, 3);
716 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
717 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
718 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
719 assert
.strictEqual(topicResults
[1].warn
.length
, 0, 'unexpected warnings length');
720 assert
.strictEqual(topicResults
[1].err
.length
, 1, 'unexpected errors length');
721 assert
.strictEqual(topicResults
[1].topicId
, undefined, 'unexpected topic id');
722 assert
.strictEqual(topicResults
[2].warn
.length
, 0, 'unexpected warnings length');
723 assert
.strictEqual(topicResults
[2].err
.length
, 0, 'unexpected errors length');
724 assert
.strictEqual(topicResults
[2].topicId
, 222, 'unexpected topic id');
726 }); // _publishTopics
728 describe('_publishRequest', function () {
729 let dbCtx
, data
, res
, ctx
;
730 beforeEach(function () {
738 it('requires a topic', async
function () {
740 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
741 assert
.fail(noExpectedException
);
743 assert(e
instanceof Errors
.ResponseError
);
746 it('processes one topic', async
function() {
747 manager
.db
.topicGetByUrl
.resolves({
750 Object
.assign(data
, testData
.validPublishRootData
);
751 manager
.db
.topicFetchRequested
.resolves();
752 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
753 assert(manager
.db
.topicFetchRequested
.called
);
754 assert
.strictEqual(res
.statusCode
, 202);
755 assert(res
.end
.called
);
757 it('processes mix of valid and invalid topics', async
function () {
758 ctx
.responseType
= 'application/json';
759 manager
.db
.topicGetByUrl
.onCall(1).resolves().resolves({
762 Object
.assign(data
, testData
.validPublishRootData
);
763 data
.url
= ['https://example.com/first', 'not a url'];
764 data
.topic
= ['https://example.com/third'];
765 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
766 assert
.strictEqual(res
.statusCode
, 207);
767 assert(res
.end
.called
);
769 it('covers topicFetchRequest failure', async
function () {
770 manager
.db
.topicGetByUrl
.resolves({
773 Object
.assign(data
, testData
.validPublishRootData
);
774 const expected
= new Error('boo');
775 manager
.db
.topicFetchRequested
.rejects(expected
);
777 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
778 assert
.fail(noExpectedException
);
780 assert
.deepStrictEqual(e
, expected
);
783 it('covers immediate processing error', async
function() {
784 manager
.options
.manager
.processImmediately
= true;
785 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
788 manager
.communication
.topicFetchClaimAndProcessById
.rejects();
789 Object
.assign(data
, testData
.validPublishRootData
);
790 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
791 assert(manager
.db
.topicFetchRequested
.called
);
792 assert
.strictEqual(res
.statusCode
, 202);
793 assert(res
.end
.called
);
794 assert(manager
.communication
.topicFetchClaimAndProcessById
.called
)
796 it('covers no immediate processing', async
function() {
797 manager
.options
.manager
.processImmediately
= false;
798 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
801 Object
.assign(data
, testData
.validPublishRootData
);
802 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
803 assert(manager
.db
.topicFetchRequested
.called
);
804 assert
.strictEqual(res
.statusCode
, 202);
805 assert(res
.end
.called
);
806 assert(!manager
.communication
.topicFetchClaimAndProcessById
.called
)
808 }); // _publishRequest
810 describe('multiPublishContent', function () {
812 beforeEach(function () {
814 url: 'https://example.com/first',
819 statusMessage: 'Accepted',
824 err: [ 'invalid topic url (failed to parse url)' ],
827 statusMessage: 'Bad Request',
830 it('covers json response', function () {
831 ctx
.responseType
= 'application/json';
832 const expected
= '[{"href":"https://example.com/first","status":202,"statusMessage":"Accepted","errors":[],"warnings":[]},{"href":"not a url","status":400,"statusMessage":"Bad Request","errors":["invalid topic url (failed to parse url)"],"warnings":[]}]';
833 const result
= Manager
.multiPublishContent(ctx
, publishTopics
);
834 assert
.deepStrictEqual(result
, expected
);
836 it('covers text response', function () {
837 ctx
.responseType
= 'text/plain';
838 const expected
= `https://example.com/first [202 Accepted]
840 not a url [400 Bad Request]
841 \terror: invalid topic url (failed to parse url)`;
842 const result
= Manager
.multiPublishContent(ctx
, publishTopics
);
843 assert
.deepStrictEqual(result
, expected
);
845 }); // multiPublishContent
847 describe('processTasks', function () {
848 it('covers', async
function () {
849 sinon
.stub(manager
.communication
.worker
, 'process').resolves();
850 await manager
.processTasks(res
, ctx
);
851 assert(manager
.communication
.worker
.process
.called
);
852 assert(res
.end
.called
);
854 it('covers error', async
function () {
855 sinon
.stub(manager
.communication
.worker
, 'process').rejects();
856 await manager
.processTasks(res
, ctx
);
857 assert(manager
.communication
.worker
.process
.called
);
858 assert(res
.end
.called
);
862 describe('updateTopic', function () {
863 it('fails if no topic exists', async
function () {
865 await manager
.updateTopic(res
, ctx
);
866 assert
.fail(noExpectedException
);
868 assert(e
instanceof Errors
.ResponseError
);
871 it('deletes', async
function () {
872 ctx
.method
= 'DELETE';
873 manager
.db
.topicGetById
.resolves({});
874 await manager
.updateTopic(res
, ctx
);
875 assert(manager
.db
.topicDeleted
.called
);
877 it('does not patch without data', async
function () {
878 manager
.db
.topicGetById
.resolves({});
879 await manager
.updateTopic(res
, ctx
);
880 assert(!manager
.db
.topicUpdate
.called
);
881 assert
.strictEqual(res
.statusCode
, 204);
883 it('does not patch with same data', async
function () {
884 manager
.db
.topicGetById
.resolves({
885 leaseSecondsPreferred: '86400',
888 leaseSecondsPreferred: '86400',
890 await manager
.updateTopic(res
, ctx
);
891 assert(!manager
.db
.topicUpdate
.called
);
892 assert
.strictEqual(res
.statusCode
, 204);
894 it('patches', async
function () {
896 leaseSecondsPreferred: '86400',
898 manager
.db
.topicGetById
.resolves({});
899 await manager
.updateTopic(res
, ctx
);
900 assert(manager
.db
.topicUpdate
.called
);
902 it('handles validation error', async
function () {
904 leaseSecondsPreferred: 'blorp',
906 manager
.db
.topicGetById
.resolves({});
907 manager
.db
.topicUpdate
.rejects(new DBErrors
.DataValidation('something'));
909 await manager
.updateTopic(res
, ctx
);
910 assert
.fail(noExpectedException
);
912 assert(e
instanceof Errors
.ResponseError
);
913 assert
.strictEqual(e
.statusCode
, 400);
916 it('handles generic error', async
function () {
917 const expected
= new Error('blah');
919 leaseSecondsPreferred: '123',
921 manager
.db
.topicGetById
.resolves({});
922 manager
.db
.topicUpdate
.rejects(expected
);
924 await manager
.updateTopic(res
, ctx
);
925 assert
.fail(noExpectedException
);
927 assert
.deepStrictEqual(e
, expected
);
932 describe('updateSubscription', function () {
933 it('fails if no subscription exists', async
function () {
935 await manager
.updateSubscription(res
, ctx
);
936 assert
.fail(noExpectedException
);
938 assert(e
instanceof Errors
.ResponseError
);
941 it('deletes', async
function () {
942 ctx
.method
= 'DELETE';
943 manager
.db
.subscriptionGetById
.resolves({});
944 await manager
.updateSubscription(res
, ctx
);
945 assert(manager
.db
.verificationInsert
.called
);
947 it('does not patch without data', async
function () {
948 manager
.db
.subscriptionGetById
.resolves({});
949 await manager
.updateSubscription(res
, ctx
);
950 assert(!manager
.db
.subscriptionUpdate
.called
);
951 assert
.strictEqual(res
.statusCode
, 204);
953 it('does not patch with same data', async
function () {
954 manager
.db
.subscriptionGetById
.resolves({
955 signatureAlgorithm: 'sha256',
958 signatureAlgorithm: 'sha256',
960 await manager
.updateSubscription(res
, ctx
);
961 assert(!manager
.db
.subscriptionUpdate
.called
);
962 assert
.strictEqual(res
.statusCode
, 204);
964 it('patches', async
function () {
966 signatureAlgorithm: 'sha256',
968 manager
.db
.subscriptionGetById
.resolves({});
969 await manager
.updateSubscription(res
, ctx
);
970 assert(manager
.db
.subscriptionUpdate
.called
);
972 it('handles validation error', async
function () {
974 signatureAlgorithm: 123,
976 manager
.db
.subscriptionGetById
.resolves({});
977 manager
.db
.subscriptionUpdate
.rejects(new DBErrors
.DataValidation('something'));
979 await manager
.updateSubscription(res
, ctx
);
980 assert
.fail(noExpectedException
);
982 assert(e
instanceof Errors
.ResponseError
);
983 assert
.strictEqual(e
.statusCode
, 400);
986 it('handles generic error', async
function () {
987 const expected
= new Error('blah');
989 signatureAlgorithm: 'blorp',
991 manager
.db
.subscriptionGetById
.resolves({});
992 manager
.db
.subscriptionUpdate
.rejects(expected
);
994 await manager
.updateSubscription(res
, ctx
);
995 assert
.fail(noExpectedException
);
997 assert
.deepStrictEqual(e
, expected
);
1000 }); // updateSubscription