update dependencies and devDependencies, fix lint issues
[websub-hub] / test / src / manager.js
1 'use strict';
2
3 const assert = require('node:assert');
4 const sinon = require('sinon');
5
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');
11
12 const stubDb = require('../stub-db');
13 const stubLogger = require('../stub-logger');
14 const testData = require('../test-data/manager');
15
16 const noExpectedException = 'did not get expected exception';
17
18 describe('Manager', function () {
19 let manager, options;
20 let req, res, ctx;
21
22 beforeEach(function () {
23 options = new Config('test');
24 req = {
25 getHeader : sinon.stub(),
26 };
27 res = {
28 end: sinon.stub(),
29 setHeader: sinon.stub(),
30 };
31 ctx = {
32 params: {},
33 queryParams: {},
34 };
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');
39 stubDb._reset();
40 stubLogger._reset();
41 });
42 afterEach(function () {
43 sinon.restore();
44 });
45
46 it('instantiates', function () {
47 assert(manager);
48 });
49
50 describe('getRoot', function () {
51 beforeEach(function () {
52 sinon.stub(common, 'isClientCached');
53 req = {};
54 });
55 it('normal response', async function () {
56 common.isClientCached.returns(false);
57 await manager.getRoot(req, res, ctx);
58 assert(res.end.called);
59 });
60 }); // getRoot
61
62 describe('getHealthcheck', function () {
63 it('normal response', async function () {
64 await manager.getHealthcheck(res, ctx);
65 assert(res.end.called);
66 });
67 }); // getById
68
69 describe('getInfo', function () {
70 it('requires query param', async function() {
71 ctx.queryParams = {};
72 try {
73 await manager.getInfo(res, ctx);
74 assert.fail(noExpectedException);
75 } catch (e) {
76 assert.strictEqual(e.statusCode, 400);
77 }
78 });
79 it('requires parsable query param', async function() {
80 ctx.queryParams = { topic: 'not a url' };
81 try {
82 await manager.getInfo(res, ctx);
83 assert.fail(noExpectedException);
84 } catch (e) {
85 assert.strictEqual(e.statusCode, 400);
86 }
87 });
88 it('does not find unhandled topic', async function() {
89 ctx.queryParams = { topic: 'https://example.com/blog/' };
90 try {
91 await manager.getInfo(res, ctx);
92 assert.fail(noExpectedException);
93 } catch (e) {
94 assert.strictEqual(e.statusCode, 404);
95 }
96 });
97 it('returns a count', async function() {
98 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
99 ctx.queryParams = {
100 topic: 'https://example.com/blog/',
101 };
102 await manager.getInfo(res, ctx);
103 assert(res.end.called);
104 });
105 it('returns a count as json', async function() {
106 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
107 ctx.responseType = 'application/json';
108 ctx.queryParams = {
109 topic: 'https://example.com/blog/',
110 };
111 await manager.getInfo(res, ctx);
112 assert(res.end.called);
113 });
114 it('returns a count as json as override format', async function() {
115 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
116 ctx.responseType = 'text/html';
117 ctx.queryParams = {
118 topic: 'https://example.com/blog/',
119 format: 'json',
120 };
121 await manager.getInfo(res, ctx);
122 assert(res.end.called);
123 assert(res.setHeader.called);
124 });
125 it('returns an svg badge as override format', async function() {
126 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
127 ctx.responseType = 'text/html';
128 ctx.queryParams = {
129 topic: 'https://example.com/blog/',
130 format: 'svg',
131 };
132 await manager.getInfo(res, ctx);
133 assert(res.end.called);
134 assert(res.setHeader.called);
135 });
136 }); // getInfo
137
138 describe('_historyBarCaption', function () {
139 it('covers today, none', function () {
140 const result = Manager._historyBarCaption(0, 0);
141 assert.strictEqual(result, 'today, no updates');
142 });
143 it('covers yesterday, singular', function () {
144 const result = Manager._historyBarCaption(1, 1);
145 assert.strictEqual(result, 'yesterday, 1 update');
146 });
147 it('covers older, plural', function () {
148 const result = Manager._historyBarCaption(7, 3);
149 assert.strictEqual(result, '7 days ago, 3 updates');
150 });
151 }); // _historyBarCaption
152
153 describe('getHistorySVG', function () {
154 beforeEach(function () {
155 manager.db.topicPublishHistory.resolves([0, 1, 2, 1, 0, 1, 2, 0, 1]);
156 });
157 it('covers', async function () {
158 await manager.getHistorySVG(res, ctx);
159 assert(res.end.called);
160 });
161 }); // getHistorySVG
162
163 describe('getAdminOverview', function () {
164 beforeEach(function () {
165 manager.db.topicGetAll.resolves([
166 {
167 id: '56c557ce-e667-11eb-bd80-0025905f714a',
168 created: new Date(),
169 url: 'https://example.com/',
170 leaseSecondsPreferred: 123,
171 leaseSecondsMin: 12,
172 leaseSecondsMax: 123456789,
173 publisherValidationUrl: null,
174 contentHashAlgorithm: 'hashy',
175 isActive: true,
176 isDeleted: false,
177 lastPublish: new Date(-Infinity),
178 contentFetchNextAttempt: undefined,
179 contentFetchAttemptsSinceSuccess: 3,
180 contentUpdated: new Date(0),
181 contentHash: 'abc',
182 contentType: 'foo',
183 subscribers: 12,
184 },
185 ]);
186 });
187 it('covers', async function () {
188 await manager.getAdminOverview(res, ctx);
189 assert(res.end.called);
190 });
191 it('covers non-matching profile', async function () {
192 ctx.session = {
193 authenticatedProfile: 'https://different.example.com/profile',
194 };
195 await manager.getAdminOverview(res, ctx);
196 assert.deepStrictEqual(ctx.topics, []);
197 assert(res.end.called);
198 });
199 }); // getAdminOverview
200
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',
206 created: new Date(),
207 url: 'https://example.com/topic',
208 leaseSecondsPreferred: 123,
209 leaseSecondsMin: 12,
210 leaseSecondsMax: 123456789,
211 publisherValidationUrl: null,
212 contentHashAlgorithm: 'hashy',
213 isActive: true,
214 isDeleted: false,
215 lastPublish: new Date(-Infinity),
216 contentFetchNextAttempt: undefined,
217 contentFetchAttemptsSinceSuccess: 3,
218 contentUpdated: new Date(0),
219 contentHash: 'abc',
220 contentType: 'foo',
221 subscribers: 12,
222 });
223 manager.db.subscriptionsByTopicId.resolves([{
224 id: '',
225 created: new Date(),
226 topicId: '56c557ce-e667-11eb-bd80-0025905f714a',
227 callback: '',
228 verified: new Date(),
229 expires: new Date(),
230 secret: '',
231 signatureAlgorithm: 'hmacy',
232 httpRemoteAddr: '',
233 httpFrom: '',
234 contentDelivered: new Date(),
235 deliveryAttemptsSinceSuccess: 0,
236 deliveryNextAttempt: new Date(-Infinity),
237 }]);
238 manager.db.topicPublishHistory.resolves([0, 1, 0, 1, 0]);
239 });
240 it('covers', async function() {
241 await manager.getTopicDetails(res, ctx);
242 assert(res.end.called);
243 });
244 it('covers non-matching profile', async function () {
245 ctx.session = {
246 authenticatedProfile: 'https://different.example.com/profile',
247 };
248 await manager.getTopicDetails(res, ctx);
249 assert.strictEqual(ctx.topic, null);
250 assert(res.end.called);
251 });
252 it('covers matching profile', async function () {
253 ctx.session = {
254 authenticatedProfile: 'https://example.com/',
255 };
256 await manager.getTopicDetails(res, ctx);
257 assert(ctx.topic);
258 assert(res.end.called);
259 });
260 }); // getTopicDetails
261
262 describe('postRoot', function () {
263 let origProcessImmediately;
264 beforeEach(function () {
265 origProcessImmediately = manager.options.manager.processImmediately;
266 ctx.parsedBody = {};
267 });
268 this.afterEach(function () {
269 manager.options.manager.processImmediately = origProcessImmediately;
270 });
271 it('requires parameters', async function () {
272 try {
273 await manager.postRoot(req, res, ctx);
274 assert.fail(noExpectedException);
275 } catch (e) {
276 assert.strictEqual(e.message, 'Bad Request');
277 }
278 });
279 it('accepts valid subscription', async function () {
280 ctx = Object.assign({}, testData.validSubscribeCtx);
281 manager.db.topicGetByUrl.resolves({
282 id: 111,
283 });
284 manager.db.verificationInsert.resolves({
285 changes: 1,
286 lastInsertRowid: undefined,
287 duration: 12,
288 });
289 await manager.postRoot(req, res, ctx);
290 assert(manager.db.verificationInsert.called);
291 assert(res.end.called);
292 });
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({
297 id: 111,
298 });
299 manager.db.verificationInsert.resolves({
300 changes: 1,
301 lastInsertRowid: undefined,
302 duration: 12,
303 });
304 await manager.postRoot(req, res, ctx);
305 assert(manager.db.verificationInsert.called);
306 assert(!manager.communication.verificationProcess.called);
307 assert(res.end.called);
308 });
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({
313 id: 111,
314 });
315 manager.db.verificationInsert.resolves({
316 changes: 1,
317 lastInsertRowid: undefined,
318 duration: 12,
319 });
320 await manager.postRoot(req, res, ctx);
321 assert(manager.db.verificationInsert.called);
322 assert(res.end.called);
323 assert(manager.logger.error.called);
324 });
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({
329 id: 111,
330 });
331 manager.db.verificationInsert.rejects(expectedException);
332 assert.rejects(async () => {
333 await manager.postRoot(req, res, ctx);
334 }, expectedException);
335 });
336 it('accepts valid unsubscription', async function () {
337 ctx = Object.assign({}, testData.validUnsubscribeCtx);
338 manager.db.topicGetByUrl.resolves({
339 id: 111,
340 });
341 manager.db.subscriptionGet.resolves({
342 id: 22,
343 });
344 manager.db.verificationInsert.resolves({
345 changes: 1,
346 lastInsertRowid: undefined,
347 duration: 12,
348 });
349 await manager.postRoot(req, res, ctx);
350 assert(res.end.called);
351 });
352 it('accepts valid publish', async function () {
353 ctx = Object.assign({}, testData.validPublishCtx);
354 manager.db.topicGetByUrl.resolves({
355 id: 111,
356 });
357 manager.db.topicFetchRequested.resolves({
358 changes: 1,
359 lastInsertRowid: undefined,
360 duration: 12,
361 });
362 await manager.postRoot(req, res, ctx);
363 assert(res.end.called);
364 });
365 }); // postRoot
366
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);
374 });
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);
380 });
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);
386 });
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);
392 });
393 }); // _profileControlsTopic
394
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);
401 });
402 }); // _getRootData
403
404 describe('_validateRootData', function () {
405 // This only wraps the other _check functions, not bothering with coverage.
406 }); // _validateRootData
407
408 describe('_checkTopic', function () {
409 let dbCtx, data, warn, err;
410 const topic = {
411 id: 111,
412 leaseSecondsPreferred: 86400 * 10,
413 leaseSecondsMax: 86400 * 20,
414 leaseSecondsMin: 86400,
415 };
416 beforeEach(function () {
417 dbCtx = {};
418 data = {};
419 warn = [];
420 err = [];
421 });
422 it('succeeds', async function () {
423 data = {
424 topic: 'http://example.com/blog',
425 };
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);
432 });
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);
438 });
439 it('warns on lease under min range', async function () {
440 data = {
441 topic: 'http://example.com/blog',
442 leaseSeconds: 97,
443 };
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);
450 });
451 it('warns on lease over max range', async function () {
452 data = {
453 topic: 'http://example.com/blog',
454 leaseSeconds: 86400 * 100,
455 };
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);
462 });
463 it('sets publisher validation state when available', async function () {
464 data = {
465 topic: 'http://example.com/blog',
466 };
467 manager.db.topicGetByUrl.resolves(Object.assign({}, topic, {
468 publisherValidationUrl: 'http://example.com/validate',
469 }));
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);
476 });
477 it('accepts new public subscribe topic', async function () {
478 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
479 data = {
480 topic: 'http://example.com/blog',
481 };
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');
486 });
487 it('does not accept new public subscribe for invalid topic', async function () {
488 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
489 data = {
490 topic: 'not a topic',
491 };
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');
495 });
496 }); // _checkTopic
497
498 describe('_checkCallbackAndSecrets', function () {
499 let data, warn, err;
500 let origStrictSecrets;
501 before(function () {
502 origStrictSecrets = manager.options.manager.strictSecrets;
503 });
504 beforeEach(function () {
505 data = {};
506 warn = [];
507 err = [];
508 });
509 afterEach(function () {
510 manager.options.manager.strictSecrets = origStrictSecrets;
511 });
512 it('succeeds', function () {
513 data = {
514 callback: 'https://example.com/callback',
515 secret: 'so safe',
516 isSecure: true,
517 };
518 manager._checkCallbackAndSecrets(data, warn, err);
519 assert.strictEqual(warn.length, 0, warn);
520 assert.strictEqual(err.length, 0, err);
521 });
522 it('errors with invalid callback', function () {
523 data = {
524 callback: 'not a url',
525 secret: 'so safe',
526 isSecure: true,
527 };
528 manager._checkCallbackAndSecrets(data, warn, err);
529 assert.strictEqual(warn.length, 0, warn);
530 assert.strictEqual(err.length, 1, err);
531 });
532 it('errors when secret too large', function () {
533 data = {
534 callback: 'https://example.com/callback',
535 secret: 'x'.repeat(256),
536 isSecure: true,
537 };
538 manager._checkCallbackAndSecrets(data, warn, err);
539 assert.strictEqual(warn.length, 0, warn);
540 assert.strictEqual(err.length, 1, err);
541 });
542 it('warns when callback is insecure', function () {
543 data = {
544 callback: 'http://example.com/callback',
545 isSecure: true,
546 };
547 manager._checkCallbackAndSecrets(data, warn, err);
548 assert.strictEqual(warn.length, 1, warn);
549 assert.strictEqual(err.length, 0, err);
550 });
551 it('warns when hub is insecure with secret', function () {
552 data = {
553 callback: 'https://example.com/callback',
554 secret: 'so safe',
555 isSecure: false,
556 };
557 manager._checkCallbackAndSecrets(data, warn, err);
558 assert.strictEqual(warn.length, 1, warn);
559 assert.strictEqual(err.length, 0, err);
560 });
561 it('errors when callback is insecure with secret and strict', function () {
562 manager.options.manager.strictSecrets = true;
563 data = {
564 callback: 'http://example.com/callback',
565 secret: 'so safe',
566 isSecure: true,
567 };
568 manager._checkCallbackAndSecrets(data, warn, err);
569 assert.strictEqual(warn.length, 1, warn);
570 assert.strictEqual(err.length, 1, err);
571 });
572 }); // _checkCallbackAndSecrets
573
574 describe('_checkMode', function () {
575 let dbCtx, data, warn, err;
576 beforeEach(function () {
577 dbCtx = {};
578 data = {};
579 warn = [];
580 err = [];
581 });
582 it('subscribe succeeds', async function () {
583 data = {
584 mode: 'subscribe',
585 };
586 await manager._checkMode(dbCtx, data, warn, err);
587 assert.strictEqual(warn.length, 0);
588 assert.strictEqual(err.length, 0);
589 });
590 it('unsubscribe succeeds', async function () {
591 data = {
592 mode: 'unsubscribe',
593 callback: 'http://example.com',
594 topicId: 123,
595 };
596 manager.db.subscriptionGet.resolves({
597 expires: (Date.now() / 1000) + 60,
598 });
599 await manager._checkMode(dbCtx, data, warn, err);
600 assert.strictEqual(warn.length, 0, warn);
601 assert.strictEqual(err.length, 0, err);
602 });
603 it('unsubscribe requires valid data', async function () {
604 data = {
605 mode: 'unsubscribe',
606 callback: 'http://example.com',
607 topicId: undefined,
608 };
609 manager.db.subscriptionGet.resolves({
610 expires: (Date.now() / 1000) - 60,
611 });
612 await manager._checkMode(dbCtx, data, warn, err);
613 assert.strictEqual(warn.length, 0, warn);
614 assert.strictEqual(err.length, 1, err);
615 });
616 it('unsubscribe ignores expired subscription', async function () {
617 data = {
618 mode: 'unsubscribe',
619 callback: 'http://example.com',
620 topicId: 123,
621 };
622 manager.db.subscriptionGet.resolves({
623 expires: (Date.now() / 1000) - 60,
624 });
625 await manager._checkMode(dbCtx, data, warn, err);
626 assert.strictEqual(warn.length, 0, warn);
627 assert.strictEqual(err.length, 1, err);
628 });
629 }); // _checkMode
630
631 describe('_publishTopics', function () {
632 let dbCtx, data, requestId;
633 beforeEach(function () {
634 dbCtx = {};
635 data = {};
636 requestId = 'blah';
637 });
638 it('succeeds', async function () {
639 manager.db.topicGetByUrl.resolves({
640 id: 222,
641 });
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');
648 });
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);
655 });
656 it('accepts new public publish topic', async function () {
657 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
658 id: 222,
659 });
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');
666 });
667 it('does not publish deleted topic', async function () {
668 manager.db.topicGetByUrl.resolves({
669 id: 222,
670 isDeleted: true,
671 });
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');
678 });
679 it('no topics', async function() {
680 Object.assign(data, testData.validPublishRootData);
681 delete data.topic;
682 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
683 assert.strictEqual(topicResults.length, 0);
684 });
685 it('multiple valid topics', async function () {
686 manager.db.topicGetByUrl.resolves({
687 id: 222,
688 });
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');
703 });
704 it('mix of valid and invalid topics', async function () {
705 manager.db.topicGetByUrl.onCall(1).resolves().resolves({
706 id: 222,
707 });
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');
722 });
723 }); // _publishTopics
724
725 describe('_publishRequest', function () {
726 let dbCtx, data, res, ctx;
727 beforeEach(function () {
728 dbCtx = {};
729 data = {};
730 res = {
731 end: sinon.stub(),
732 };
733 ctx = {};
734 });
735 it('requires a topic', async function () {
736 try {
737 await manager._publishRequest(dbCtx, data, res, ctx);
738 assert.fail(noExpectedException);
739 } catch (e) {
740 assert(e instanceof Errors.ResponseError);
741 }
742 });
743 it('processes one topic', async function() {
744 manager.db.topicGetByUrl.resolves({
745 id: 222,
746 });
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);
753 });
754 it('processes mix of valid and invalid topics', async function () {
755 ctx.responseType = 'application/json';
756 manager.db.topicGetByUrl.onCall(1).resolves().resolves({
757 id: 222,
758 });
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);
765 });
766 it('covers topicFetchRequest failure', async function () {
767 manager.db.topicGetByUrl.resolves({
768 id: 222,
769 });
770 Object.assign(data, testData.validPublishRootData);
771 const expected = new Error('boo');
772 manager.db.topicFetchRequested.rejects(expected);
773 try {
774 await manager._publishRequest(dbCtx, data, res, ctx);
775 assert.fail(noExpectedException);
776 } catch (e) {
777 assert.deepStrictEqual(e, expected);
778 }
779 });
780 it('covers immediate processing error', async function() {
781 manager.options.manager.processImmediately = true;
782 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
783 id: 222,
784 });
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);
792 });
793 it('covers no immediate processing', async function() {
794 manager.options.manager.processImmediately = false;
795 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
796 id: 222,
797 });
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);
804 });
805 }); // _publishRequest
806
807 describe('multiPublishContent', function () {
808 let publishTopics;
809 beforeEach(function () {
810 publishTopics = [{
811 url: 'https://example.com/first',
812 warn: [],
813 err: [],
814 topicId: 222,
815 status: 202,
816 statusMessage: 'Accepted',
817 },
818 {
819 url: 'not a url',
820 warn: [],
821 err: [ 'invalid topic url (failed to parse url)' ],
822 topicId: undefined,
823 status: 400,
824 statusMessage: 'Bad Request',
825 }];
826 });
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);
832 });
833 it('covers text response', function () {
834 ctx.responseType = 'text/plain';
835 const expected = `https://example.com/first [202 Accepted]
836 ----
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);
841 });
842 }); // multiPublishContent
843
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);
850 });
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);
856 });
857 }); // processTasks
858
859 describe('updateTopic', function () {
860 it('fails if no topic exists', async function () {
861 try {
862 await manager.updateTopic(res, ctx);
863 assert.fail(noExpectedException);
864 } catch (e) {
865 assert(e instanceof Errors.ResponseError);
866 }
867 });
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);
873 });
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);
879 });
880 it('does not patch with same data', async function () {
881 manager.db.topicGetById.resolves({
882 leaseSecondsPreferred: '86400',
883 });
884 ctx.parsedBody = {
885 leaseSecondsPreferred: '86400',
886 };
887 await manager.updateTopic(res, ctx);
888 assert(!manager.db.topicUpdate.called);
889 assert.strictEqual(res.statusCode, 204);
890 });
891 it('patches', async function () {
892 ctx.queryParams = {
893 leaseSecondsPreferred: '86400',
894 };
895 manager.db.topicGetById.resolves({});
896 await manager.updateTopic(res, ctx);
897 assert(manager.db.topicUpdate.called);
898 });
899 it('handles validation error', async function () {
900 ctx.queryParams = {
901 leaseSecondsPreferred: 'blorp',
902 };
903 manager.db.topicGetById.resolves({});
904 manager.db.topicUpdate.rejects(new DBErrors.DataValidation('something'));
905 try {
906 await manager.updateTopic(res, ctx);
907 assert.fail(noExpectedException);
908 } catch (e) {
909 assert(e instanceof Errors.ResponseError);
910 assert.strictEqual(e.statusCode, 400);
911 }
912 });
913 it('handles generic error', async function () {
914 const expected = new Error('blah');
915 ctx.queryParams = {
916 leaseSecondsPreferred: '123',
917 };
918 manager.db.topicGetById.resolves({});
919 manager.db.topicUpdate.rejects(expected);
920 try {
921 await manager.updateTopic(res, ctx);
922 assert.fail(noExpectedException);
923 } catch (e) {
924 assert.deepStrictEqual(e, expected);
925 }
926 });
927 }); // updateTopic
928
929 describe('updateSubscription', function () {
930 it('fails if no subscription exists', async function () {
931 try {
932 await manager.updateSubscription(res, ctx);
933 assert.fail(noExpectedException);
934 } catch (e) {
935 assert(e instanceof Errors.ResponseError);
936 }
937 });
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);
943 });
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);
949 });
950 it('does not patch with same data', async function () {
951 manager.db.subscriptionGetById.resolves({
952 signatureAlgorithm: 'sha256',
953 });
954 ctx.parsedBody = {
955 signatureAlgorithm: 'sha256',
956 };
957 await manager.updateSubscription(res, ctx);
958 assert(!manager.db.subscriptionUpdate.called);
959 assert.strictEqual(res.statusCode, 204);
960 });
961 it('patches', async function () {
962 ctx.queryParams = {
963 signatureAlgorithm: 'sha256',
964 };
965 manager.db.subscriptionGetById.resolves({});
966 await manager.updateSubscription(res, ctx);
967 assert(manager.db.subscriptionUpdate.called);
968 });
969 it('handles validation error', async function () {
970 ctx.queryParams = {
971 signatureAlgorithm: 123,
972 };
973 manager.db.subscriptionGetById.resolves({});
974 manager.db.subscriptionUpdate.rejects(new DBErrors.DataValidation('something'));
975 try {
976 await manager.updateSubscription(res, ctx);
977 assert.fail(noExpectedException);
978 } catch (e) {
979 assert(e instanceof Errors.ResponseError);
980 assert.strictEqual(e.statusCode, 400);
981 }
982 });
983 it('handles generic error', async function () {
984 const expected = new Error('blah');
985 ctx.queryParams = {
986 signatureAlgorithm: 'blorp',
987 };
988 manager.db.subscriptionGetById.resolves({});
989 manager.db.subscriptionUpdate.rejects(expected);
990 try {
991 await manager.updateSubscription(res, ctx);
992 assert.fail(noExpectedException);
993 } catch (e) {
994 assert.deepStrictEqual(e, expected);
995 }
996 });
997 }); // updateSubscription
998
999 }); // Manager