update CHANGELOG.md
[websub-hub] / test / src / manager.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
9 const 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');
14
15 const stubDb = require('../stub-db');
16 const stubLogger = require('../stub-logger');
17 const testData = require('../test-data/manager');
18
19 const noExpectedException = 'did not get expected exception';
20
21 describe('Manager', function () {
22 let manager, options;
23 let req, res, ctx;
24
25 beforeEach(function () {
26 options = new Config('test');
27 req = {
28 getHeader : sinon.stub(),
29 };
30 res = {
31 end: sinon.stub(),
32 setHeader: sinon.stub(),
33 };
34 ctx = {
35 params: {},
36 };
37 manager = new Manager(stubLogger, stubDb, options);
38 sinon.stub(manager.communication, 'verificationProcess');
39 sinon.stub(manager.communication, 'topicFetchProcess');
40 sinon.stub(manager.communication, 'topicFetchClaimAndProcessById');
41 stubDb._reset();
42 stubLogger._reset();
43 });
44 afterEach(function () {
45 sinon.restore();
46 });
47
48 it('instantiates', function () {
49 assert(manager);
50 });
51
52 describe('getRoot', function () {
53 beforeEach(function () {
54 sinon.stub(common, 'isClientCached');
55 req = {};
56 });
57 it('normal response', async function () {
58 common.isClientCached.returns(false);
59 await manager.getRoot(req, res, ctx);
60 assert(res.end.called);
61 });
62 it('repeat response', async function () {
63 manager.startTime = (new Date()).toGMTString();
64 common.isClientCached.returns(true);
65 await manager.getRoot(req, res, ctx);
66 assert(res.end.called);
67 });
68 it('cached response', async function () {
69 common.isClientCached.returns(true);
70 await manager.getRoot(req, res, ctx);
71 assert(res.end.called);
72 assert.strictEqual(res.statusCode, 304);
73 });
74 }); // getRoot
75
76 describe('getHealthcheck', function () {
77 it('normal response', async function () {
78 await manager.getHealthcheck(res, ctx);
79 assert(res.end.called);
80 });
81 }); // getById
82
83 describe('getInfo', function () {
84 it('requires query param', async function() {
85 ctx.queryParams = {};
86 try {
87 await manager.getInfo(res, ctx);
88 assert.fail(noExpectedException);
89 } catch (e) {
90 assert.strictEqual(e.statusCode, 400);
91 }
92 });
93 it('requires parsable query param', async function() {
94 ctx.queryParams = { topic: 'not a url' };
95 try {
96 await manager.getInfo(res, ctx);
97 assert.fail(noExpectedException);
98 } catch (e) {
99 assert.strictEqual(e.statusCode, 400);
100 }
101 });
102 it('does not find unhandled topic', async function() {
103 ctx.queryParams = { topic: 'https://example.com/blog/' };
104 try {
105 await manager.getInfo(res, ctx);
106 assert.fail(noExpectedException);
107 } catch (e) {
108 assert.strictEqual(e.statusCode, 404);
109 }
110 });
111 it('returns a count', async function() {
112 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
113 ctx.queryParams = {
114 topic: 'https://example.com/blog/',
115 };
116 await manager.getInfo(res, ctx);
117 assert(res.end.called);
118 });
119 it('returns a count as json', async function() {
120 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
121 ctx.responseType = 'application/json';
122 ctx.queryParams = {
123 topic: 'https://example.com/blog/',
124 };
125 await manager.getInfo(res, ctx);
126 assert(res.end.called);
127 });
128 it('returns a count as json as override format', async function() {
129 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
130 ctx.responseType = 'text/html';
131 ctx.queryParams = {
132 topic: 'https://example.com/blog/',
133 format: 'json',
134 };
135 await manager.getInfo(res, ctx);
136 assert(res.end.called);
137 assert(res.setHeader.called);
138 });
139 it('returns an svg badge as override format', async function() {
140 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
141 ctx.responseType = 'text/html';
142 ctx.queryParams = {
143 topic: 'https://example.com/blog/',
144 format: 'svg',
145 };
146 await manager.getInfo(res, ctx);
147 assert(res.end.called);
148 assert(res.setHeader.called);
149 });
150 }); // getInfo
151
152 describe('getAdminOverview', function () {
153 beforeEach(function () {
154 manager.db.topicGetAll.resolves([
155 {
156 id: '56c557ce-e667-11eb-bd80-0025905f714a',
157 created: new Date(),
158 url: 'https://example.com/',
159 leaseSecondsPreferred: 123,
160 leaseSecondsMin: 12,
161 leaseSecondsMax: 123456789,
162 publisherValidationUrl: null,
163 contentHashAlgorithm: 'hashy',
164 isActive: true,
165 isDeleted: false,
166 lastPublish: new Date(-Infinity),
167 contentFetchNextAttempt: undefined,
168 contentFetchAttemptsSinceSuccess: 3,
169 contentUpdated: new Date(0),
170 contentHash: 'abc',
171 contentType: 'foo',
172 subscribers: 12,
173 },
174 ]);
175 });
176 it('covers', async function () {
177 await manager.getAdminOverview(res, ctx);
178 assert(res.end.called);
179 });
180 it('covers non-matching profile', async function () {
181 ctx.session = {
182 authenticatedProfile: 'https://different.example.com/profile',
183 };
184 await manager.getAdminOverview(res, ctx);
185 assert.deepStrictEqual(ctx.topics, []);
186 assert(res.end.called);
187 });
188 }); // getAdminOverview
189
190 describe('getTopicDetails', function () {
191 beforeEach(function () {
192 ctx.params.topicId = '56c557ce-e667-11eb-bd80-0025905f714a';
193 manager.db.topicGetById.resolves({
194 id: '56c557ce-e667-11eb-bd80-0025905f714a',
195 created: new Date(),
196 url: 'https://example.com/',
197 leaseSecondsPreferred: 123,
198 leaseSecondsMin: 12,
199 leaseSecondsMax: 123456789,
200 publisherValidationUrl: null,
201 contentHashAlgorithm: 'hashy',
202 isActive: true,
203 isDeleted: false,
204 lastPublish: new Date(-Infinity),
205 contentFetchNextAttempt: undefined,
206 contentFetchAttemptsSinceSuccess: 3,
207 contentUpdated: new Date(0),
208 contentHash: 'abc',
209 contentType: 'foo',
210 subscribers: 12,
211 });
212 manager.db.subscriptionsByTopicId.resolves([{
213 id: '',
214 created: new Date(),
215 topicId: '56c557ce-e667-11eb-bd80-0025905f714a',
216 callback: '',
217 verified: new Date(),
218 expires: new Date(),
219 secret: '',
220 signatureAlgorithm: 'hmacy',
221 httpRemoteAddr: '',
222 httpFrom: '',
223 contentDelivered: new Date(),
224 deliveryAttemptsSinceSuccess: 0,
225 deliveryNextAttempt: new Date(-Infinity),
226 }]);
227 });
228 it('covers', async function() {
229 await manager.getTopicDetails(res, ctx);
230 assert(res.end.called);
231 });
232 it('covers non-matching profile', async function () {
233 ctx.session = {
234 authenticatedProfile: 'https://different.example.com/profile',
235 };
236 await manager.getTopicDetails(res, ctx);
237 assert.strictEqual(ctx.topic, null);
238 assert(res.end.called);
239 });
240 it('covers matching profile', async function () {
241 ctx.session = {
242 authenticatedProfile: 'https://example.com/profile',
243 };
244 await manager.getTopicDetails(res, ctx);
245 assert(ctx.topic);
246 assert(res.end.called);
247 });
248 }); // getTopicDetails
249
250 describe('postRoot', function () {
251 let origProcessImmediately;
252 beforeEach(function () {
253 origProcessImmediately = manager.options.manager.processImmediately;
254 ctx.parsedBody = {};
255 });
256 this.afterEach(function () {
257 manager.options.manager.processImmediately = origProcessImmediately;
258 });
259 it('requires parameters', async function () {
260 try {
261 await manager.postRoot(req, res, ctx);
262 assert.fail(noExpectedException);
263 } catch (e) {
264 assert.strictEqual(e.message, 'Bad Request');
265 }
266 });
267 it('accepts valid subscription', async function () {
268 ctx = Object.assign({}, testData.validSubscribeCtx);
269 manager.db.topicGetByUrl.resolves({
270 id: 111,
271 });
272 manager.db.verificationInsert.resolves({
273 changes: 1,
274 lastInsertRowid: undefined,
275 duration: 12,
276 });
277 await manager.postRoot(req, res, ctx);
278 assert(manager.db.verificationInsert.called);
279 assert(res.end.called);
280 });
281 it('accepts valid subscription without claiming work', async function () {
282 manager.options.manager.processImmediately = false;
283 ctx = Object.assign({}, testData.validSubscribeCtx);
284 manager.db.topicGetByUrl.resolves({
285 id: 111,
286 });
287 manager.db.verificationInsert.resolves({
288 changes: 1,
289 lastInsertRowid: undefined,
290 duration: 12,
291 });
292 await manager.postRoot(req, res, ctx);
293 assert(manager.db.verificationInsert.called);
294 assert(!manager.communication.verificationProcess.called);
295 assert(res.end.called);
296 });
297 it('accepts valid subscription, covers processVerification failure', async function () {
298 ctx = Object.assign({}, testData.validSubscribeCtx);
299 manager.communication.verificationProcess.rejects('failed');
300 manager.db.topicGetByUrl.resolves({
301 id: 111,
302 });
303 manager.db.verificationInsert.resolves({
304 changes: 1,
305 lastInsertRowid: undefined,
306 duration: 12,
307 });
308 await manager.postRoot(req, res, ctx);
309 assert(manager.db.verificationInsert.called);
310 assert(res.end.called);
311 assert(manager.logger.error.called);
312 });
313 it('covers db.verificationInsert failure', async function () {
314 const expectedException = new Error('failure');
315 ctx = Object.assign({}, testData.validSubscribeCtx);
316 manager.db.topicGetByUrl.resolves({
317 id: 111,
318 });
319 manager.db.verificationInsert.rejects(expectedException);
320 assert.rejects(async () => {
321 await manager.postRoot(req, res, ctx);
322 }, expectedException);
323 });
324 it('accepts valid unsubscription', async function () {
325 ctx = Object.assign({}, testData.validUnsubscribeCtx);
326 manager.db.topicGetByUrl.resolves({
327 id: 111,
328 });
329 manager.db.subscriptionGet.resolves({
330 id: 22,
331 });
332 manager.db.verificationInsert.resolves({
333 changes: 1,
334 lastInsertRowid: undefined,
335 duration: 12,
336 });
337 await manager.postRoot(req, res, ctx);
338 assert(res.end.called);
339 });
340 it('accepts valid publish', async function () {
341 ctx = Object.assign({}, testData.validPublishCtx);
342 manager.db.topicGetByUrl.resolves({
343 id: 111,
344 });
345 manager.db.topicFetchRequested.resolves({
346 changes: 1,
347 lastInsertRowid: undefined,
348 duration: 12,
349 });
350 await manager.postRoot(req, res, ctx);
351 assert(res.end.called);
352 });
353 }); // postRoot
354
355 describe('_getRootData', function () {
356 it('extracts expected values', function () {
357 req.getHeader.returns('user@example.com');
358 ctx = Object.assign({}, testData.validSubscribeCtx)
359 const result = Manager._getRootData(req, ctx);
360 assert.deepStrictEqual(result, testData.validRootData);
361 });
362 }); // _getRootData
363
364 describe('_validateRootData', function () {
365 // This only wraps the other _check functions, not bothering with coverage.
366 }); // _validateRootData
367
368 describe('_checkTopic', function () {
369 let dbCtx, data, warn, err;
370 const topic = {
371 id: 111,
372 leaseSecondsPreferred: 86400 * 10,
373 leaseSecondsMax: 86400 * 20,
374 leaseSecondsMin: 86400,
375 };
376 beforeEach(function () {
377 dbCtx = {};
378 data = {};
379 warn = [];
380 err = [];
381 });
382 it('succeeds', async function () {
383 data = {
384 topic: 'http://example.com/blog',
385 };
386 manager.db.topicGetByUrl.resolves(topic);
387 await manager._checkTopic(dbCtx, data, warn, err);
388 assert.strictEqual(warn.length, 0, warn);
389 assert.strictEqual(err.length, 0, err);
390 assert.strictEqual(data.topicId, 111);
391 assert.strictEqual(data.leaseSeconds, 864000);
392 });
393 it('errors on unknown topic', async function () {
394 manager.db.topicGetByUrl.resolves();
395 await manager._checkTopic(dbCtx, data, warn, err);
396 assert.strictEqual(warn.length, 0, warn);
397 assert.strictEqual(err.length, 1, err);
398 });
399 it('warns on lease under min range', async function () {
400 data = {
401 topic: 'http://example.com/blog',
402 leaseSeconds: 97,
403 };
404 manager.db.topicGetByUrl.resolves(topic);
405 await manager._checkTopic(dbCtx, data, warn, err);
406 assert.strictEqual(warn.length, 1, warn);
407 assert.strictEqual(err.length, 0, err);
408 assert.strictEqual(data.topicId, 111);
409 assert.strictEqual(data.leaseSeconds, 86400);
410 });
411 it('warns on lease over max range', async function () {
412 data = {
413 topic: 'http://example.com/blog',
414 leaseSeconds: 86400 * 100,
415 };
416 manager.db.topicGetByUrl.resolves(topic);
417 await manager._checkTopic(dbCtx, data, warn, err);
418 assert.strictEqual(warn.length, 1, warn);
419 assert.strictEqual(err.length, 0, err);
420 assert.strictEqual(data.topicId, 111);
421 assert.strictEqual(data.leaseSeconds, 86400 * 20);
422 });
423 it('sets publisher validation state when available', async function () {
424 data = {
425 topic: 'http://example.com/blog',
426 };
427 manager.db.topicGetByUrl.resolves(Object.assign({}, topic, {
428 publisherValidationUrl: 'http://example.com/validate',
429 }));
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);
435 assert.strictEqual(data.isPublisherValidated, false);
436 });
437 it('accepts new public subscribe topic', async function () {
438 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
439 data = {
440 topic: 'http://example.com/blog',
441 };
442 await manager._checkTopic(dbCtx, data, warn, err);
443 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
444 assert.strictEqual(err.length, 0, 'unexpected errors length');
445 assert.strictEqual(data.topicId, 111, 'unexpected topic id');
446 });
447 it('does not accept new public subscribe for invalid topic', async function () {
448 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
449 data = {
450 topic: 'not a topic',
451 };
452 await manager._checkTopic(dbCtx, data, warn, err);
453 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
454 assert.strictEqual(err.length, 1, 'unexpected errors length');
455 });
456 }); // _checkTopic
457
458 describe('_checkCallbackAndSecrets', function () {
459 let data, warn, err;
460 let origStrictSecrets;
461 before(function () {
462 origStrictSecrets = manager.options.manager.strictSecrets;
463 });
464 beforeEach(function () {
465 data = {};
466 warn = [];
467 err = [];
468 });
469 afterEach(function () {
470 manager.options.manager.strictSecrets = origStrictSecrets;
471 });
472 it('succeeds', function () {
473 data = {
474 callback: 'https://example.com/callback',
475 secret: 'so safe',
476 isSecure: true,
477 };
478 manager._checkCallbackAndSecrets(data, warn, err);
479 assert.strictEqual(warn.length, 0, warn);
480 assert.strictEqual(err.length, 0, err);
481 });
482 it('errors with invalid callback', function () {
483 data = {
484 callback: 'not a url',
485 secret: 'so safe',
486 isSecure: true,
487 };
488 manager._checkCallbackAndSecrets(data, warn, err);
489 assert.strictEqual(warn.length, 0, warn);
490 assert.strictEqual(err.length, 1, err);
491 });
492 it('errors when secret too large', function () {
493 data = {
494 callback: 'https://example.com/callback',
495 secret: 'x'.repeat(256),
496 isSecure: true,
497 };
498 manager._checkCallbackAndSecrets(data, warn, err);
499 assert.strictEqual(warn.length, 0, warn);
500 assert.strictEqual(err.length, 1, err);
501 });
502 it('warns when callback is insecure', function () {
503 data = {
504 callback: 'http://example.com/callback',
505 isSecure: true,
506 };
507 manager._checkCallbackAndSecrets(data, warn, err);
508 assert.strictEqual(warn.length, 1, warn);
509 assert.strictEqual(err.length, 0, err);
510 });
511 it('warns when hub is insecure with secret', function () {
512 data = {
513 callback: 'https://example.com/callback',
514 secret: 'so safe',
515 isSecure: false,
516 };
517 manager._checkCallbackAndSecrets(data, warn, err);
518 assert.strictEqual(warn.length, 1, warn);
519 assert.strictEqual(err.length, 0, err);
520 });
521 it('errors when callback is insecure with secret and strict', function () {
522 manager.options.manager.strictSecrets = true;
523 data = {
524 callback: 'http://example.com/callback',
525 secret: 'so safe',
526 isSecure: true,
527 };
528 manager._checkCallbackAndSecrets(data, warn, err);
529 assert.strictEqual(warn.length, 1, warn);
530 assert.strictEqual(err.length, 1, err);
531 });
532 }); // _checkCallbackAndSecrets
533
534 describe('_checkMode', function () {
535 let dbCtx, data, warn, err;
536 beforeEach(function () {
537 dbCtx = {};
538 data = {};
539 warn = [];
540 err = [];
541 });
542 it('subscribe succeeds', async function () {
543 data = {
544 mode: 'subscribe',
545 };
546 await manager._checkMode(dbCtx, data, warn, err);
547 assert.strictEqual(warn.length, 0);
548 assert.strictEqual(err.length, 0);
549 });
550 it('unsubscribe succeeds', async function () {
551 data = {
552 mode: 'unsubscribe',
553 callback: 'http://example.com',
554 topicId: 123,
555 };
556 manager.db.subscriptionGet.resolves({
557 expires: (Date.now() / 1000) + 60,
558 });
559 await manager._checkMode(dbCtx, data, warn, err);
560 assert.strictEqual(warn.length, 0, warn);
561 assert.strictEqual(err.length, 0, err);
562 });
563 it('unsubscribe requires valid data', async function () {
564 data = {
565 mode: 'unsubscribe',
566 callback: 'http://example.com',
567 topicId: undefined,
568 };
569 manager.db.subscriptionGet.resolves({
570 expires: (Date.now() / 1000) - 60,
571 });
572 await manager._checkMode(dbCtx, data, warn, err);
573 assert.strictEqual(warn.length, 0, warn);
574 assert.strictEqual(err.length, 1, err);
575 });
576 it('unsubscribe ignores expired subscription', async function () {
577 data = {
578 mode: 'unsubscribe',
579 callback: 'http://example.com',
580 topicId: 123,
581 };
582 manager.db.subscriptionGet.resolves({
583 expires: (Date.now() / 1000) - 60,
584 });
585 await manager._checkMode(dbCtx, data, warn, err);
586 assert.strictEqual(warn.length, 0, warn);
587 assert.strictEqual(err.length, 1, err);
588 });
589 }); // _checkMode
590
591 describe('_publishTopics', function () {
592 let dbCtx, data, requestId;
593 beforeEach(function () {
594 dbCtx = {};
595 data = {};
596 requestId = 'blah';
597 });
598 it('succeeds', async function () {
599 manager.db.topicGetByUrl.resolves({
600 id: 222,
601 });
602 Object.assign(data, testData.validPublishRootData);
603 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
604 assert.strictEqual(topicResults.length, 1);
605 assert.strictEqual(topicResults[0].warn.length, 0, 'unexpected warnings length');
606 assert.strictEqual(topicResults[0].err.length, 0, 'unexpected errors length');
607 assert.strictEqual(topicResults[0].topicId, 222, 'unexpected topic id');
608 });
609 it('fails bad url', async function () {
610 Object.assign(data, testData.validPublishRootData, { topic: 'not_a_url' });
611 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
612 assert.strictEqual(topicResults.length, 1);
613 assert.strictEqual(topicResults[0].err.length, 1, 'unexpected errors length');
614 assert.strictEqual(topicResults[0].warn.length, 0);
615 });
616 it('accepts new public publish topic', async function () {
617 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
618 id: 222,
619 });
620 Object.assign(data, testData.validPublishRootData);
621 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
622 assert.strictEqual(topicResults.length, 1);
623 assert.strictEqual(topicResults[0].warn.length, 0, 'unexpected warnings length');
624 assert.strictEqual(topicResults[0].err.length, 0, 'unexpected errors length');
625 assert.strictEqual(topicResults[0].topicId, 222, 'unexpected topic id');
626 });
627 it('does not publish deleted topic', async function () {
628 manager.db.topicGetByUrl.resolves({
629 id: 222,
630 isDeleted: true,
631 });
632 Object.assign(data, testData.validPublishRootData);
633 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
634 assert.strictEqual(topicResults.length, 1);
635 assert.strictEqual(topicResults[0].warn.length, 0, 'unexpected warnings length');
636 assert.strictEqual(topicResults[0].err.length, 1, 'unexpected errors length');
637 assert.strictEqual(topicResults[0].topicId, undefined, 'unexpected topic id');
638 });
639 it('no topics', async function() {
640 Object.assign(data, testData.validPublishRootData);
641 delete data.topic;
642 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
643 assert.strictEqual(topicResults.length, 0);
644 });
645 it('multiple valid topics', async function () {
646 manager.db.topicGetByUrl.resolves({
647 id: 222,
648 });
649 Object.assign(data, testData.validPublishRootData);
650 data.url = ['https://example.com/first', 'https://example.com/second'];
651 data.topic = ['https://example.com/third'];
652 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
653 assert.strictEqual(topicResults.length, 3);
654 assert.strictEqual(topicResults[0].warn.length, 0, 'unexpected warnings length');
655 assert.strictEqual(topicResults[0].err.length, 0, 'unexpected errors length');
656 assert.strictEqual(topicResults[0].topicId, 222, 'unexpected topic id');
657 assert.strictEqual(topicResults[1].warn.length, 0, 'unexpected warnings length');
658 assert.strictEqual(topicResults[1].err.length, 0, 'unexpected errors length');
659 assert.strictEqual(topicResults[1].topicId, 222, 'unexpected topic id');
660 assert.strictEqual(topicResults[2].warn.length, 0, 'unexpected warnings length');
661 assert.strictEqual(topicResults[2].err.length, 0, 'unexpected errors length');
662 assert.strictEqual(topicResults[2].topicId, 222, 'unexpected topic id');
663 });
664 it('mix of valid and invalid topics', async function () {
665 manager.db.topicGetByUrl.onCall(1).resolves().resolves({
666 id: 222,
667 });
668 Object.assign(data, testData.validPublishRootData);
669 data.url = ['https://example.com/first', 'not a url'];
670 data.topic = ['https://example.com/third'];
671 const topicResults = await manager._publishTopics(dbCtx, data, requestId);
672 assert.strictEqual(topicResults.length, 3);
673 assert.strictEqual(topicResults[0].warn.length, 0, 'unexpected warnings length');
674 assert.strictEqual(topicResults[0].err.length, 0, 'unexpected errors length');
675 assert.strictEqual(topicResults[0].topicId, 222, 'unexpected topic id');
676 assert.strictEqual(topicResults[1].warn.length, 0, 'unexpected warnings length');
677 assert.strictEqual(topicResults[1].err.length, 1, 'unexpected errors length');
678 assert.strictEqual(topicResults[1].topicId, undefined, 'unexpected topic id');
679 assert.strictEqual(topicResults[2].warn.length, 0, 'unexpected warnings length');
680 assert.strictEqual(topicResults[2].err.length, 0, 'unexpected errors length');
681 assert.strictEqual(topicResults[2].topicId, 222, 'unexpected topic id');
682 });
683 }); // _publishTopics
684
685 describe('_publishRequest', function () {
686 let dbCtx, data, res, ctx;
687 beforeEach(function () {
688 dbCtx = {};
689 data = {};
690 res = {
691 end: sinon.stub(),
692 };
693 ctx = {};
694 });
695 it('requires a topic', async function () {
696 try {
697 await manager._publishRequest(dbCtx, data, res, ctx);
698 assert.fail(noExpectedException);
699 } catch (e) {
700 assert(e instanceof Errors.ResponseError);
701 }
702 });
703 it('processes one topic', async function() {
704 manager.db.topicGetByUrl.resolves({
705 id: 222,
706 });
707 Object.assign(data, testData.validPublishRootData);
708 manager.db.topicFetchRequested.resolves();
709 await manager._publishRequest(dbCtx, data, res, ctx);
710 assert(manager.db.topicFetchRequested.called);
711 assert.strictEqual(res.statusCode, 202);
712 assert(res.end.called);
713 });
714 it('processes mix of valid and invalid topics', async function () {
715 ctx.responseType = 'application/json';
716 manager.db.topicGetByUrl.onCall(1).resolves().resolves({
717 id: 222,
718 });
719 Object.assign(data, testData.validPublishRootData);
720 data.url = ['https://example.com/first', 'not a url'];
721 data.topic = ['https://example.com/third'];
722 await manager._publishRequest(dbCtx, data, res, ctx);
723 assert.strictEqual(res.statusCode, 207);
724 assert(res.end.called);
725 });
726 it('covers topicFetchRequest failure', async function () {
727 manager.db.topicGetByUrl.resolves({
728 id: 222,
729 });
730 Object.assign(data, testData.validPublishRootData);
731 const expected = new Error('boo');
732 manager.db.topicFetchRequested.rejects(expected);
733 try {
734 await manager._publishRequest(dbCtx, data, res, ctx);
735 assert.fail(noExpectedException);
736 } catch (e) {
737 assert.deepStrictEqual(e, expected);
738 }
739 });
740 it('covers immediate processing error', async function() {
741 manager.options.manager.processImmediately = true;
742 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
743 id: 222,
744 });
745 manager.communication.topicFetchClaimAndProcessById.rejects();
746 Object.assign(data, testData.validPublishRootData);
747 await manager._publishRequest(dbCtx, data, res, ctx);
748 assert(manager.db.topicFetchRequested.called);
749 assert.strictEqual(res.statusCode, 202);
750 assert(res.end.called);
751 assert(manager.communication.topicFetchClaimAndProcessById.called)
752 });
753 it('covers no immediate processing', async function() {
754 manager.options.manager.processImmediately = false;
755 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
756 id: 222,
757 });
758 Object.assign(data, testData.validPublishRootData);
759 await manager._publishRequest(dbCtx, data, res, ctx);
760 assert(manager.db.topicFetchRequested.called);
761 assert.strictEqual(res.statusCode, 202);
762 assert(res.end.called);
763 assert(!manager.communication.topicFetchClaimAndProcessById.called)
764 });
765 }); // _publishRequest
766
767 describe('multiPublishContent', function () {
768 let publishTopics;
769 beforeEach(function () {
770 publishTopics = [{
771 url: 'https://example.com/first',
772 warn: [],
773 err: [],
774 topicId: 222,
775 status: 202,
776 statusMessage: 'Accepted',
777 },
778 {
779 url: 'not a url',
780 warn: [],
781 err: [ 'invalid topic url (failed to parse url)' ],
782 topicId: undefined,
783 status: 400,
784 statusMessage: 'Bad Request',
785 }];
786 });
787 it('covers json response', function () {
788 ctx.responseType = 'application/json';
789 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":[]}]';
790 const result = Manager.multiPublishContent(ctx, publishTopics);
791 assert.deepStrictEqual(result, expected);
792 });
793 it('covers text response', function () {
794 ctx.responseType = 'text/plain';
795 const expected = `https://example.com/first [202 Accepted]
796 ----
797 not a url [400 Bad Request]
798 \terror: invalid topic url (failed to parse url)`;
799 const result = Manager.multiPublishContent(ctx, publishTopics);
800 assert.deepStrictEqual(result, expected);
801 });
802 }); // multiPublishContent
803
804 describe('processTasks', function () {
805 it('covers', async function () {
806 sinon.stub(manager.communication.worker, 'process').resolves();
807 await manager.processTasks(res, ctx);
808 assert(manager.communication.worker.process.called);
809 assert(res.end.called);
810 });
811 it('covers error', async function () {
812 sinon.stub(manager.communication.worker, 'process').rejects();
813 await manager.processTasks(res, ctx);
814 assert(manager.communication.worker.process.called);
815 assert(res.end.called);
816 });
817 }); // processTasks
818
819 describe('updateTopic', function () {
820 it('fails if no topic exists', async function () {
821 try {
822 await manager.updateTopic(res, ctx);
823 assert.fail(noExpectedException);
824 } catch (e) {
825 assert(e instanceof Errors.ResponseError);
826 }
827 });
828 it('deletes', async function () {
829 ctx.method = 'DELETE';
830 manager.db.topicGetById.resolves({});
831 await manager.updateTopic(res, ctx);
832 assert(manager.db.topicDeleted.called);
833 });
834 it('does not patch without data', async function () {
835 manager.db.topicGetById.resolves({});
836 await manager.updateTopic(res, ctx);
837 assert(!manager.db.topicUpdate.called);
838 assert.strictEqual(res.statusCode, 204);
839 });
840 it('does not patch with same data', async function () {
841 manager.db.topicGetById.resolves({
842 leaseSecondsPreferred: '86400',
843 });
844 ctx.parsedBody = {
845 leaseSecondsPreferred: '86400',
846 };
847 await manager.updateTopic(res, ctx);
848 assert(!manager.db.topicUpdate.called);
849 assert.strictEqual(res.statusCode, 204);
850 });
851 it('patches', async function () {
852 ctx.queryParams = {
853 leaseSecondsPreferred: '86400',
854 };
855 manager.db.topicGetById.resolves({});
856 await manager.updateTopic(res, ctx);
857 assert(manager.db.topicUpdate.called);
858 });
859 it('handles validation error', async function () {
860 ctx.queryParams = {
861 leaseSecondsPreferred: 'blorp',
862 };
863 manager.db.topicGetById.resolves({});
864 manager.db.topicUpdate.rejects(new DBErrors.DataValidation('something'));
865 try {
866 await manager.updateTopic(res, ctx);
867 assert.fail(noExpectedException);
868 } catch (e) {
869 assert(e instanceof Errors.ResponseError);
870 assert.strictEqual(e.statusCode, 400);
871 }
872 });
873 it('handles generic error', async function () {
874 const expected = new Error('blah');
875 ctx.queryParams = {
876 leaseSecondsPreferred: '123',
877 };
878 manager.db.topicGetById.resolves({});
879 manager.db.topicUpdate.rejects(expected);
880 try {
881 await manager.updateTopic(res, ctx);
882 assert.fail(noExpectedException);
883 } catch (e) {
884 assert.deepStrictEqual(e, expected);
885 }
886 });
887 }); // updateTopic
888
889 describe('updateSubscription', function () {
890 it('fails if no subscription exists', async function () {
891 try {
892 await manager.updateSubscription(res, ctx);
893 assert.fail(noExpectedException);
894 } catch (e) {
895 assert(e instanceof Errors.ResponseError);
896 }
897 });
898 it('deletes', async function () {
899 ctx.method = 'DELETE';
900 manager.db.subscriptionGetById.resolves({});
901 await manager.updateSubscription(res, ctx);
902 assert(manager.db.verificationInsert.called);
903 });
904 it('does not patch without data', async function () {
905 manager.db.subscriptionGetById.resolves({});
906 await manager.updateSubscription(res, ctx);
907 assert(!manager.db.subscriptionUpdate.called);
908 assert.strictEqual(res.statusCode, 204);
909 });
910 it('does not patch with same data', async function () {
911 manager.db.subscriptionGetById.resolves({
912 signatureAlgorithm: 'sha256',
913 });
914 ctx.parsedBody = {
915 signatureAlgorithm: 'sha256',
916 };
917 await manager.updateSubscription(res, ctx);
918 assert(!manager.db.subscriptionUpdate.called);
919 assert.strictEqual(res.statusCode, 204);
920 });
921 it('patches', async function () {
922 ctx.queryParams = {
923 signatureAlgorithm: 'sha256',
924 };
925 manager.db.subscriptionGetById.resolves({});
926 await manager.updateSubscription(res, ctx);
927 assert(manager.db.subscriptionUpdate.called);
928 });
929 it('handles validation error', async function () {
930 ctx.queryParams = {
931 signatureAlgorithm: 123,
932 };
933 manager.db.subscriptionGetById.resolves({});
934 manager.db.subscriptionUpdate.rejects(new DBErrors.DataValidation('something'));
935 try {
936 await manager.updateSubscription(res, ctx);
937 assert.fail(noExpectedException);
938 } catch (e) {
939 assert(e instanceof Errors.ResponseError);
940 assert.strictEqual(e.statusCode, 400);
941 }
942 });
943 it('handles generic error', async function () {
944 const expected = new Error('blah');
945 ctx.queryParams = {
946 signatureAlgorithm: 'blorp',
947 };
948 manager.db.subscriptionGetById.resolves({});
949 manager.db.subscriptionUpdate.rejects(expected);
950 try {
951 await manager.updateSubscription(res, ctx);
952 assert.fail(noExpectedException);
953 } catch (e) {
954 assert.deepStrictEqual(e, expected);
955 }
956 });
957 }); // updateSubscription
958
959 }); // Manager