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