Merge branch 'v1.3-dev'
[websub-hub] / 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 stubDb._reset();
41 stubLogger._reset();
42 });
43 afterEach(function () {
44 sinon.restore();
45 });
46
47 it('instantiates', function () {
48 assert(manager);
49 });
50
51 describe('getRoot', function () {
52 beforeEach(function () {
53 sinon.stub(common, 'isClientCached');
54 req = {};
55 });
56 it('normal response', async function () {
57 common.isClientCached.returns(false);
58 await manager.getRoot(req, res, ctx);
59 assert(res.end.called);
60 });
61 it('repeat response', async function () {
62 manager.startTime = (new Date()).toGMTString();
63 common.isClientCached.returns(true);
64 await manager.getRoot(req, res, ctx);
65 assert(res.end.called);
66 });
67 it('cached response', async function () {
68 common.isClientCached.returns(true);
69 await manager.getRoot(req, res, ctx);
70 assert(res.end.called);
71 assert.strictEqual(res.statusCode, 304);
72 });
73 }); // getRoot
74
75 describe('getHealthcheck', function () {
76 it('normal response', async function () {
77 await manager.getHealthcheck(res, ctx);
78 assert(res.end.called);
79 });
80 }); // getById
81
82 describe('getInfo', function () {
83 it('requires query param', async function() {
84 ctx.queryParams = {};
85 try {
86 await manager.getInfo(res, ctx);
87 assert.fail(noExpectedException);
88 } catch (e) {
89 assert.strictEqual(e.statusCode, 400);
90 }
91 });
92 it('requires parsable query param', async function() {
93 ctx.queryParams = { topic: 'not a url' };
94 try {
95 await manager.getInfo(res, ctx);
96 assert.fail(noExpectedException);
97 } catch (e) {
98 assert.strictEqual(e.statusCode, 400);
99 }
100 });
101 it('does not find unhandled topic', async function() {
102 ctx.queryParams = { topic: 'https://example.com/blog/' };
103 try {
104 await manager.getInfo(res, ctx);
105 assert.fail(noExpectedException);
106 } catch (e) {
107 assert.strictEqual(e.statusCode, 404);
108 }
109 });
110 it('returns a count', async function() {
111 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
112 ctx.queryParams = {
113 topic: 'https://example.com/blog/',
114 };
115 await manager.getInfo(res, ctx);
116 assert(res.end.called);
117 });
118 it('returns a count as json', async function() {
119 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
120 ctx.responseType = 'application/json';
121 ctx.queryParams = {
122 topic: 'https://example.com/blog/',
123 };
124 await manager.getInfo(res, ctx);
125 assert(res.end.called);
126 });
127 it('returns a count as json 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: 'json',
133 };
134 await manager.getInfo(res, ctx);
135 assert(res.end.called);
136 assert(res.setHeader.called);
137 });
138 it('returns an svg badge as override format', async function() {
139 manager.db.subscriptionCountByTopicUrl.resolves({ count: 4 });
140 ctx.responseType = 'text/html';
141 ctx.queryParams = {
142 topic: 'https://example.com/blog/',
143 format: 'svg',
144 };
145 await manager.getInfo(res, ctx);
146 assert(res.end.called);
147 assert(res.setHeader.called);
148 });
149 }); // getInfo
150
151 describe('getAdminOverview', function () {
152 it('covers', async function () {
153 manager.db.topicGetAll.resolves([
154 {
155 id: '56c557ce-e667-11eb-bd80-0025905f714a',
156 created: new Date(),
157 url: 'https://example.com/',
158 leaseSecondsPreferred: 123,
159 leaseSecondsMin: 12,
160 leaseSecondsMax: 123456789,
161 publisherValidationUrl: null,
162 contentHashAlgorithm: 'hashy',
163 isActive: true,
164 isDeleted: false,
165 lastPublish: new Date(-Infinity),
166 contentFetchNextAttempt: undefined,
167 contentFetchAttemptsSinceSuccess: 3,
168 contentUpdated: new Date(0),
169 contentHash: 'abc',
170 contentType: 'foo',
171 subscribers: 12,
172 },
173 ]);
174 await manager.getAdminOverview(res, ctx);
175 assert(res.end.called);
176 });
177 });
178
179 describe('getTopicDetails', function () {
180 it('covers', async function() {
181 ctx.params.topicId = '56c557ce-e667-11eb-bd80-0025905f714a';
182 manager.db.topicGetById.resolves({
183 id: '56c557ce-e667-11eb-bd80-0025905f714a',
184 created: new Date(),
185 url: 'https://example.com/',
186 leaseSecondsPreferred: 123,
187 leaseSecondsMin: 12,
188 leaseSecondsMax: 123456789,
189 publisherValidationUrl: null,
190 contentHashAlgorithm: 'hashy',
191 isActive: true,
192 isDeleted: false,
193 lastPublish: new Date(-Infinity),
194 contentFetchNextAttempt: undefined,
195 contentFetchAttemptsSinceSuccess: 3,
196 contentUpdated: new Date(0),
197 contentHash: 'abc',
198 contentType: 'foo',
199 subscribers: 12,
200 });
201 manager.db.subscriptionsByTopicId.resolves([{
202 id: '',
203 created: new Date(),
204 topicId: '56c557ce-e667-11eb-bd80-0025905f714a',
205 callback: '',
206 verified: new Date(),
207 expires: new Date(),
208 secret: '',
209 signatureAlgorithm: 'hmacy',
210 httpRemoteAddr: '',
211 httpFrom: '',
212 contentDelivered: new Date(),
213 deliveryAttemptsSinceSuccess: 0,
214 deliveryNextAttempt: new Date(-Infinity),
215 }]);
216 await manager.getTopicDetails(res, ctx);
217 assert(res.end.called);
218 });
219 }); // getTopicDetails
220
221 describe('postRoot', function () {
222 let origProcessImmediately;
223 beforeEach(function () {
224 origProcessImmediately = manager.options.manager.processImmediately;
225 ctx.parsedBody = {};
226 });
227 this.afterEach(function () {
228 manager.options.manager.processImmediately = origProcessImmediately;
229 });
230 it('requires parameters', async function () {
231 try {
232 await manager.postRoot(req, res, ctx);
233 assert.fail(noExpectedException);
234 } catch (e) {
235 assert.strictEqual(e.message, 'Bad Request');
236 }
237 });
238 it('accepts valid subscription', async function () {
239 ctx = Object.assign({}, testData.validSubscribeCtx);
240 manager.db.topicGetByUrl.resolves({
241 id: 111,
242 });
243 manager.db.verificationInsert.resolves({
244 changes: 1,
245 lastInsertRowid: undefined,
246 duration: 12,
247 });
248 await manager.postRoot(req, res, ctx);
249 assert(manager.db.verificationInsert.called);
250 assert(res.end.called);
251 });
252 it('accepts valid subscription without claiming work', async function () {
253 manager.options.manager.processImmediately = false;
254 ctx = Object.assign({}, testData.validSubscribeCtx);
255 manager.db.topicGetByUrl.resolves({
256 id: 111,
257 });
258 manager.db.verificationInsert.resolves({
259 changes: 1,
260 lastInsertRowid: undefined,
261 duration: 12,
262 });
263 await manager.postRoot(req, res, ctx);
264 assert(manager.db.verificationInsert.called);
265 assert(!manager.communication.verificationProcess.called);
266 assert(res.end.called);
267 });
268 it('accepts valid subscription, covers processVerification failure', async function () {
269 ctx = Object.assign({}, testData.validSubscribeCtx);
270 manager.communication.verificationProcess.rejects('failed');
271 manager.db.topicGetByUrl.resolves({
272 id: 111,
273 });
274 manager.db.verificationInsert.resolves({
275 changes: 1,
276 lastInsertRowid: undefined,
277 duration: 12,
278 });
279 await manager.postRoot(req, res, ctx);
280 assert(manager.db.verificationInsert.called);
281 assert(res.end.called);
282 assert(manager.logger.error.called);
283 });
284 it('covers db.verificationInsert failure', async function () {
285 const expectedException = new Error('failure');
286 ctx = Object.assign({}, testData.validSubscribeCtx);
287 manager.db.topicGetByUrl.resolves({
288 id: 111,
289 });
290 manager.db.verificationInsert.rejects(expectedException);
291 assert.rejects(async () => {
292 await manager.postRoot(req, res, ctx);
293 }, expectedException);
294 });
295 it('accepts valid unsubscription', async function () {
296 ctx = Object.assign({}, testData.validUnsubscribeCtx);
297 manager.db.topicGetByUrl.resolves({
298 id: 111,
299 });
300 manager.db.subscriptionGet.resolves({
301 id: 22,
302 });
303 manager.db.verificationInsert.resolves({
304 changes: 1,
305 lastInsertRowid: undefined,
306 duration: 12,
307 });
308 await manager.postRoot(req, res, ctx);
309 assert(res.end.called);
310 });
311 it('accepts valid publish', async function () {
312 ctx = Object.assign({}, testData.validPublishCtx);
313 manager.db.topicGetByUrl.resolves({
314 id: 111,
315 });
316 manager.db.topicFetchRequested.resolves({
317 changes: 1,
318 lastInsertRowid: undefined,
319 duration: 12,
320 });
321 await manager.postRoot(req, res, ctx);
322 assert(res.end.called);
323 });
324 }); // postRoot
325
326 describe('_getRootData', function () {
327 it('extracts expected values', function () {
328 req.getHeader.returns('user@example.com');
329 ctx = Object.assign({}, testData.validSubscribeCtx)
330 const result = Manager._getRootData(req, ctx);
331 assert.deepStrictEqual(result, testData.validRootData);
332 });
333 }); // _getRootData
334
335 describe('_validateRootData', function () {
336 // This only wraps the other _check functions, not bothering with coverage.
337 }); // _validateRootData
338
339 describe('_checkTopic', function () {
340 let dbCtx, data, warn, err;
341 const topic = {
342 id: 111,
343 leaseSecondsPreferred: 86400 * 10,
344 leaseSecondsMax: 86400 * 20,
345 leaseSecondsMin: 86400,
346 };
347 beforeEach(function () {
348 dbCtx = {};
349 data = {};
350 warn = [];
351 err = [];
352 });
353 it('succeeds', async function () {
354 data = {
355 topic: 'http://example.com/blog',
356 };
357 manager.db.topicGetByUrl.resolves(topic);
358 await manager._checkTopic(dbCtx, data, warn, err);
359 assert.strictEqual(warn.length, 0, warn);
360 assert.strictEqual(err.length, 0, err);
361 assert.strictEqual(data.topicId, 111);
362 assert.strictEqual(data.leaseSeconds, 864000);
363 });
364 it('errors on unknown topic', async function () {
365 manager.db.topicGetByUrl.resolves();
366 await manager._checkTopic(dbCtx, data, warn, err);
367 assert.strictEqual(warn.length, 0, warn);
368 assert.strictEqual(err.length, 1, err);
369 });
370 it('warns on lease under min range', async function () {
371 data = {
372 topic: 'http://example.com/blog',
373 leaseSeconds: 97,
374 };
375 manager.db.topicGetByUrl.resolves(topic);
376 await manager._checkTopic(dbCtx, data, warn, err);
377 assert.strictEqual(warn.length, 1, warn);
378 assert.strictEqual(err.length, 0, err);
379 assert.strictEqual(data.topicId, 111);
380 assert.strictEqual(data.leaseSeconds, 86400);
381 });
382 it('warns on lease over max range', async function () {
383 data = {
384 topic: 'http://example.com/blog',
385 leaseSeconds: 86400 * 100,
386 };
387 manager.db.topicGetByUrl.resolves(topic);
388 await manager._checkTopic(dbCtx, data, warn, err);
389 assert.strictEqual(warn.length, 1, warn);
390 assert.strictEqual(err.length, 0, err);
391 assert.strictEqual(data.topicId, 111);
392 assert.strictEqual(data.leaseSeconds, 86400 * 20);
393 });
394 it('sets publisher validation state when available', async function () {
395 data = {
396 topic: 'http://example.com/blog',
397 };
398 manager.db.topicGetByUrl.resolves(Object.assign({}, topic, {
399 publisherValidationUrl: 'http://example.com/validate',
400 }));
401 await manager._checkTopic(dbCtx, data, warn, err);
402 assert.strictEqual(warn.length, 0, warn);
403 assert.strictEqual(err.length, 0, err);
404 assert.strictEqual(data.topicId, 111);
405 assert.strictEqual(data.leaseSeconds, 864000);
406 assert.strictEqual(data.isPublisherValidated, false);
407 });
408 it('accepts new public subscribe topic', async function () {
409 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
410 data = {
411 topic: 'http://example.com/blog',
412 };
413 await manager._checkTopic(dbCtx, data, warn, err);
414 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
415 assert.strictEqual(err.length, 0, 'unexpected errors length');
416 assert.strictEqual(data.topicId, 111, 'unexpected topic id');
417 });
418 it('does not accept new public subscribe for invalid topic', async function () {
419 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves(topic);
420 data = {
421 topic: 'not a topic',
422 };
423 await manager._checkTopic(dbCtx, data, warn, err);
424 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
425 assert.strictEqual(err.length, 1, 'unexpected errors length');
426 });
427 }); // _checkTopic
428
429 describe('_checkCallbackAndSecrets', function () {
430 let data, warn, err;
431 let origStrictSecrets;
432 before(function () {
433 origStrictSecrets = manager.options.manager.strictSecrets;
434 });
435 beforeEach(function () {
436 data = {};
437 warn = [];
438 err = [];
439 });
440 afterEach(function () {
441 manager.options.manager.strictSecrets = origStrictSecrets;
442 });
443 it('succeeds', function () {
444 data = {
445 callback: 'https://example.com/callback',
446 secret: 'so safe',
447 isSecure: true,
448 };
449 manager._checkCallbackAndSecrets(data, warn, err);
450 assert.strictEqual(warn.length, 0, warn);
451 assert.strictEqual(err.length, 0, err);
452 });
453 it('errors with invalid callback', function () {
454 data = {
455 callback: 'not a url',
456 secret: 'so safe',
457 isSecure: true,
458 };
459 manager._checkCallbackAndSecrets(data, warn, err);
460 assert.strictEqual(warn.length, 0, warn);
461 assert.strictEqual(err.length, 1, err);
462 });
463 it('errors when secret too large', function () {
464 data = {
465 callback: 'https://example.com/callback',
466 secret: 'x'.repeat(256),
467 isSecure: true,
468 };
469 manager._checkCallbackAndSecrets(data, warn, err);
470 assert.strictEqual(warn.length, 0, warn);
471 assert.strictEqual(err.length, 1, err);
472 });
473 it('warns when callback is insecure', function () {
474 data = {
475 callback: 'http://example.com/callback',
476 isSecure: true,
477 };
478 manager._checkCallbackAndSecrets(data, warn, err);
479 assert.strictEqual(warn.length, 1, warn);
480 assert.strictEqual(err.length, 0, err);
481 });
482 it('warns when hub is insecure with secret', function () {
483 data = {
484 callback: 'https://example.com/callback',
485 secret: 'so safe',
486 isSecure: false,
487 };
488 manager._checkCallbackAndSecrets(data, warn, err);
489 assert.strictEqual(warn.length, 1, warn);
490 assert.strictEqual(err.length, 0, err);
491 });
492 it('errors when callback is insecure with secret and strict', function () {
493 manager.options.manager.strictSecrets = true;
494 data = {
495 callback: 'http://example.com/callback',
496 secret: 'so safe',
497 isSecure: true,
498 };
499 manager._checkCallbackAndSecrets(data, warn, err);
500 assert.strictEqual(warn.length, 1, warn);
501 assert.strictEqual(err.length, 1, err);
502 });
503 }); // _checkCallbackAndSecrets
504
505 describe('_checkMode', function () {
506 let dbCtx, data, warn, err;
507 beforeEach(function () {
508 dbCtx = {};
509 data = {};
510 warn = [];
511 err = [];
512 });
513 it('subscribe succeeds', async function () {
514 data = {
515 mode: 'subscribe',
516 };
517 await manager._checkMode(dbCtx, data, warn, err);
518 assert.strictEqual(warn.length, 0);
519 assert.strictEqual(err.length, 0);
520 });
521 it('unsubscribe succeeds', async function () {
522 data = {
523 mode: 'unsubscribe',
524 callback: 'http://example.com',
525 topicId: 123,
526 };
527 manager.db.subscriptionGet.resolves({
528 expires: (Date.now() / 1000) + 60,
529 });
530 await manager._checkMode(dbCtx, data, warn, err);
531 assert.strictEqual(warn.length, 0, warn);
532 assert.strictEqual(err.length, 0, err);
533 });
534 it('unsubscribe requires valid data', async function () {
535 data = {
536 mode: 'unsubscribe',
537 callback: 'http://example.com',
538 topicId: undefined,
539 };
540 manager.db.subscriptionGet.resolves({
541 expires: (Date.now() / 1000) - 60,
542 });
543 await manager._checkMode(dbCtx, data, warn, err);
544 assert.strictEqual(warn.length, 0, warn);
545 assert.strictEqual(err.length, 1, err);
546 });
547 it('unsubscribe ignores expired subscription', async function () {
548 data = {
549 mode: 'unsubscribe',
550 callback: 'http://example.com',
551 topicId: 123,
552 };
553 manager.db.subscriptionGet.resolves({
554 expires: (Date.now() / 1000) - 60,
555 });
556 await manager._checkMode(dbCtx, data, warn, err);
557 assert.strictEqual(warn.length, 0, warn);
558 assert.strictEqual(err.length, 1, err);
559 });
560 }); // _checkMode
561
562 describe('_checkPublish', function () {
563 let dbCtx, data, warn, err, requestId;
564 beforeEach(function () {
565 dbCtx = {};
566 data = {};
567 warn = [];
568 err = [];
569 requestId = 'blah';
570 });
571 it('succeeds', async function () {
572 manager.db.topicGetByUrl.resolves({
573 id: 222,
574 });
575 Object.assign(data, testData.validPublishRootData);
576 await manager._checkPublish(dbCtx, data, warn, err, requestId);
577 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
578 assert.strictEqual(err.length, 0, 'unexpected errors length');
579 assert.strictEqual(data.topicId, 222, 'unexpected topic id');
580 });
581 it('fails bad url', async function () {
582 Object.assign(data, testData.validPublishRootData, { topic: 'not_a_url' });
583 await manager._checkPublish(dbCtx, data, warn, err, requestId);
584 assert.strictEqual(err.length, 1, 'unexpected errors length');
585 assert.strictEqual(warn.length, 0);
586 });
587 it('accepts new public publish topic', async function () {
588 manager.db.topicGetByUrl.onCall(0).resolves().onCall(1).resolves({
589 id: 222,
590 });
591 Object.assign(data, testData.validPublishRootData);
592 await manager._checkPublish(dbCtx, data, warn, err, requestId);
593 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
594 assert.strictEqual(err.length, 0, 'unexpected errors length');
595 assert.strictEqual(data.topicId, 222, 'unexpected topic id');
596 });
597 it('does not publish deleted topic', async function () {
598 manager.db.topicGetByUrl.resolves({
599 id: 222,
600 isDeleted: true,
601 });
602 Object.assign(data, testData.validPublishRootData);
603 await manager._checkPublish(dbCtx, data, warn, err, requestId);
604 assert.strictEqual(warn.length, 0, 'unexpected warnings length');
605 assert.strictEqual(err.length, 1, 'unexpected errors length');
606 assert.strictEqual(data.topicId, undefined, 'unexpected topic id');
607 });
608 }); // _checkPublish
609
610 describe('processTasks', function () {
611 it('covers', async function () {
612 sinon.stub(manager.communication.worker, 'process');
613 await manager.processTasks(res, ctx);
614 assert(manager.communication.worker.process.called);
615 assert(res.end.called);
616 });
617 }); // processTasks
618
619 describe('updateTopic', function () {
620 it('fails if no topic exists', async function () {
621 try {
622 await manager.updateTopic(res, ctx);
623 assert.fail(noExpectedException);
624 } catch (e) {
625 assert(e instanceof Errors.ResponseError);
626 }
627 });
628 it('deletes', async function () {
629 ctx.method = 'DELETE';
630 manager.db.topicGetById.resolves({});
631 await manager.updateTopic(res, ctx);
632 assert(manager.db.topicDeleted.called);
633 });
634 it('does not patch without data', async function () {
635 manager.db.topicGetById.resolves({});
636 await manager.updateTopic(res, ctx);
637 assert(!manager.db.topicUpdate.called);
638 assert.strictEqual(res.statusCode, 204);
639 });
640 it('does not patch with same data', async function () {
641 manager.db.topicGetById.resolves({
642 leaseSecondsPreferred: '86400',
643 });
644 ctx.parsedBody = {
645 leaseSecondsPreferred: '86400',
646 };
647 await manager.updateTopic(res, ctx);
648 assert(!manager.db.topicUpdate.called);
649 assert.strictEqual(res.statusCode, 204);
650 });
651 it('patches', async function () {
652 ctx.queryParams = {
653 leaseSecondsPreferred: '86400',
654 };
655 manager.db.topicGetById.resolves({});
656 await manager.updateTopic(res, ctx);
657 assert(manager.db.topicUpdate.called);
658 });
659 it('handles validation error', async function () {
660 ctx.queryParams = {
661 leaseSecondsPreferred: 'blorp',
662 };
663 manager.db.topicGetById.resolves({});
664 manager.db.topicUpdate.rejects(new DBErrors.DataValidation('something'));
665 try {
666 await manager.updateTopic(res, ctx);
667 assert.fail(noExpectedException);
668 } catch (e) {
669 assert(e instanceof Errors.ResponseError);
670 assert.strictEqual(e.statusCode, 400);
671 }
672 });
673 it('handles generic error', async function () {
674 const expected = new Error('blah');
675 ctx.queryParams = {
676 leaseSecondsPreferred: '123',
677 };
678 manager.db.topicGetById.resolves({});
679 manager.db.topicUpdate.rejects(expected);
680 try {
681 await manager.updateTopic(res, ctx);
682 assert.fail(noExpectedException);
683 } catch (e) {
684 assert.deepStrictEqual(e, expected);
685 }
686 });
687 }); // updateTopic
688
689 describe('updateSubscription', function () {
690 it('fails if no subscription exists', async function () {
691 try {
692 await manager.updateSubscription(res, ctx);
693 assert.fail(noExpectedException);
694 } catch (e) {
695 assert(e instanceof Errors.ResponseError);
696 }
697 });
698 it('deletes', async function () {
699 ctx.method = 'DELETE';
700 manager.db.subscriptionGetById.resolves({});
701 await manager.updateSubscription(res, ctx);
702 assert(manager.db.verificationInsert.called);
703 });
704 it('does not patch without data', async function () {
705 manager.db.subscriptionGetById.resolves({});
706 await manager.updateSubscription(res, ctx);
707 assert(!manager.db.subscriptionUpdate.called);
708 assert.strictEqual(res.statusCode, 204);
709 });
710 it('does not patch with same data', async function () {
711 manager.db.subscriptionGetById.resolves({
712 signatureAlgorithm: 'sha256',
713 });
714 ctx.parsedBody = {
715 signatureAlgorithm: 'sha256',
716 };
717 await manager.updateSubscription(res, ctx);
718 assert(!manager.db.subscriptionUpdate.called);
719 assert.strictEqual(res.statusCode, 204);
720 });
721 it('patches', async function () {
722 ctx.queryParams = {
723 signatureAlgorithm: 'sha256',
724 };
725 manager.db.subscriptionGetById.resolves({});
726 await manager.updateSubscription(res, ctx);
727 assert(manager.db.subscriptionUpdate.called);
728 });
729 it('handles validation error', async function () {
730 ctx.queryParams = {
731 signatureAlgorithm: 123,
732 };
733 manager.db.subscriptionGetById.resolves({});
734 manager.db.subscriptionUpdate.rejects(new DBErrors.DataValidation('something'));
735 try {
736 await manager.updateSubscription(res, ctx);
737 assert.fail(noExpectedException);
738 } catch (e) {
739 assert(e instanceof Errors.ResponseError);
740 assert.strictEqual(e.statusCode, 400);
741 }
742 });
743 it('handles generic error', async function () {
744 const expected = new Error('blah');
745 ctx.queryParams = {
746 signatureAlgorithm: 'blorp',
747 };
748 manager.db.subscriptionGetById.resolves({});
749 manager.db.subscriptionUpdate.rejects(expected);
750 try {
751 await manager.updateSubscription(res, ctx);
752 assert.fail(noExpectedException);
753 } catch (e) {
754 assert.deepStrictEqual(e, expected);
755 }
756 });
757 }); // updateSubscription
758
759 }); // Manager