3 const assert
= require('node:assert');
4 const sinon
= require('sinon');
6 const Manager
= require('../../src/manager');
7 const Config
= require('../../config');
8 const common
= require('../../src/common');
9 const Errors
= require('../../src/errors');
10 const DBErrors
= require('../../src/db/errors');
12 const stubDb
= require('../stub-db');
13 const stubLogger
= require('../stub-logger');
14 const testData
= require('../test-data/manager');
16 const noExpectedException
= 'did not get expected exception';
18 describe('Manager', function () {
22 beforeEach(function () {
23 options
= new Config('test');
25 getHeader : sinon
.stub(),
29 setHeader: sinon
.stub(),
35 manager
= new Manager(stubLogger
, stubDb
, options
);
36 sinon
.stub(manager
.communication
, 'verificationProcess');
37 sinon
.stub(manager
.communication
, 'topicFetchProcess');
38 sinon
.stub(manager
.communication
, 'topicFetchClaimAndProcessById');
42 afterEach(function () {
46 it('instantiates', function () {
50 describe('getRoot', function () {
51 beforeEach(function () {
52 sinon
.stub(common
, 'isClientCached');
55 it('normal response', async
function () {
56 common
.isClientCached
.returns(false);
57 await manager
.getRoot(req
, res
, ctx
);
58 assert(res
.end
.called
);
62 describe('getHealthcheck', function () {
63 it('normal response', async
function () {
64 await manager
.getHealthcheck(res
, ctx
);
65 assert(res
.end
.called
);
69 describe('getInfo', function () {
70 it('requires query param', async
function() {
73 await manager
.getInfo(res
, ctx
);
74 assert
.fail(noExpectedException
);
76 assert
.strictEqual(e
.statusCode
, 400);
79 it('requires parsable query param', async
function() {
80 ctx
.queryParams
= { topic: 'not a url' };
82 await manager
.getInfo(res
, ctx
);
83 assert
.fail(noExpectedException
);
85 assert
.strictEqual(e
.statusCode
, 400);
88 it('does not find unhandled topic', async
function() {
89 ctx
.queryParams
= { topic: 'https://example.com/blog/' };
91 await manager
.getInfo(res
, ctx
);
92 assert
.fail(noExpectedException
);
94 assert
.strictEqual(e
.statusCode
, 404);
97 it('returns a count', async
function() {
98 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
100 topic: 'https://example.com/blog/',
102 await manager
.getInfo(res
, ctx
);
103 assert(res
.end
.called
);
105 it('returns a count as json', async
function() {
106 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
107 ctx
.responseType
= 'application/json';
109 topic: 'https://example.com/blog/',
111 await manager
.getInfo(res
, ctx
);
112 assert(res
.end
.called
);
114 it('returns a count as json as override format', async
function() {
115 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
116 ctx
.responseType
= 'text/html';
118 topic: 'https://example.com/blog/',
121 await manager
.getInfo(res
, ctx
);
122 assert(res
.end
.called
);
123 assert(res
.setHeader
.called
);
125 it('returns an svg badge as override format', async
function() {
126 manager
.db
.subscriptionCountByTopicUrl
.resolves({ count: 4 });
127 ctx
.responseType
= 'text/html';
129 topic: 'https://example.com/blog/',
132 await manager
.getInfo(res
, ctx
);
133 assert(res
.end
.called
);
134 assert(res
.setHeader
.called
);
138 describe('_historyBarCaption', function () {
139 it('covers today, none', function () {
140 const result
= Manager
._historyBarCaption(0, 0);
141 assert
.strictEqual(result
, 'today, no updates');
143 it('covers yesterday, singular', function () {
144 const result
= Manager
._historyBarCaption(1, 1);
145 assert
.strictEqual(result
, 'yesterday, 1 update');
147 it('covers older, plural', function () {
148 const result
= Manager
._historyBarCaption(7, 3);
149 assert
.strictEqual(result
, '7 days ago, 3 updates');
151 }); // _historyBarCaption
153 describe('getHistorySVG', function () {
154 beforeEach(function () {
155 manager
.db
.topicPublishHistory
.resolves([0, 1, 2, 1, 0, 1, 2, 0, 1]);
157 it('covers', async
function () {
158 await manager
.getHistorySVG(res
, ctx
);
159 assert(res
.end
.called
);
163 describe('getAdminOverview', function () {
164 beforeEach(function () {
165 manager
.db
.topicGetAll
.resolves([
167 id: '56c557ce-e667-11eb-bd80-0025905f714a',
169 url: 'https://example.com/',
170 leaseSecondsPreferred: 123,
172 leaseSecondsMax: 123456789,
173 publisherValidationUrl: null,
174 contentHashAlgorithm: 'hashy',
177 lastPublish: new Date(-Infinity
),
178 contentFetchNextAttempt: undefined,
179 contentFetchAttemptsSinceSuccess: 3,
180 contentUpdated: new Date(0),
187 it('covers', async
function () {
188 await manager
.getAdminOverview(res
, ctx
);
189 assert(res
.end
.called
);
191 it('covers non-matching profile', async
function () {
193 authenticatedProfile: 'https://different.example.com/profile',
195 await manager
.getAdminOverview(res
, ctx
);
196 assert
.deepStrictEqual(ctx
.topics
, []);
197 assert(res
.end
.called
);
199 }); // getAdminOverview
201 describe('getTopicDetails', function () {
202 beforeEach(function () {
203 ctx
.params
.topicId
= '56c557ce-e667-11eb-bd80-0025905f714a';
204 manager
.db
.topicGetById
.resolves({
205 id: '56c557ce-e667-11eb-bd80-0025905f714a',
207 url: 'https://example.com/topic',
208 leaseSecondsPreferred: 123,
210 leaseSecondsMax: 123456789,
211 publisherValidationUrl: null,
212 contentHashAlgorithm: 'hashy',
215 lastPublish: new Date(-Infinity
),
216 contentFetchNextAttempt: undefined,
217 contentFetchAttemptsSinceSuccess: 3,
218 contentUpdated: new Date(0),
223 manager
.db
.subscriptionsByTopicId
.resolves([{
226 topicId: '56c557ce-e667-11eb-bd80-0025905f714a',
228 verified: new Date(),
231 signatureAlgorithm: 'hmacy',
234 contentDelivered: new Date(),
235 deliveryAttemptsSinceSuccess: 0,
236 deliveryNextAttempt: new Date(-Infinity
),
238 manager
.db
.topicPublishHistory
.resolves([0, 1, 0, 1, 0]);
240 it('covers', async
function() {
241 await manager
.getTopicDetails(res
, ctx
);
242 assert(res
.end
.called
);
244 it('covers non-matching profile', async
function () {
246 authenticatedProfile: 'https://different.example.com/profile',
248 await manager
.getTopicDetails(res
, ctx
);
249 assert
.strictEqual(ctx
.topic
, null);
250 assert(res
.end
.called
);
252 it('covers matching profile', async
function () {
254 authenticatedProfile: 'https://example.com/',
256 await manager
.getTopicDetails(res
, ctx
);
258 assert(res
.end
.called
);
260 }); // getTopicDetails
262 describe('postRoot', function () {
263 let origProcessImmediately
;
264 beforeEach(function () {
265 origProcessImmediately
= manager
.options
.manager
.processImmediately
;
268 this.afterEach(function () {
269 manager
.options
.manager
.processImmediately
= origProcessImmediately
;
271 it('requires parameters', async
function () {
273 await manager
.postRoot(req
, res
, ctx
);
274 assert
.fail(noExpectedException
);
276 assert
.strictEqual(e
.message
, 'Bad Request');
279 it('accepts valid subscription', async
function () {
280 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
281 manager
.db
.topicGetByUrl
.resolves({
284 manager
.db
.verificationInsert
.resolves({
286 lastInsertRowid: undefined,
289 await manager
.postRoot(req
, res
, ctx
);
290 assert(manager
.db
.verificationInsert
.called
);
291 assert(res
.end
.called
);
293 it('accepts valid subscription without claiming work', async
function () {
294 manager
.options
.manager
.processImmediately
= false;
295 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
296 manager
.db
.topicGetByUrl
.resolves({
299 manager
.db
.verificationInsert
.resolves({
301 lastInsertRowid: undefined,
304 await manager
.postRoot(req
, res
, ctx
);
305 assert(manager
.db
.verificationInsert
.called
);
306 assert(!manager
.communication
.verificationProcess
.called
);
307 assert(res
.end
.called
);
309 it('accepts valid subscription, covers processVerification failure', async
function () {
310 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
311 manager
.communication
.verificationProcess
.rejects('failed');
312 manager
.db
.topicGetByUrl
.resolves({
315 manager
.db
.verificationInsert
.resolves({
317 lastInsertRowid: undefined,
320 await manager
.postRoot(req
, res
, ctx
);
321 assert(manager
.db
.verificationInsert
.called
);
322 assert(res
.end
.called
);
323 assert(manager
.logger
.error
.called
);
325 it('covers db.verificationInsert failure', async
function () {
326 const expectedException
= new Error('failure');
327 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
328 manager
.db
.topicGetByUrl
.resolves({
331 manager
.db
.verificationInsert
.rejects(expectedException
);
332 assert
.rejects(async () => {
333 await manager
.postRoot(req
, res
, ctx
);
334 }, expectedException
);
336 it('accepts valid unsubscription', async
function () {
337 ctx
= Object
.assign({}, testData
.validUnsubscribeCtx
);
338 manager
.db
.topicGetByUrl
.resolves({
341 manager
.db
.subscriptionGet
.resolves({
344 manager
.db
.verificationInsert
.resolves({
346 lastInsertRowid: undefined,
349 await manager
.postRoot(req
, res
, ctx
);
350 assert(res
.end
.called
);
352 it('accepts valid publish', async
function () {
353 ctx
= Object
.assign({}, testData
.validPublishCtx
);
354 manager
.db
.topicGetByUrl
.resolves({
357 manager
.db
.topicFetchRequested
.resolves({
359 lastInsertRowid: undefined,
362 await manager
.postRoot(req
, res
, ctx
);
363 assert(res
.end
.called
);
367 describe('_profileControlsTopic', function () {
368 let profileUrlObj
, topicUrlObj
;
369 it('allows exact match', function () {
370 profileUrlObj
= new URL('https://profile.example.com/');
371 topicUrlObj
= new URL('https://profile.example.com/');
372 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
373 assert
.strictEqual(result
, true);
375 it('allows descendent-path match', function () {
376 profileUrlObj
= new URL('https://profile.example.com/');
377 topicUrlObj
= new URL('https://profile.example.com/feed/atom');
378 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
379 assert
.strictEqual(result
, true);
381 it('disallows non-descendent-path', function () {
382 profileUrlObj
= new URL('https://profile.example.com/itsame');
383 topicUrlObj
= new URL('https://profile.example.com/');
384 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
385 assert
.strictEqual(result
, false);
387 it('disallows non-matched host', function () {
388 profileUrlObj
= new URL('https://profile.example.com/itsame');
389 topicUrlObj
= new URL('https://elsewhere.example.com/itsame/feed');
390 const result
= Manager
._profileControlsTopic(profileUrlObj
, topicUrlObj
);
391 assert
.strictEqual(result
, false);
393 }); // _profileControlsTopic
395 describe('_getRootData', function () {
396 it('extracts expected values', function () {
397 req
.getHeader
.returns('user@example.com');
398 ctx
= Object
.assign({}, testData
.validSubscribeCtx
);
399 const result
= Manager
._getRootData(req
, ctx
);
400 assert
.deepStrictEqual(result
, testData
.validRootData
);
404 describe('_validateRootData', function () {
405 // This only wraps the other _check functions, not bothering with coverage.
406 }); // _validateRootData
408 describe('_checkTopic', function () {
409 let dbCtx
, data
, warn
, err
;
412 leaseSecondsPreferred: 86400 * 10,
413 leaseSecondsMax: 86400 * 20,
414 leaseSecondsMin: 86400,
416 beforeEach(function () {
422 it('succeeds', async
function () {
424 topic: 'http://example.com/blog',
426 manager
.db
.topicGetByUrl
.resolves(topic
);
427 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
428 assert
.strictEqual(warn
.length
, 0, warn
);
429 assert
.strictEqual(err
.length
, 0, err
);
430 assert
.strictEqual(data
.topicId
, 111);
431 assert
.strictEqual(data
.leaseSeconds
, 864000);
433 it('errors on unknown topic', async
function () {
434 manager
.db
.topicGetByUrl
.resolves();
435 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
436 assert
.strictEqual(warn
.length
, 0, warn
);
437 assert
.strictEqual(err
.length
, 1, err
);
439 it('warns on lease under min range', async
function () {
441 topic: 'http://example.com/blog',
444 manager
.db
.topicGetByUrl
.resolves(topic
);
445 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
446 assert
.strictEqual(warn
.length
, 1, warn
);
447 assert
.strictEqual(err
.length
, 0, err
);
448 assert
.strictEqual(data
.topicId
, 111);
449 assert
.strictEqual(data
.leaseSeconds
, 86400);
451 it('warns on lease over max range', async
function () {
453 topic: 'http://example.com/blog',
454 leaseSeconds: 86400 * 100,
456 manager
.db
.topicGetByUrl
.resolves(topic
);
457 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
458 assert
.strictEqual(warn
.length
, 1, warn
);
459 assert
.strictEqual(err
.length
, 0, err
);
460 assert
.strictEqual(data
.topicId
, 111);
461 assert
.strictEqual(data
.leaseSeconds
, 86400 * 20);
463 it('sets publisher validation state when available', async
function () {
465 topic: 'http://example.com/blog',
467 manager
.db
.topicGetByUrl
.resolves(Object
.assign({}, topic
, {
468 publisherValidationUrl: 'http://example.com/validate',
470 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
471 assert
.strictEqual(warn
.length
, 0, warn
);
472 assert
.strictEqual(err
.length
, 0, err
);
473 assert
.strictEqual(data
.topicId
, 111);
474 assert
.strictEqual(data
.leaseSeconds
, 864000);
475 assert
.strictEqual(data
.isPublisherValidated
, false);
477 it('accepts new public subscribe topic', async
function () {
478 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves(topic
);
480 topic: 'http://example.com/blog',
482 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
483 assert
.strictEqual(warn
.length
, 0, 'unexpected warnings length');
484 assert
.strictEqual(err
.length
, 0, 'unexpected errors length');
485 assert
.strictEqual(data
.topicId
, 111, 'unexpected topic id');
487 it('does not accept new public subscribe for invalid topic', async
function () {
488 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves(topic
);
490 topic: 'not a topic',
492 await manager
._checkTopic(dbCtx
, data
, warn
, err
);
493 assert
.strictEqual(warn
.length
, 0, 'unexpected warnings length');
494 assert
.strictEqual(err
.length
, 1, 'unexpected errors length');
498 describe('_checkCallbackAndSecrets', function () {
500 let origStrictSecrets
;
502 origStrictSecrets
= manager
.options
.manager
.strictSecrets
;
504 beforeEach(function () {
509 afterEach(function () {
510 manager
.options
.manager
.strictSecrets
= origStrictSecrets
;
512 it('succeeds', function () {
514 callback: 'https://example.com/callback',
518 manager
._checkCallbackAndSecrets(data
, warn
, err
);
519 assert
.strictEqual(warn
.length
, 0, warn
);
520 assert
.strictEqual(err
.length
, 0, err
);
522 it('errors with invalid callback', function () {
524 callback: 'not a url',
528 manager
._checkCallbackAndSecrets(data
, warn
, err
);
529 assert
.strictEqual(warn
.length
, 0, warn
);
530 assert
.strictEqual(err
.length
, 1, err
);
532 it('errors when secret too large', function () {
534 callback: 'https://example.com/callback',
535 secret: 'x'.repeat(256),
538 manager
._checkCallbackAndSecrets(data
, warn
, err
);
539 assert
.strictEqual(warn
.length
, 0, warn
);
540 assert
.strictEqual(err
.length
, 1, err
);
542 it('warns when callback is insecure', function () {
544 callback: 'http://example.com/callback',
547 manager
._checkCallbackAndSecrets(data
, warn
, err
);
548 assert
.strictEqual(warn
.length
, 1, warn
);
549 assert
.strictEqual(err
.length
, 0, err
);
551 it('warns when hub is insecure with secret', function () {
553 callback: 'https://example.com/callback',
557 manager
._checkCallbackAndSecrets(data
, warn
, err
);
558 assert
.strictEqual(warn
.length
, 1, warn
);
559 assert
.strictEqual(err
.length
, 0, err
);
561 it('errors when callback is insecure with secret and strict', function () {
562 manager
.options
.manager
.strictSecrets
= true;
564 callback: 'http://example.com/callback',
568 manager
._checkCallbackAndSecrets(data
, warn
, err
);
569 assert
.strictEqual(warn
.length
, 1, warn
);
570 assert
.strictEqual(err
.length
, 1, err
);
572 }); // _checkCallbackAndSecrets
574 describe('_checkMode', function () {
575 let dbCtx
, data
, warn
, err
;
576 beforeEach(function () {
582 it('subscribe succeeds', async
function () {
586 await manager
._checkMode(dbCtx
, data
, warn
, err
);
587 assert
.strictEqual(warn
.length
, 0);
588 assert
.strictEqual(err
.length
, 0);
590 it('unsubscribe succeeds', async
function () {
593 callback: 'http://example.com',
596 manager
.db
.subscriptionGet
.resolves({
597 expires: (Date
.now() / 1000) + 60,
599 await manager
._checkMode(dbCtx
, data
, warn
, err
);
600 assert
.strictEqual(warn
.length
, 0, warn
);
601 assert
.strictEqual(err
.length
, 0, err
);
603 it('unsubscribe requires valid data', async
function () {
606 callback: 'http://example.com',
609 manager
.db
.subscriptionGet
.resolves({
610 expires: (Date
.now() / 1000) - 60,
612 await manager
._checkMode(dbCtx
, data
, warn
, err
);
613 assert
.strictEqual(warn
.length
, 0, warn
);
614 assert
.strictEqual(err
.length
, 1, err
);
616 it('unsubscribe ignores expired subscription', async
function () {
619 callback: 'http://example.com',
622 manager
.db
.subscriptionGet
.resolves({
623 expires: (Date
.now() / 1000) - 60,
625 await manager
._checkMode(dbCtx
, data
, warn
, err
);
626 assert
.strictEqual(warn
.length
, 0, warn
);
627 assert
.strictEqual(err
.length
, 1, err
);
631 describe('_publishTopics', function () {
632 let dbCtx
, data
, requestId
;
633 beforeEach(function () {
638 it('succeeds', async
function () {
639 manager
.db
.topicGetByUrl
.resolves({
642 Object
.assign(data
, testData
.validPublishRootData
);
643 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
644 assert
.strictEqual(topicResults
.length
, 1);
645 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
646 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
647 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
649 it('fails bad url', async
function () {
650 Object
.assign(data
, testData
.validPublishRootData
, { topic: 'not_a_url' });
651 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
652 assert
.strictEqual(topicResults
.length
, 1);
653 assert
.strictEqual(topicResults
[0].err
.length
, 1, 'unexpected errors length');
654 assert
.strictEqual(topicResults
[0].warn
.length
, 0);
656 it('accepts new public publish topic', async
function () {
657 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
660 Object
.assign(data
, testData
.validPublishRootData
);
661 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
662 assert
.strictEqual(topicResults
.length
, 1);
663 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
664 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
665 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
667 it('does not publish deleted topic', async
function () {
668 manager
.db
.topicGetByUrl
.resolves({
672 Object
.assign(data
, testData
.validPublishRootData
);
673 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
674 assert
.strictEqual(topicResults
.length
, 1);
675 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
676 assert
.strictEqual(topicResults
[0].err
.length
, 1, 'unexpected errors length');
677 assert
.strictEqual(topicResults
[0].topicId
, undefined, 'unexpected topic id');
679 it('no topics', async
function() {
680 Object
.assign(data
, testData
.validPublishRootData
);
682 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
683 assert
.strictEqual(topicResults
.length
, 0);
685 it('multiple valid topics', async
function () {
686 manager
.db
.topicGetByUrl
.resolves({
689 Object
.assign(data
, testData
.validPublishRootData
);
690 data
.url
= ['https://example.com/first', 'https://example.com/second'];
691 data
.topic
= ['https://example.com/third'];
692 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
693 assert
.strictEqual(topicResults
.length
, 3);
694 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
695 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
696 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
697 assert
.strictEqual(topicResults
[1].warn
.length
, 0, 'unexpected warnings length');
698 assert
.strictEqual(topicResults
[1].err
.length
, 0, 'unexpected errors length');
699 assert
.strictEqual(topicResults
[1].topicId
, 222, 'unexpected topic id');
700 assert
.strictEqual(topicResults
[2].warn
.length
, 0, 'unexpected warnings length');
701 assert
.strictEqual(topicResults
[2].err
.length
, 0, 'unexpected errors length');
702 assert
.strictEqual(topicResults
[2].topicId
, 222, 'unexpected topic id');
704 it('mix of valid and invalid topics', async
function () {
705 manager
.db
.topicGetByUrl
.onCall(1).resolves().resolves({
708 Object
.assign(data
, testData
.validPublishRootData
);
709 data
.url
= ['https://example.com/first', 'not a url'];
710 data
.topic
= ['https://example.com/third'];
711 const topicResults
= await manager
._publishTopics(dbCtx
, data
, requestId
);
712 assert
.strictEqual(topicResults
.length
, 3);
713 assert
.strictEqual(topicResults
[0].warn
.length
, 0, 'unexpected warnings length');
714 assert
.strictEqual(topicResults
[0].err
.length
, 0, 'unexpected errors length');
715 assert
.strictEqual(topicResults
[0].topicId
, 222, 'unexpected topic id');
716 assert
.strictEqual(topicResults
[1].warn
.length
, 0, 'unexpected warnings length');
717 assert
.strictEqual(topicResults
[1].err
.length
, 1, 'unexpected errors length');
718 assert
.strictEqual(topicResults
[1].topicId
, undefined, 'unexpected topic id');
719 assert
.strictEqual(topicResults
[2].warn
.length
, 0, 'unexpected warnings length');
720 assert
.strictEqual(topicResults
[2].err
.length
, 0, 'unexpected errors length');
721 assert
.strictEqual(topicResults
[2].topicId
, 222, 'unexpected topic id');
723 }); // _publishTopics
725 describe('_publishRequest', function () {
726 let dbCtx
, data
, res
, ctx
;
727 beforeEach(function () {
735 it('requires a topic', async
function () {
737 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
738 assert
.fail(noExpectedException
);
740 assert(e
instanceof Errors
.ResponseError
);
743 it('processes one topic', async
function() {
744 manager
.db
.topicGetByUrl
.resolves({
747 Object
.assign(data
, testData
.validPublishRootData
);
748 manager
.db
.topicFetchRequested
.resolves();
749 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
750 assert(manager
.db
.topicFetchRequested
.called
);
751 assert
.strictEqual(res
.statusCode
, 202);
752 assert(res
.end
.called
);
754 it('processes mix of valid and invalid topics', async
function () {
755 ctx
.responseType
= 'application/json';
756 manager
.db
.topicGetByUrl
.onCall(1).resolves().resolves({
759 Object
.assign(data
, testData
.validPublishRootData
);
760 data
.url
= ['https://example.com/first', 'not a url'];
761 data
.topic
= ['https://example.com/third'];
762 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
763 assert
.strictEqual(res
.statusCode
, 207);
764 assert(res
.end
.called
);
766 it('covers topicFetchRequest failure', async
function () {
767 manager
.db
.topicGetByUrl
.resolves({
770 Object
.assign(data
, testData
.validPublishRootData
);
771 const expected
= new Error('boo');
772 manager
.db
.topicFetchRequested
.rejects(expected
);
774 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
775 assert
.fail(noExpectedException
);
777 assert
.deepStrictEqual(e
, expected
);
780 it('covers immediate processing error', async
function() {
781 manager
.options
.manager
.processImmediately
= true;
782 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
785 manager
.communication
.topicFetchClaimAndProcessById
.rejects();
786 Object
.assign(data
, testData
.validPublishRootData
);
787 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
788 assert(manager
.db
.topicFetchRequested
.called
);
789 assert
.strictEqual(res
.statusCode
, 202);
790 assert(res
.end
.called
);
791 assert(manager
.communication
.topicFetchClaimAndProcessById
.called
);
793 it('covers no immediate processing', async
function() {
794 manager
.options
.manager
.processImmediately
= false;
795 manager
.db
.topicGetByUrl
.onCall(0).resolves().onCall(1).resolves({
798 Object
.assign(data
, testData
.validPublishRootData
);
799 await manager
._publishRequest(dbCtx
, data
, res
, ctx
);
800 assert(manager
.db
.topicFetchRequested
.called
);
801 assert
.strictEqual(res
.statusCode
, 202);
802 assert(res
.end
.called
);
803 assert(!manager
.communication
.topicFetchClaimAndProcessById
.called
);
805 }); // _publishRequest
807 describe('multiPublishContent', function () {
809 beforeEach(function () {
811 url: 'https://example.com/first',
816 statusMessage: 'Accepted',
821 err: [ 'invalid topic url (failed to parse url)' ],
824 statusMessage: 'Bad Request',
827 it('covers json response', function () {
828 ctx
.responseType
= 'application/json';
829 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":[]}]';
830 const result
= Manager
.multiPublishContent(ctx
, publishTopics
);
831 assert
.deepStrictEqual(result
, expected
);
833 it('covers text response', function () {
834 ctx
.responseType
= 'text/plain';
835 const expected
= `https://example.com/first [202 Accepted]
837 not a url [400 Bad Request]
838 \terror: invalid topic url (failed to parse url)`;
839 const result
= Manager
.multiPublishContent(ctx
, publishTopics
);
840 assert
.deepStrictEqual(result
, expected
);
842 }); // multiPublishContent
844 describe('processTasks', function () {
845 it('covers', async
function () {
846 sinon
.stub(manager
.communication
.worker
, 'process').resolves();
847 await manager
.processTasks(res
, ctx
);
848 assert(manager
.communication
.worker
.process
.called
);
849 assert(res
.end
.called
);
851 it('covers error', async
function () {
852 sinon
.stub(manager
.communication
.worker
, 'process').rejects();
853 await manager
.processTasks(res
, ctx
);
854 assert(manager
.communication
.worker
.process
.called
);
855 assert(res
.end
.called
);
859 describe('updateTopic', function () {
860 it('fails if no topic exists', async
function () {
862 await manager
.updateTopic(res
, ctx
);
863 assert
.fail(noExpectedException
);
865 assert(e
instanceof Errors
.ResponseError
);
868 it('deletes', async
function () {
869 ctx
.method
= 'DELETE';
870 manager
.db
.topicGetById
.resolves({});
871 await manager
.updateTopic(res
, ctx
);
872 assert(manager
.db
.topicDeleted
.called
);
874 it('does not patch without data', async
function () {
875 manager
.db
.topicGetById
.resolves({});
876 await manager
.updateTopic(res
, ctx
);
877 assert(!manager
.db
.topicUpdate
.called
);
878 assert
.strictEqual(res
.statusCode
, 204);
880 it('does not patch with same data', async
function () {
881 manager
.db
.topicGetById
.resolves({
882 leaseSecondsPreferred: '86400',
885 leaseSecondsPreferred: '86400',
887 await manager
.updateTopic(res
, ctx
);
888 assert(!manager
.db
.topicUpdate
.called
);
889 assert
.strictEqual(res
.statusCode
, 204);
891 it('patches', async
function () {
893 leaseSecondsPreferred: '86400',
895 manager
.db
.topicGetById
.resolves({});
896 await manager
.updateTopic(res
, ctx
);
897 assert(manager
.db
.topicUpdate
.called
);
899 it('handles validation error', async
function () {
901 leaseSecondsPreferred: 'blorp',
903 manager
.db
.topicGetById
.resolves({});
904 manager
.db
.topicUpdate
.rejects(new DBErrors
.DataValidation('something'));
906 await manager
.updateTopic(res
, ctx
);
907 assert
.fail(noExpectedException
);
909 assert(e
instanceof Errors
.ResponseError
);
910 assert
.strictEqual(e
.statusCode
, 400);
913 it('handles generic error', async
function () {
914 const expected
= new Error('blah');
916 leaseSecondsPreferred: '123',
918 manager
.db
.topicGetById
.resolves({});
919 manager
.db
.topicUpdate
.rejects(expected
);
921 await manager
.updateTopic(res
, ctx
);
922 assert
.fail(noExpectedException
);
924 assert
.deepStrictEqual(e
, expected
);
929 describe('updateSubscription', function () {
930 it('fails if no subscription exists', async
function () {
932 await manager
.updateSubscription(res
, ctx
);
933 assert
.fail(noExpectedException
);
935 assert(e
instanceof Errors
.ResponseError
);
938 it('deletes', async
function () {
939 ctx
.method
= 'DELETE';
940 manager
.db
.subscriptionGetById
.resolves({});
941 await manager
.updateSubscription(res
, ctx
);
942 assert(manager
.db
.verificationInsert
.called
);
944 it('does not patch without data', async
function () {
945 manager
.db
.subscriptionGetById
.resolves({});
946 await manager
.updateSubscription(res
, ctx
);
947 assert(!manager
.db
.subscriptionUpdate
.called
);
948 assert
.strictEqual(res
.statusCode
, 204);
950 it('does not patch with same data', async
function () {
951 manager
.db
.subscriptionGetById
.resolves({
952 signatureAlgorithm: 'sha256',
955 signatureAlgorithm: 'sha256',
957 await manager
.updateSubscription(res
, ctx
);
958 assert(!manager
.db
.subscriptionUpdate
.called
);
959 assert
.strictEqual(res
.statusCode
, 204);
961 it('patches', async
function () {
963 signatureAlgorithm: 'sha256',
965 manager
.db
.subscriptionGetById
.resolves({});
966 await manager
.updateSubscription(res
, ctx
);
967 assert(manager
.db
.subscriptionUpdate
.called
);
969 it('handles validation error', async
function () {
971 signatureAlgorithm: 123,
973 manager
.db
.subscriptionGetById
.resolves({});
974 manager
.db
.subscriptionUpdate
.rejects(new DBErrors
.DataValidation('something'));
976 await manager
.updateSubscription(res
, ctx
);
977 assert
.fail(noExpectedException
);
979 assert(e
instanceof Errors
.ResponseError
);
980 assert
.strictEqual(e
.statusCode
, 400);
983 it('handles generic error', async
function () {
984 const expected
= new Error('blah');
986 signatureAlgorithm: 'blorp',
988 manager
.db
.subscriptionGetById
.resolves({});
989 manager
.db
.subscriptionUpdate
.rejects(expected
);
991 await manager
.updateSubscription(res
, ctx
);
992 assert
.fail(noExpectedException
);
994 assert
.deepStrictEqual(e
, expected
);
997 }); // updateSubscription