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