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