2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string, sonarjs/no-identical-functions */
6 const assert
= require('assert');
7 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
9 const Manager
= require('../../src/manager');
10 const Config
= require('../../config');
11 const Enum
= require('../../src/enum');
12 const { ResponseError
} = require('../../src/errors');
13 const { UnexpectedResult
} = require('../../src/db/errors');
14 const dns
= require('dns');
16 const StubDatabase
= require('../stub-db');
17 const StubLogger
= require('../stub-logger');
19 const expectedException
= new Error('oh no');
20 const noExpectedException
= 'did not get expected exception';
22 describe('Manager', function () {
23 let manager
, options
, stubDb
, logger
;
26 beforeEach(function () {
27 logger
= new StubLogger();
29 stubDb
= new StubDatabase();
31 options
= new Config('test');
33 getHeader : sinon
.stub(),
37 setHeader: sinon
.stub(),
48 manager
= new Manager(logger
, stubDb
, options
);
49 sinon
.stub(manager
.communication
, 'fetchProfile');
50 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
51 sinon
.stub(manager
.communication
, 'deliverTicket');
52 sinon
.stub(dns
, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
53 sinon
.stub(manager
.queuePublisher
, 'connect');
54 sinon
.stub(manager
.queuePublisher
, 'establishAMQPPlumbing');
55 sinon
.stub(manager
.queuePublisher
, 'publish');
58 afterEach(function () {
62 describe('constructor', function () {
63 it('instantiates', function () {
66 it('covers no queuing', function () {
67 options
.queues
.amqp
.url
= undefined;
68 manager
= new Manager(logger
, stubDb
, options
);
73 describe('initialize', function () {
75 beforeEach(function () {
76 spy
= sinon
.spy(manager
, '_connectQueues');
78 it('covers', async
function () {
79 await manager
.initialize();
82 it('covers no queue', async
function () {
83 delete options
.queues
.amqp
.url
;
84 manager
= new Manager(logger
, stubDb
, options
);
85 await manager
.initialize();
86 assert(spy
.notCalled
);
90 describe('getRoot', function () {
91 it('normal response', async
function () {
92 await manager
.getRoot(res
, ctx
);
93 assert(res
.end
.called
);
97 describe('getMeta', function () {
98 it('normal response', async
function () {
99 await manager
.getMeta(res
, ctx
);
100 assert(res
.end
.called
);
101 JSON
.parse(res
.end
.args
[0][0]);
103 it('covers no ticket queue', async
function () {
104 delete options
.queues
.amqp
.url
;
105 manager
= new Manager(logger
, stubDb
, options
);
106 await manager
.getMeta(res
, ctx
);
107 assert(res
.end
.called
);
111 describe('getHealthcheck', function () {
112 it('normal response', async
function () {
113 await manager
.getHealthcheck(res
, ctx
);
114 assert(res
.end
.called
);
116 }); // getHealthcheck
118 describe('getAuthorization', function () {
119 it('covers missing redirect fields', async
function () {
120 await manager
.getAuthorization(res
, ctx
);
121 assert
.strictEqual(res
.statusCode
, 400);
123 it('requires a configured profile', async
function () {
124 manager
.db
.profilesScopesByIdentifier
.resolves({
139 manager
.communication
.fetchClientIdentifier
.resolves({
142 ctx
.authenticationId
= 'username';
143 Object
.assign(ctx
.queryParams
, {
144 'client_id': 'https://client.example.com/',
145 'redirect_uri': 'https://client.example.com/action',
146 'response_type': 'code',
148 'code_challenge_method': 'S256',
149 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
150 'scope': 'profile email',
152 await manager
.getAuthorization(res
, ctx
);
153 assert
.strictEqual(res
.statusCode
, 302);
154 assert(ctx
.session
.error
);
155 assert(res
.setHeader
.called
);
157 it('covers valid', async
function () {
158 manager
.db
.profilesScopesByIdentifier
.resolves({
160 'https://profile.example.com/': {
163 profiles: ['https://profile.example.com'],
178 profiles: ['https://profile.example.com/'],
181 profiles: ['https://profile.example.com/'],
183 manager
.communication
.fetchClientIdentifier
.resolves({
186 ctx
.authenticationId
= 'username';
187 Object
.assign(ctx
.queryParams
, {
188 'client_id': 'https://client.example.com/',
189 'redirect_uri': 'https://client.example.com/action',
190 'response_type': 'code',
192 'code_challenge_method': 'S256',
193 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
194 'scope': 'profile email',
195 'me': 'https://profile.example.com/',
197 await manager
.getAuthorization(res
, ctx
);
198 assert
.strictEqual(res
.statusCode
, 200);
199 assert
.strictEqual(ctx
.session
.error
, undefined);
200 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
201 assert
.strictEqual(ctx
.notifications
.length
, 0);
203 it('succeeds with mismatched profile hint', async
function () {
204 manager
.db
.profilesScopesByIdentifier
.resolves({
206 'https://profile.example.com/': {
209 profiles: ['https://profile.example.com'],
224 profiles: ['https://profile.example.com/'],
227 profiles: ['https://profile.example.com/'],
229 manager
.communication
.fetchClientIdentifier
.resolves({
232 ctx
.authenticationId
= 'username';
233 Object
.assign(ctx
.queryParams
, {
234 'client_id': 'https://client.example.com/',
235 'redirect_uri': 'https://client.example.com/action',
236 'response_type': 'code',
238 'code_challenge_method': 'S256',
239 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
240 'scope': 'profile email',
241 'me': 'https://somethingelse.example.com/',
243 await manager
.getAuthorization(res
, ctx
);
244 assert(!('me' in ctx
.session
));
245 assert
.strictEqual(res
.statusCode
, 200);
246 assert
.strictEqual(ctx
.session
.error
, undefined);
247 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
249 it('covers invalid redirect', async
function () {
250 manager
.db
.profilesScopesByIdentifier
.resolves({
252 'https://profile.example.com/': {
255 profiles: ['https://profile.example.com'],
270 profiles: ['https://profile.example.com/'],
273 profiles: ['https://profile.example.com/'],
275 manager
.communication
.fetchClientIdentifier
.resolves({
278 ctx
.authenticationId
= 'username';
279 Object
.assign(ctx
.queryParams
, {
280 'client_id': 'https://client.example.com/',
281 'redirect_uri': 'https://client.example.com/action',
282 'response_type': 'blargl',
284 'code_challenge_method': 'S256',
285 'code_challenge': 'IZ9Jmupp0tvhT37e1KxfSZQXwcAGKHuVE51Z3xf5eog',
287 await manager
.getAuthorization(res
, ctx
);
288 assert
.strictEqual(res
.statusCode
, 302);
289 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
290 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 2);
292 it('covers legacy non-PKCE missing fields', async
function () {
293 manager
.db
.profilesScopesByIdentifier
.resolves({
295 'https://profile.example.com/': {
298 profiles: ['https://profile.example.com'],
313 profiles: ['https://profile.example.com/'],
316 profiles: ['https://profile.example.com/'],
318 manager
.communication
.fetchClientIdentifier
.resolves({
321 ctx
.authenticationId
= 'username';
322 Object
.assign(ctx
.queryParams
, {
323 'client_id': 'https://client.example.com/',
324 'redirect_uri': 'https://client.example.com/action',
325 'response_type': 'code',
327 'scope': 'profile email',
328 'me': 'https://profile.example.com/',
330 manager
.options
.manager
.allowLegacyNonPKCE
= true;
332 await manager
.getAuthorization(res
, ctx
);
333 assert
.strictEqual(res
.statusCode
, 200);
334 assert
.strictEqual(ctx
.session
.error
, undefined);
335 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 0);
337 it('rejects legacy non-PKCE not missing all fields', async
function () {
338 manager
.db
.profilesScopesByIdentifier
.resolves({
340 'https://profile.example.com/': {
343 profiles: ['https://profile.example.com'],
358 profiles: ['https://profile.example.com/'],
361 profiles: ['https://profile.example.com/'],
363 manager
.communication
.fetchClientIdentifier
.resolves({
366 ctx
.authenticationId
= 'username';
367 Object
.assign(ctx
.queryParams
, {
368 'client_id': 'https://client.example.com/',
369 'redirect_uri': 'https://client.example.com/action',
370 'response_type': 'code',
371 'code_challenge_method': 'S256',
373 'scope': 'profile email',
374 'me': 'https://profile.example.com/',
376 manager
.options
.manager
.allowLegacyNonPKCE
= true;
378 await manager
.getAuthorization(res
, ctx
);
379 assert
.strictEqual(res
.statusCode
, 302);
380 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
381 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
383 it('rejects legacy non-PKCE not missing all fields', async
function () {
384 manager
.db
.profilesScopesByIdentifier
.resolves({
386 'https://profile.example.com/': {
389 profiles: ['https://profile.example.com'],
404 profiles: ['https://profile.example.com/'],
407 profiles: ['https://profile.example.com/'],
409 manager
.communication
.fetchClientIdentifier
.resolves({
412 ctx
.authenticationId
= 'username';
413 Object
.assign(ctx
.queryParams
, {
414 'client_id': 'https://client.example.com/',
415 'redirect_uri': 'https://client.example.com/action',
416 'response_type': 'code',
417 'code_challenge': 'xxx',
419 'scope': 'profile email',
420 'me': 'https://profile.example.com/',
422 manager
.options
.manager
.allowLegacyNonPKCE
= true;
424 await manager
.getAuthorization(res
, ctx
);
425 assert
.strictEqual(res
.statusCode
, 302);
426 assert
.strictEqual(ctx
.session
.error
, 'invalid_request');
427 assert
.strictEqual(ctx
.session
.errorDescriptions
.length
, 1);
428 }); }); // getAuthorization
430 describe('_setError', function () {
431 it('covers', function () {
432 const err
= 'invalid_request';
433 const errDesc
= 'something went wrong';
434 Manager
._setError(ctx
, err
, errDesc
);
436 it('covers bad error', function () {
437 const err
= 'floopy';
438 const errDesc
= 'something went wrong';
440 Manager
._setError(ctx
, err
, errDesc
);
441 assert
.fail(noExpectedException
);
443 assert(e
instanceof RangeError
);
446 it('covers invalid error description', function () {
447 const err
= 'invalid_scope';
448 const errDesc
= 'something "went wrong"!';
450 Manager
._setError(ctx
, err
, errDesc
);
451 assert
.fail(noExpectedException
);
453 assert(e
instanceof RangeError
);
458 describe('_clientIdRequired', function () {
459 let clientIdentifier
;
460 beforeEach(function () {
464 manager
.communication
.fetchClientIdentifier
.resolves(clientIdentifier
);
466 it('covers valid', async
function () {
467 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
469 await manager
._clientIdRequired(ctx
);
471 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
472 assert
.strictEqual(ctx
.session
.error
, undefined);
473 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
475 it('requires client_id', async
function () {
476 ctx
.queryParams
['client_id'] = undefined;
478 await manager
._clientIdRequired(ctx
);
480 assert(ctx
.session
.error
);
481 assert(ctx
.session
.errorDescriptions
.length
);
483 it('requires valid client_id', async
function () {
484 ctx
.queryParams
['client_id'] = 'not a url';
486 await manager
._clientIdRequired(ctx
);
488 assert(ctx
.session
.error
);
489 assert(ctx
.session
.errorDescriptions
.length
);
491 it('rejects strange schema', async
function () {
492 ctx
.queryParams
['client_id'] = 'file:///etc/shadow';
494 await manager
._clientIdRequired(ctx
);
496 assert(ctx
.session
.error
);
497 assert(ctx
.session
.errorDescriptions
.length
);
499 it('rejects un-allowed parts', async
function () {
500 ctx
.queryParams
['client_id'] = 'https://user:pass@client.example.com/#here';
502 await manager
._clientIdRequired(ctx
);
504 assert(ctx
.session
.error
);
505 assert(ctx
.session
.errorDescriptions
.length
);
507 it('rejects relative paths', async
function () {
508 ctx
.queryParams
['client_id'] = 'https://client.example.com/x/../y/';
510 await manager
._clientIdRequired(ctx
);
512 assert(ctx
.session
.error
);
513 assert(ctx
.session
.errorDescriptions
.length
);
515 it('rejects ipv6 hostname', async
function () {
516 ctx
.queryParams
['client_id'] = 'https://[fd12:3456:789a:1::1]/';
518 await manager
._clientIdRequired(ctx
);
520 assert(ctx
.session
.error
);
521 assert(ctx
.session
.errorDescriptions
.length
);
523 it('allows ipv6 loopback hostname', async
function () {
524 ctx
.queryParams
['client_id'] = 'https://[::1]/';
526 await manager
._clientIdRequired(ctx
);
528 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
529 assert
.strictEqual(ctx
.session
.error
, undefined);
530 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
532 it('rejects ipv4 hostname', async
function () {
533 ctx
.queryParams
['client_id'] = 'https://10.9.8.7/';
535 await manager
._clientIdRequired(ctx
);
537 assert(ctx
.session
.error
);
538 assert(ctx
.session
.errorDescriptions
.length
);
540 it('allows ipv4 loopback hostname', async
function () {
541 ctx
.queryParams
['client_id'] = 'https:/127.0.10.100/';
543 await manager
._clientIdRequired(ctx
);
545 assert
.deepStrictEqual(ctx
.session
.clientIdentifier
, clientIdentifier
);
546 assert
.strictEqual(ctx
.session
.error
, undefined);
547 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
549 it('requires response', async
function () {
550 manager
.communication
.fetchClientIdentifier
.restore();
551 sinon
.stub(manager
.communication
, 'fetchClientIdentifier').resolves();
552 ctx
.queryParams
['client_id'] = 'https://client.example.com/';
554 await manager
._clientIdRequired(ctx
);
556 assert(ctx
.session
.error
);
557 assert(ctx
.session
.errorDescriptions
.length
);
559 }); // _clientIdRequired
561 describe('_redirectURIRequired', function () {
562 beforeEach(function () {
563 ctx
.session
.clientId
= new URL('https://client.example.com/');
564 ctx
.session
.clientIdentifier
= {
566 'redirect_uri': ['https://alternate.example.com/', 'https://other.example.com/'],
570 it('covers valid', function () {
571 ctx
.queryParams
['redirect_uri'] = 'https://client.example.com/return';
573 Manager
._redirectURIRequired(ctx
);
575 assert
.strictEqual(ctx
.session
.error
, undefined);
576 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
578 it('requires redirect_uri', function () {
579 ctx
.queryParams
['redirect_uri'] = undefined;
581 Manager
._redirectURIRequired(ctx
);
583 assert(ctx
.session
.error
);
584 assert(ctx
.session
.errorDescriptions
.length
);
586 it('requires valid redirect_uri', function () {
587 ctx
.queryParams
['redirect_uri'] = 'not a url';
589 Manager
._redirectURIRequired(ctx
);
591 assert(ctx
.session
.error
);
592 assert(ctx
.session
.errorDescriptions
.length
);
594 it('rejects no matching alternate redirect_uri from client_id', function () {
595 ctx
.queryParams
['redirect_uri'] = 'https://unlisted.example.com/';
597 Manager
._redirectURIRequired(ctx
);
599 assert(ctx
.session
.error
);
600 assert(ctx
.session
.errorDescriptions
.length
);
602 it('allows alternate redirect_uri from client_id', function () {
603 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/';
605 Manager
._redirectURIRequired(ctx
);
607 assert
.strictEqual(ctx
.session
.error
, undefined);
608 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
610 }); // _redirectURIRequired
612 describe('_responseTypeRequired', function () {
613 it('covers valid', function () {
614 ctx
.queryParams
['response_type'] = 'code';
616 Manager
._responseTypeRequired(ctx
);
618 assert
.strictEqual(ctx
.session
.error
, undefined);
619 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
621 it('requires response_type', function () {
622 ctx
.queryParams
['response_type'] = undefined;
624 Manager
._responseTypeRequired(ctx
);
626 assert(ctx
.session
.error
);
627 assert(ctx
.session
.errorDescriptions
.length
);
629 it('rejects invalid', function () {
630 ctx
.queryParams
['response_type'] = 'flarp';
632 Manager
._responseTypeRequired(ctx
);
634 assert(ctx
.session
.error
);
635 assert(ctx
.session
.errorDescriptions
.length
);
637 }); // _responseTypeRequired
639 describe('_stateRequired', function () {
640 it('covers valid', function () {
641 ctx
.queryParams
['state'] = 'StateStateState';
643 Manager
._stateRequired(ctx
);
645 assert
.strictEqual(ctx
.session
.error
, undefined);
646 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
648 it('requires state', function () {
649 ctx
.queryParams
['state'] = undefined;
651 Manager
._stateRequired(ctx
);
653 assert(ctx
.session
.error
);
654 assert(ctx
.session
.errorDescriptions
.length
);
656 }); // _stateRequired
658 describe('_codeChallengeMethodRequired', function () {
659 it('covers valid', function () {
660 ctx
.queryParams
['code_challenge_method'] = 'S256';
662 manager
._codeChallengeMethodRequired(ctx
);
664 assert
.strictEqual(ctx
.session
.error
, undefined);
665 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
667 it('requires code_challenge_method', function () {
668 ctx
.queryParams
['code_challenge_method'] = undefined;
670 manager
._codeChallengeMethodRequired(ctx
);
672 assert(ctx
.session
.error
);
673 assert(ctx
.session
.errorDescriptions
.length
);
675 it('rejects invalid', function () {
676 ctx
.queryParams
['code_challenge_method'] = 'MD5';
678 manager
._codeChallengeMethodRequired(ctx
);
680 assert(ctx
.session
.error
);
681 assert(ctx
.session
.errorDescriptions
.length
);
683 it('covers legacy non-PKCE', function () {
684 ctx
.queryParams
['code_challenge_method'] = undefined;
685 manager
.options
.manager
.allowLegacyNonPKCE
= true;
687 manager
._codeChallengeMethodRequired(ctx
);
689 assert
.strictEqual(ctx
.session
.error
, undefined);
690 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
692 }); // _codeChallengeMethodRequired
694 describe('_codeChallengeRequired', function () {
695 it('covers valid', function () {
696 ctx
.queryParams
['code_challenge'] = 'NBKNqs1TfjQFqpewPNOstmQ5MJnLoeTTbjqtQ9JbZOo';
698 manager
._codeChallengeRequired(ctx
);
700 assert
.strictEqual(ctx
.session
.error
, undefined);
701 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
703 it('requires code_challenge', function () {
704 ctx
.queryParams
['code_challenge'] = undefined;
706 manager
._codeChallengeRequired(ctx
);
708 assert(ctx
.session
.error
);
709 assert(ctx
.session
.errorDescriptions
.length
);
711 it('rejects invalid', function () {
712 ctx
.queryParams
['code_challenge'] = 'not base64/url encoded';
714 manager
._codeChallengeRequired(ctx
);
716 assert(ctx
.session
.error
);
717 assert(ctx
.session
.errorDescriptions
.length
);
719 it('covers legacy non-PKCE', function () {
720 ctx
.queryParams
['code_challenge'] = undefined;
721 manager
.options
.manager
.allowLegacyNonPKCE
= true;
723 manager
._codeChallengeRequired(ctx
);
725 assert
.strictEqual(ctx
.session
.error
, undefined);
726 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
729 }); // _codeChallengeRequired
731 describe('_redirectURIRequired', function () {
732 beforeEach(function () {
733 sinon
.stub(Manager
, '_setError');
734 ctx
.queryParams
['redirect_uri'] = 'https://example.com/redirect';
735 ctx
.session
.clientId
= new URL('https://example.com/');
737 it('requires redirect_uri', function () {
738 delete ctx
.queryParams
['redirect_uri'];
739 Manager
._redirectURIRequired(ctx
);
740 assert(Manager
._setError
.called
);
742 it('requires valid redirect_uri', function () {
743 ctx
.queryParams
['redirect_uri'] = 'not a uri';
744 Manager
._redirectURIRequired(ctx
);
745 assert(Manager
._setError
.called
);
747 it('sets redirectUri if no clientId', function () {
748 delete ctx
.session
.clientId
;
749 Manager
._redirectURIRequired(ctx
);
750 assert(Manager
._setError
.notCalled
);
751 assert(ctx
.session
.redirectUri
instanceof URL
);
753 it('sets redirectUri if clientId matches', function () {
754 Manager
._redirectURIRequired(ctx
);
755 assert(Manager
._setError
.notCalled
);
756 assert(ctx
.session
.redirectUri
instanceof URL
);
758 it('rejects mis-matched', function () {
759 ctx
.queryParams
['redirect_uri'] = 'https://example.com:8080/redirect';
760 Manager
._redirectURIRequired(ctx
);
761 assert(Manager
._setError
.called
);
762 assert
.strictEqual(ctx
.session
.redirectUri
, undefined);
764 it('allows client-specified alternate redirect uri', function () {
765 ctx
.session
.clientIdentifier
= {
767 'redirect_uri': ['https://alternate.example.com/redirect'],
770 ctx
.queryParams
['redirect_uri'] = 'https://alternate.example.com/redirect';
771 Manager
._redirectURIRequired(ctx
);
772 assert(Manager
._setError
.notCalled
);
773 assert(ctx
.session
.redirectUri
instanceof URL
);
775 }); // _redirectURIRequired
777 describe('_scopeOptional', function () {
778 it('covers valid', function () {
779 ctx
.queryParams
['scope'] = 'profile email';
780 manager
._scopeOptional(ctx
);
781 assert
.strictEqual(ctx
.session
.error
, undefined);
782 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
783 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
785 it('allows empty', function () {
786 ctx
.queryParams
['scope'] = undefined;
787 manager
._scopeOptional(ctx
);
788 assert
.strictEqual(ctx
.session
.error
, undefined);
789 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
790 assert
.strictEqual(ctx
.session
.scope
.length
, 0);
792 it('rejects invalid scope combination', function () {
793 ctx
.queryParams
['scope'] = 'email';
794 manager
._scopeOptional(ctx
);
795 assert(ctx
.session
.error
);
796 assert(ctx
.session
.errorDescriptions
.length
);
798 it('ignores invalid scope', function () {
799 ctx
.queryParams
['scope'] = 'profile email "funny_business"';
800 manager
._scopeOptional(ctx
);
801 assert
.strictEqual(ctx
.session
.error
, undefined);
802 assert
.deepStrictEqual(ctx
.session
.errorDescriptions
, undefined);
803 assert
.strictEqual(ctx
.session
.scope
.length
, 2);
805 }); // _scopeOptional
807 describe('_meOptional', function () {
808 this.beforeEach(function () {
809 ctx
.queryParams
['me'] = 'https://profile.example.com/';
811 it('covers valid', async
function () {
812 await manager
._meOptional(ctx
);
814 assert
.strictEqual(ctx
.session
.me
.href
, ctx
.queryParams
['me']);
816 it('ignore invalid', async
function () {
817 ctx
.queryParams
['me'] = 'not a url';
819 await manager
._meOptional(ctx
);
821 assert
.strictEqual(ctx
.session
.me
, undefined);
823 it('allows empty', async
function () {
824 ctx
.queryParams
['me'] = undefined;
826 await manager
._meOptional(ctx
);
828 assert
.strictEqual(ctx
.session
.me
, undefined);
832 describe('_profileValidForIdentifier', function () {
833 beforeEach(function () {
835 profiles: ['https://profile.example.com/', 'https://example.com/profile'],
836 me: new URL('https://example.com/profile'),
839 it('covers valid', async
function () {
841 const result
= await manager
._profileValidForIdentifier(ctx
);
843 assert
.strictEqual(result
, true);
845 it('covers missing me', async
function () {
846 delete ctx
.session
.me
;
848 const result
= await manager
._profileValidForIdentifier(ctx
);
850 assert
.strictEqual(result
, false);
852 }); // _profileValidForIdentifier
854 describe('_parseLifespan', function () {
855 let field
, customField
;
856 beforeEach(function () {
858 customField
= 'lifespan-seconds';
859 ctx
.parsedBody
['lifespan'] = undefined;
860 ctx
.parsedBody
['lifespan-seconds'] = undefined;
862 it('returns nothing without fields', function () {
863 const result
= manager
._parseLifespan(ctx
, field
, customField
);
864 assert
.strictEqual(result
, undefined);
866 it('returns nothing for unrecognized field', function () {
867 ctx
.parsedBody
['lifespan'] = 'a while';
868 const result
= manager
._parseLifespan(ctx
, field
, customField
);
869 assert
.strictEqual(result
, undefined);
871 it('returns recognized preset value', function () {
872 ctx
.parsedBody
['lifespan'] = '1d';
873 const result
= manager
._parseLifespan(ctx
, field
, customField
);
874 assert
.strictEqual(result
, 86400);
876 it('returns valid custom value', function () {
877 ctx
.parsedBody
['lifespan'] = 'custom';
878 ctx
.parsedBody
['lifespan-seconds'] = '123';
879 const result
= manager
._parseLifespan(ctx
, field
, customField
);
880 assert
.strictEqual(result
, 123);
882 it('returns nothing for invalid custom value', function () {
883 ctx
.parsedBody
['lifespan'] = 'custom';
884 ctx
.parsedBody
['lifespan-seconds'] = 'Not a number';
885 const result
= manager
._parseLifespan(ctx
, field
, customField
);
886 assert
.strictEqual(result
, undefined);
888 it('returns nothing for invalid custom value', function () {
889 ctx
.parsedBody
['lifespan'] = 'custom';
890 ctx
.parsedBody
['lifespan-seconds'] = '-50';
891 const result
= manager
._parseLifespan(ctx
, field
, customField
);
892 assert
.strictEqual(result
, undefined);
894 }); // _parseLifespan
896 describe('_parseConsentScopes', function () {
897 it('covers no scopes', function () {
898 const result
= manager
._parseConsentScopes(ctx
);
899 assert
.deepStrictEqual(result
, []);
901 it('filters invalid scopes', function () {
902 ctx
.parsedBody
['accepted_scopes'] = ['read', 'email'];
903 ctx
.parsedBody
['ad_hoc_scopes'] = 'bad"scope create ';
904 const result
= manager
._parseConsentScopes(ctx
);
905 assert
.deepStrictEqual(result
, ['read', 'create']);
907 }); // _parseConsentScopes
909 describe('_parseConsentMe', function () {
910 beforeEach(function () {
911 ctx
.session
.profiles
= ['https://me.example.com/'];
913 it('covers valid', function () {
914 const expected
= 'https://me.example.com/';
915 ctx
.parsedBody
['me'] = expected
;
916 const result
= manager
._parseConsentMe(ctx
);
918 assert
.strictEqual(result
.href
, expected
);
920 it('rejects unsupported', function () {
921 ctx
.parsedBody
['me'] = 'https://notme.example.com/';
922 const result
= manager
._parseConsentMe(ctx
);
924 assert(ctx
.session
.error
);
926 it('rejects invalid', function () {
927 ctx
.parsedBody
['me'] = 'bagel';
928 const result
= manager
._parseConsentMe(ctx
);
930 assert(ctx
.session
.error
);
932 }); // _parseConsentMe
934 describe('_fetchConsentProfileData', function () {
936 beforeEach(function () {
938 url: 'https://profile.example.com/',
940 manager
.communication
.fetchProfile
.resolves(profileResponse
);
942 it('covers success', async
function () {
943 const expected
= profileResponse
;
944 const result
= await manager
._fetchConsentProfileData(ctx
);
945 assert
.deepStrictEqual(result
, expected
);
946 assert(!ctx
.session
.error
);
948 it('covers empty response', async
function () {
949 manager
.communication
.fetchProfile
.resolves();
950 const result
= await manager
._fetchConsentProfileData(ctx
);
951 assert
.deepStrictEqual(result
, undefined);
952 assert(ctx
.session
.error
);
954 it('covers failure', async
function () {
955 manager
.communication
.fetchProfile
.rejects();
956 const result
= await manager
._fetchConsentProfileData(ctx
);
957 assert
.deepStrictEqual(result
, undefined);
958 assert(ctx
.session
.error
);
960 }); // _fetchConsentProfileData
962 describe('postConsent', function () {
964 beforeEach(function () {
965 sinon
.stub(manager
.mysteryBox
, 'unpack');
966 sinon
.stub(manager
.mysteryBox
, 'pack');
967 manager
.communication
.fetchProfile
.resolves({
968 url: 'https://profile.example.com/',
971 clientId: 'https://example.com/',
972 redirectUri: 'https://example.com/_redirect',
973 profiles: ['https://profile.example.com/'],
975 manager
.mysteryBox
.unpack
.resolves(oldSession
);
976 ctx
.parsedBody
['me'] = 'https://profile.example.com/';
977 ctx
.parsedBody
['accept'] = 'true';
979 it('covers valid', async
function () {
980 await manager
.postConsent(res
, ctx
);
981 assert(!ctx
.session
.error
, ctx
.session
.error
);
982 assert
.strictEqual(res
.statusCode
, 302);
984 it('covers valid with expiration and refresh', async
function () {
985 ctx
.parsedBody
['expires'] = '1d';
986 ctx
.parsedBody
['refresh'] = '1w';
987 await manager
.postConsent(res
, ctx
);
988 assert(!ctx
.session
.error
, ctx
.session
.error
);
989 assert
.strictEqual(res
.statusCode
, 302);
991 it('covers denial', async
function () {
992 ctx
.parsedBody
['accept'] = 'false';
993 await manager
.postConsent(res
, ctx
);
994 assert(ctx
.session
.error
);
995 assert
.strictEqual(ctx
.session
.error
, 'access_denied');
996 assert
.strictEqual(res
.statusCode
, 302);
998 it('covers profile fetch failure', async
function () {
999 manager
.communication
.fetchProfile
.resolves();
1000 await manager
.postConsent(res
, ctx
);
1001 assert
.strictEqual(res
.statusCode
, 302);
1002 assert(ctx
.session
.error
);
1004 it('covers bad code', async
function () {
1005 manager
.mysteryBox
.unpack
.rejects();
1006 await manager
.postConsent(res
, ctx
);
1007 assert
.strictEqual(res
.statusCode
, 400);
1008 assert(ctx
.session
.error
);
1010 it('removes email scope without profile', async
function () {
1011 ctx
.parsedBody
['accepted_scopes'] = ['email', 'create'];
1012 await manager
.postConsent(res
, ctx
);
1013 assert(!ctx
.session
.acceptedScopes
.includes('email'));
1015 it('merges valid ad-hoc scopes', async
function () {
1016 ctx
.parsedBody
['accepted_scopes'] = ['email', 'create'];
1017 ctx
.parsedBody
['ad_hoc_scopes'] = ' my:scope "badScope';
1018 await manager
.postConsent(res
, ctx
);
1019 assert(ctx
.session
.acceptedScopes
.includes('my:scope'));
1021 it('covers invalid selected me profile', async
function () {
1022 ctx
.parsedBody
['me'] = 'https://different.example.com/';
1023 await manager
.postConsent(res
, ctx
);
1024 assert(ctx
.session
.error
);
1026 it('covers invalid me url', async
function () {
1027 ctx
.parsedBody
['me'] = 'bagel';
1028 await manager
.postConsent(res
, ctx
);
1029 assert(ctx
.session
.error
);
1031 it('covers profile fetch error', async
function () {
1032 manager
.communication
.fetchProfile
.rejects(expectedException
);
1033 await manager
.postConsent(res
, ctx
);
1034 assert
.strictEqual(res
.statusCode
, 302);
1035 assert(ctx
.session
.error
);
1039 describe('postAuthorization', function () {
1040 let code
, parsedBody
;
1041 beforeEach(function () {
1042 sinon
.stub(manager
.mysteryBox
, 'unpack');
1044 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1045 codeChallengeMethod: 'S256',
1046 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1047 clientId: 'https://app.example.com/',
1048 redirectUri: 'https://app.example.com/_redirect',
1049 acceptedScopes: ['profile'],
1051 me: 'https://client.example.com/',
1052 identifier: 'username',
1054 name: 'Firsty McLastname',
1055 email: 'f.mclastname@example.com',
1059 code: 'codeCodeCode',
1060 'client_id': 'https://app.example.com/',
1061 'redirect_uri': 'https://app.example.com/_redirect',
1062 'grant_type': 'authorization_code',
1063 'code_verifier': 'verifier',
1066 it('covers valid', async
function () {
1067 manager
.db
.redeemCode
.resolves(true);
1068 manager
.mysteryBox
.unpack
.resolves(code
);
1069 Object
.assign(ctx
.parsedBody
, parsedBody
);
1071 await manager
.postAuthorization(res
, ctx
);
1072 assert(!ctx
.session
.error
, ctx
.session
.error
);
1073 assert(!res
.end
.firstCall
.args
[0].includes('email'));
1075 it('includes email if accepted in scope', async
function () {
1076 code
.acceptedScopes
= ['profile', 'email'];
1077 manager
.db
.redeemCode
.resolves(true);
1078 manager
.mysteryBox
.unpack
.resolves(code
);
1079 Object
.assign(ctx
.parsedBody
, parsedBody
);
1081 await manager
.postAuthorization(res
, ctx
);
1082 assert(!ctx
.session
.error
);
1083 assert(res
.end
.firstCall
.args
[0].includes('email'));
1085 it('fails if already redeemed', async
function () {
1086 manager
.db
.redeemCode
.resolves(false);
1087 manager
.mysteryBox
.unpack
.resolves(code
);
1088 Object
.assign(ctx
.parsedBody
, parsedBody
);
1090 await manager
.postAuthorization(res
, ctx
);
1091 assert(ctx
.session
.error
);
1093 it('covers bad request', async
function () {
1094 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1095 Object
.assign(ctx
.parsedBody
, parsedBody
);
1097 await manager
.postAuthorization(res
, ctx
);
1098 assert(ctx
.session
.error
);
1100 }); // postAuthorization
1102 describe('_ingestPostAuthorizationRequest', function () {
1103 beforeEach(function () {
1104 sinon
.stub(manager
, '_restoreSessionFromCode');
1105 sinon
.stub(manager
, '_checkSessionMatchingClientId');
1106 sinon
.stub(manager
, '_checkSessionMatchingRedirectUri');
1107 sinon
.stub(manager
, '_checkGrantType');
1108 sinon
.stub(manager
, '_checkSessionMatchingCodeVerifier');
1110 it('covers valid', async
function () {
1111 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1113 me: 'https://profile.example.com/',
1118 await manager
._ingestPostAuthorizationRequest(ctx
);
1119 assert(!ctx
.session
.error
);
1121 it('requires data', async
function () {
1122 delete ctx
.parsedBody
;
1123 await manager
._ingestPostAuthorizationRequest(ctx
);
1124 assert(ctx
.session
.error
);
1126 it('requires me field', async
function () {
1127 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1132 await manager
._ingestPostAuthorizationRequest(ctx
);
1133 assert(ctx
.session
.error
);
1135 it('requires minted field', async
function () {
1136 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1138 me: 'https://profile.example.com/',
1141 await manager
._ingestPostAuthorizationRequest(ctx
);
1142 assert(ctx
.session
.error
);
1144 it('rejects expired code', async
function () {
1145 manager
._restoreSessionFromCode
.callsFake((ctx
) => {
1147 me: 'https://profile.example.com/',
1148 minted: Date
.now() - 86400000,
1152 await manager
._ingestPostAuthorizationRequest(ctx
);
1153 assert(ctx
.session
.error
);
1155 }); // _ingestPostAuthorizationRequest
1157 describe('_restoreSessionFromCode', function () {
1159 beforeEach(function () {
1160 sinon
.stub(manager
.mysteryBox
, 'unpack');
1162 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1163 codeChallengeMethod: 'S256',
1164 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1165 clientId: 'https://app.example.com/',
1166 redirectUri: 'https://app.example.com/_redirect',
1167 acceptedScopes: ['profile'],
1169 me: 'https://client.example.com/',
1170 identifier: 'username',
1172 name: 'Firsty McLastname',
1173 email: 'f.mclastname@example.com',
1177 it('covers valid', async
function () {
1178 ctx
.parsedBody
['code'] = 'codeCodeCode';
1179 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1180 const expected
= Object
.assign({}, ctx
, {
1181 session: unpackedCode
,
1183 await manager
._restoreSessionFromCode(ctx
);
1184 assert
.deepStrictEqual(ctx
, expected
);
1185 assert(!ctx
.session
.error
);
1187 it('requires code', async
function () {
1188 ctx
.parsedBody
['code'] = '';
1189 manager
.mysteryBox
.unpack
.resolves({
1190 me: 'https://example.com/me',
1192 await manager
._restoreSessionFromCode(ctx
);
1193 assert(ctx
.session
.error
);
1195 it('covers invalid code', async
function () {
1196 ctx
.parsedBody
['code'] = 'codeCodeCode';
1197 manager
.mysteryBox
.unpack
.rejects();
1198 await manager
._restoreSessionFromCode(ctx
);
1199 assert(ctx
.session
.error
);
1201 it('covers missing code fields', async
function () {
1202 ctx
.parsedBody
['code'] = 'codeCodeCode';
1203 delete unpackedCode
.clientId
;
1204 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1205 await manager
._restoreSessionFromCode(ctx
);
1206 assert(ctx
.session
.error
);
1208 it('covers legacy non-PKCE missing fields', async
function () {
1209 ctx
.parsedBody
['code'] = 'codeCodeCode';
1210 delete unpackedCode
.codeChallengeMethod
;
1211 delete unpackedCode
.codeChallenge
;
1212 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1213 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1214 const expected
= Object
.assign({}, ctx
, {
1215 session: unpackedCode
,
1217 await manager
._restoreSessionFromCode(ctx
);
1218 assert
.deepStrictEqual(ctx
, expected
);
1219 assert(!ctx
.session
.error
);
1221 }); // _restoreSessionFromCode
1223 describe('_checkSessionMatchingClientId', function () {
1224 it('covers valid', async
function () {
1226 clientId: 'https://client.example.com/',
1228 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1230 manager
._checkSessionMatchingClientId(ctx
);
1231 assert(!ctx
.session
.error
);
1233 it('covers missing', async
function () {
1235 clientId: 'https://client.example.com/',
1237 ctx
.parsedBody
['client_id'] = undefined;
1239 manager
._checkSessionMatchingClientId(ctx
);
1240 assert(ctx
.session
.error
);
1242 it('covers un-parsable', async
function () {
1244 clientId: 'https://client.example.com/',
1246 ctx
.parsedBody
['client_id'] = 'not a url';
1248 manager
._checkSessionMatchingClientId(ctx
);
1249 assert(ctx
.session
.error
);
1251 it('covers mismatch', async
function () {
1253 clientId: 'https://client.example.com/',
1255 ctx
.parsedBody
['client_id'] = 'https://otherclient.example.com/';
1257 manager
._checkSessionMatchingClientId(ctx
);
1258 assert(ctx
.session
.error
);
1260 }); // _checkSessionMatchingClientId
1262 describe('_checkSessionMatchingRedirectUri', function () {
1263 it('covers valid', async
function () {
1264 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/_redirect';
1265 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1267 manager
._checkSessionMatchingRedirectUri(ctx
);
1268 assert(!ctx
.session
.error
);
1270 it('requires field', async
function () {
1271 ctx
.parsedBody
['redirect_uri'] = undefined;
1272 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1274 manager
._checkSessionMatchingRedirectUri(ctx
);
1275 assert(ctx
.session
.error
);
1277 it('requires valid field', async
function () {
1278 ctx
.parsedBody
['redirect_uri'] = 'not a url';
1279 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1281 manager
._checkSessionMatchingRedirectUri(ctx
);
1282 assert(ctx
.session
.error
);
1284 it('requires match', async
function () {
1285 ctx
.parsedBody
['redirect_uri'] = 'https://client.example.com/other';
1286 ctx
.session
.redirectUri
= 'https://client.example.com/_redirect';
1288 manager
._checkSessionMatchingRedirectUri(ctx
);
1289 assert(ctx
.session
.error
);
1291 }); // _checkSessionMatchingRedirectUri
1293 describe('_checkGrantType', function () {
1294 it('covers valid', async
function () {
1295 ctx
.parsedBody
['grant_type'] = 'authorization_code';
1297 manager
._checkGrantType(ctx
);
1298 assert(!ctx
.session
.error
);
1300 it('allows missing, because of one client', async
function () {
1301 ctx
.parsedBody
['grant_type'] = undefined;
1303 manager
._checkGrantType(ctx
);
1304 assert(!ctx
.session
.error
);
1306 it('rejects invalid', async
function () {
1307 ctx
.parsedBody
['grant_type'] = 'pigeon_dance';
1309 manager
._checkGrantType(ctx
);
1310 assert(ctx
.session
.error
);
1312 }); // _checkGrantType
1314 describe('_checkSessionMatchingCodeVerifier', function () {
1315 it('covers valid', async
function () {
1316 ctx
.parsedBody
['code_verifier'] = 'verifier';
1317 ctx
.session
.codeChallengeMethod
= 'S256';
1318 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1320 manager
._checkSessionMatchingCodeVerifier(ctx
);
1321 assert(!ctx
.session
.error
);
1323 it('requires field', async
function () {
1324 ctx
.parsedBody
['code_verifier'] = undefined;
1325 ctx
.session
.codeChallengeMethod
= 'S256';
1326 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1328 manager
._checkSessionMatchingCodeVerifier(ctx
);
1329 assert(ctx
.session
.error
);
1331 it('requires match', async
function () {
1332 ctx
.parsedBody
['code_verifier'] = 'wrongverifier';
1333 ctx
.session
.codeChallengeMethod
= 'S256';
1334 ctx
.session
.codeChallenge
= 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ';
1336 manager
._checkSessionMatchingCodeVerifier(ctx
);
1337 assert(ctx
.session
.error
);
1339 it('covers legacy non-PKCE missing fields', async
function () {
1340 ctx
.parsedBody
['code_verifier'] = undefined;
1341 ctx
.session
.codeChallengeMethod
= undefined;
1342 ctx
.session
.codeChallenge
= undefined;
1343 manager
.options
.manager
.allowLegacyNonPKCE
= true;
1345 manager
._checkSessionMatchingCodeVerifier(ctx
);
1346 assert(!ctx
.session
.error
);
1348 }); // _checkSessionMatchingCodeVerifier
1350 describe('postToken', function () {
1352 beforeEach(function () {
1353 ctx
.session
.acceptedScopes
= [];
1355 codeId: 'cffe1558-35f0-11ec-98bc-0025905f714a',
1356 codeChallengeMethod: 'S256',
1357 codeChallenge: 'iMnq5o6zALKXGivsnlom_0F5_WYda32GHkxlV7mq7hQ',
1358 clientId: 'https://app.example.com/',
1359 redirectUri: 'https://app.example.com/return',
1360 acceptedScopes: ['profile', 'email', 'tricks'],
1362 me: 'https://client.example.com/',
1363 identifier: 'username',
1365 name: 'Firsty McLastname',
1366 email: 'f.mclastname@example.com',
1367 url: 'https://example.com/',
1371 describe('Revocation (legacy)', function () {
1372 beforeEach(function () {
1373 sinon
.stub(manager
, '_revokeToken');
1375 it('covers revocation', async
function () {
1376 manager
._revokeToken
.resolves();
1381 await manager
.postToken(req
, res
, ctx
);
1382 assert(manager
._revokeToken
.called
);
1385 describe('Validation (legacy)', function () {
1386 beforeEach(function () {
1387 sinon
.stub(manager
, '_validateToken');
1388 req
.getHeader
.returns({ Authorization: 'Bearer XXX' });
1390 it('covers validation', async
function () {
1391 ctx
.bearer
= { isValid: true };
1392 await manager
.postToken(req
, res
, ctx
);
1393 assert(manager
._validateToken
.called
);
1396 describe('Refresh', function () {
1397 beforeEach(function () {
1398 sinon
.stub(manager
, '_refreshToken');
1400 it('covers refresh', async
function () {
1401 ctx
.parsedBody
['grant_type'] = 'refresh_token';
1402 await manager
.postToken(req
, res
, ctx
);
1403 assert(manager
._refreshToken
.called
);
1406 describe('Ticket Redemption', function () {
1407 beforeEach(function () {
1408 sinon
.stub(manager
, '_ticketAuthToken');
1410 it('covers ticket', async
function () {
1411 ctx
.parsedBody
['grant_type'] = 'ticket';
1412 await manager
.postToken(req
, res
, ctx
);
1413 assert(manager
._ticketAuthToken
.called
);
1415 it('covers no ticket queue', async
function () {
1416 delete options
.queues
.amqp
.url
;
1417 manager
= new Manager(logger
, stubDb
, options
);
1418 sinon
.stub(manager
.communication
, 'fetchProfile');
1419 sinon
.stub(manager
.communication
, 'fetchClientIdentifier');
1420 sinon
.stub(manager
.communication
, 'deliverTicket');
1422 ctx
.parsedBody
['grant_type'] = 'ticket';
1423 await assert
.rejects(() => manager
.postToken(req
, res
, ctx
), ResponseError
);
1425 }); // Ticket Redemption
1426 describe('Code Redemption', function () {
1427 beforeEach(function () {
1428 sinon
.stub(manager
.mysteryBox
, 'unpack');
1429 sinon
.spy(manager
.mysteryBox
, 'pack');
1430 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1432 'redirect_uri': 'https://app.example.com/return',
1436 it('covers invalid code', async
function () {
1437 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1439 await manager
.postToken(req
, res
, ctx
);
1440 assert
.fail(noExpectedException
);
1442 assert(e
instanceof ResponseError
);
1445 it('covers mismatched redirect', async
function () {
1446 ctx
.parsedBody
['redirect_uri'] = 'https://elsewhere.example.com/';
1448 await manager
.postToken(req
, res
, ctx
);
1449 assert
.fail(noExpectedException
);
1451 assert(e
instanceof ResponseError
);
1454 it('covers success', async
function () {
1455 manager
.db
.redeemCode
.resolves(true);
1456 await manager
.postToken(req
, res
, ctx
);
1457 assert(res
.end
.called
);
1458 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 1);
1460 it('covers success with refresh', async
function () {
1461 manager
.db
.redeemCode
.resolves(true);
1462 unpackedCode
.refreshLifespan
= 86400;
1463 unpackedCode
.tokenLifespan
= 86400;
1464 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1465 await manager
.postToken(req
, res
, ctx
);
1466 assert(res
.end
.called
);
1467 assert
.strictEqual(manager
.mysteryBox
.pack
.callCount
, 2);
1469 it('covers redemption failure', async
function () {
1470 manager
.db
.redeemCode
.resolves(false);
1472 await manager
.postToken(req
, res
, ctx
);
1473 assert
.fail(noExpectedException
);
1475 assert(e
instanceof ResponseError
);
1478 it('removes email from profile if not in scope', async
function () {
1479 manager
.db
.redeemCode
.resolves(true);
1480 unpackedCode
.acceptedScopes
= ['profile', 'tricks'];
1481 manager
.mysteryBox
.unpack
.resolves(unpackedCode
);
1482 await manager
.postToken(req
, res
, ctx
);
1483 assert(res
.end
.called
);
1484 const response
= JSON
.parse(res
.end
.args
[0][0]);
1485 assert(!('email' in response
.profile
));
1488 }); // Code Redemption
1489 describe('Invalid grant_type', function () {
1490 it('throws response error', async
function () {
1491 ctx
.parsedBody
['grant_type'] = 'bad';
1493 await manager
.postToken(req
, res
, ctx
);
1494 assert
.fail(noExpectedException
);
1496 assert(e
instanceof ResponseError
);
1499 }); // Invalid grant_type
1502 describe('_validateToken', function () {
1504 beforeEach(function () {
1506 sinon
.stub(manager
, '_checkTokenValidationRequest');
1508 it('covers valid token', async
function () {
1514 await manager
._validateToken(dbCtx
, req
, res
, ctx
);
1515 assert(res
.end
.called
);
1517 it('covers invalid token', async
function () {
1521 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1523 it('covers errors', async
function () {
1527 ctx
.session
.error
= 'error';
1528 ctx
.session
.errorDescriptions
= ['error_description'];
1529 await assert
.rejects(manager
._validateToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1531 }); // _validateToken
1533 describe('_checkTokenValidationRequest', function () {
1535 beforeEach(function () {
1537 sinon
.stub(manager
.mysteryBox
, 'unpack');
1539 it('does nothing with no auth header', async
function () {
1540 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1542 it('does nothing with unknown auth header', async
function () {
1543 req
.getHeader
.returns('flarp authy woo');
1544 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1546 it('requires a valid auth token', async
function () {
1547 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1548 req
.getHeader
.returns('Bearer XXX');
1549 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1550 assert(ctx
.session
.error
);
1552 it('requires valid auth token fields', async
function () {
1553 manager
.mysteryBox
.unpack
.resolves({});
1554 req
.getHeader
.returns('Bearer XXX');
1555 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1556 assert(ctx
.session
.error
)
1558 it('covers no token', async
function () {
1559 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1560 req
.getHeader
.returns('Bearer XXX');
1561 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1562 assert(ctx
.session
.error
)
1564 it('covers db error', async
function () {
1565 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1566 manager
.db
.tokenGetByCodeId
.rejects(expectedException
);
1567 req
.getHeader
.returns('Bearer XXX');
1568 await assert
.rejects(manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
), expectedException
);
1570 it('valid token', async
function () {
1571 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1572 manager
.db
.tokenGetByCodeId
.resolves({
1574 expires: new Date(Date
.now() + 86400000),
1576 req
.getHeader
.returns('Bearer XXX');
1577 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1578 assert
.strictEqual(ctx
.bearer
.isValid
, true);
1580 it('revoked token', async
function () {
1581 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1582 manager
.db
.tokenGetByCodeId
.resolves({
1584 expires: new Date(Date
.now() + 86400000),
1586 req
.getHeader
.returns('Bearer XXX');
1587 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1588 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1590 it('expired token', async
function () {
1591 manager
.mysteryBox
.unpack
.resolves({ c: 'xxx' });
1592 manager
.db
.tokenGetByCodeId
.resolves({
1594 expires: new Date(Date
.now() - 86400000),
1596 req
.getHeader
.returns('Bearer XXX');
1597 await manager
._checkTokenValidationRequest(dbCtx
, req
, ctx
);
1598 assert
.strictEqual(ctx
.bearer
.isValid
, false);
1600 }); // _checkTokenValidationRequest
1602 describe('postIntrospection', function () {
1603 let inactiveToken
, activeToken
, dbResponse
;
1604 beforeEach(function () {
1606 profile: 'https://profile.example.com/',
1607 clientId: 'https://client.example.com/',
1608 scopes: ['scope1', 'scope2'],
1609 created: new Date(),
1612 inactiveToken
= JSON
.stringify({
1615 activeToken
= JSON
.stringify({
1617 me: dbResponse
.profile
,
1618 'client_id': dbResponse
.clientId
,
1619 scope: dbResponse
.scopes
.join(' '),
1620 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1622 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '7e9991dc-9cd5-11ec-85c4-0025905f714a' });
1623 manager
.db
.tokenGetByCodeId
.resolves(dbResponse
);
1625 it('covers bad token', async
function () {
1626 manager
.mysteryBox
.unpack
.rejects();
1627 await manager
.postIntrospection(res
, ctx
);
1628 assert(res
.end
.called
);
1629 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1631 it('covers token not in db', async
function () {
1632 manager
.db
.tokenGetByCodeId
.resolves();
1633 await manager
.postIntrospection(res
, ctx
);
1634 assert(res
.end
.called
);
1635 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1637 it('covers valid token', async
function () {
1638 await manager
.postIntrospection(res
, ctx
);
1639 assert(res
.end
.called
);
1640 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1642 it('covers expired token', async
function () {
1643 dbResponse
.expires
= new Date((new Date()).getTime() - 86400000);
1644 await manager
.postIntrospection(res
, ctx
);
1645 assert(res
.end
.called
);
1646 assert
.strictEqual(res
.end
.args
[0][0], inactiveToken
);
1648 it('covers expiring token', async
function () {
1649 dbResponse
.expires
= new Date((new Date()).getTime() + 86400000);
1650 activeToken
= JSON
.stringify({
1652 me: dbResponse
.profile
,
1653 'client_id': dbResponse
.clientId
,
1654 scope: dbResponse
.scopes
.join(' '),
1655 iat: Math
.ceil(dbResponse
.created
.getTime() / 1000),
1656 exp: Math
.ceil(dbResponse
.expires
/ 1000),
1658 await manager
.postIntrospection(res
, ctx
);
1659 assert(res
.end
.called
);
1660 assert
.strictEqual(res
.end
.args
[0][0], activeToken
);
1662 it('covers ticket', async
function () {
1663 ctx
.parsedBody
['token_hint_type'] = 'ticket';
1664 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1665 manager
.mysteryBox
.unpack
.resolves({
1666 c: '515172ae-5b0b-11ed-a6af-0025905f714a',
1667 iss: nowEpoch
- 86400,
1668 exp: nowEpoch
+ 86400,
1669 sub: 'https://subject.exmaple.com/',
1670 res: 'https://profile.example.com/feed',
1671 scope: ['read', 'role:private'],
1673 profile: 'https://profile.example.com/',
1675 await manager
.postIntrospection(res
, ctx
);
1676 assert(res
.end
.called
);
1678 }); // postIntrospection
1680 describe('_revokeToken', function () {
1682 beforeEach(function () {
1685 it('requires token field', async
function () {
1686 await manager
._revokeToken(dbCtx
, res
, ctx
);
1687 assert(res
.end
.called
);
1688 assert
.strictEqual(res
.statusCode
, 400);
1690 it('requires parsable token', async
function () {
1691 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ notC: 'foop' });
1692 ctx
.parsedBody
['token'] = 'invalid token';
1693 ctx
.parsedBody
['token_type_hint'] = 'access_token';
1694 await manager
._revokeToken(dbCtx
, res
, ctx
);
1695 assert(res
.end
.called
);
1696 assert
.strictEqual(res
.statusCode
, 400);
1698 it('requires parsable token', async
function () {
1699 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves();
1700 ctx
.parsedBody
['token'] = 'invalid token';
1701 ctx
.parsedBody
['token_type_hint'] = 'refresh_token';
1702 await manager
._revokeToken(dbCtx
, res
, ctx
);
1703 assert(res
.end
.called
);
1704 assert
.strictEqual(res
.statusCode
, 400);
1706 it('succeeds', async
function () {
1707 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1708 ctx
.parsedBody
['token'] = 'valid token';
1709 await manager
._revokeToken(dbCtx
, res
, ctx
);
1710 assert(manager
.db
.tokenRevokeByCodeId
.called
);
1711 assert(res
.end
.called
);
1713 it('succeeds for refresh token', async
function () {
1714 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ rc: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1715 ctx
.parsedBody
['token'] = 'valid token';
1716 await manager
._revokeToken(dbCtx
, res
, ctx
);
1717 assert(manager
.db
.tokenRefreshRevokeByCodeId
.called
);
1718 assert(res
.end
.called
);
1720 it('covers non-revokable token', async
function () {
1721 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1722 manager
.db
.tokenRevokeByCodeId
.rejects(new UnexpectedResult());
1723 ctx
.parsedBody
['token'] = 'valid token';
1724 await manager
._revokeToken(dbCtx
, res
, ctx
);
1725 assert
.strictEqual(res
.statusCode
, 404);
1727 it('covers failure', async
function () {
1728 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({ c: '8e4aed9e-fa3e-11ec-992e-0025905f714a' });
1729 manager
.db
.tokenRevokeByCodeId
.rejects(expectedException
);
1730 ctx
.parsedBody
['token'] = 'valid token';
1731 ctx
.parsedBody
['token_type_hint'] = 'ignores_bad_hint';
1732 await assert
.rejects(manager
._revokeToken(dbCtx
, res
, ctx
), expectedException
, noExpectedException
);
1736 describe('_scopeDifference', function () {
1737 let previousScopes
, requestedScopes
;
1738 beforeEach(function () {
1739 previousScopes
= ['a', 'b', 'c'];
1740 requestedScopes
= ['b', 'c', 'd'];
1742 it('covers', function () {
1743 const expected
= ['a'];
1744 const result
= Manager
._scopeDifference(previousScopes
, requestedScopes
);
1745 assert
.deepStrictEqual(result
, expected
);
1747 }); // _scopeDifference
1749 describe('_refreshToken', function () {
1751 beforeEach(function () {
1753 ctx
.parsedBody
['client_id'] = 'https://client.example.com/';
1754 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1755 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves({
1756 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1757 ts: nowEpoch
- 86400,
1758 exp: nowEpoch
+ 86400,
1760 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('newToken');
1761 const futureDate
= new Date(Date
.now() + 86400000);
1762 manager
.db
.tokenGetByCodeId
.resolves({
1763 refreshExpires: futureDate
,
1765 clientId: 'https://client.example.com/',
1766 scopes: ['profile', 'create'],
1768 manager
.db
.refreshCode
.resolves({
1769 expires: futureDate
,
1770 refreshExpires: futureDate
,
1773 it('requires a token', async
function () {
1774 manager
.mysteryBox
.unpack
.rejects();
1775 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1777 it('requires token to have refresh field', async
function () {
1778 manager
.mysteryBox
.unpack
.resolves();
1779 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1781 it('requires token to exist in db', async
function () {
1782 manager
.db
.tokenGetByCodeId
.resolves();
1783 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1785 it('requires token be refreshable', async
function () {
1786 manager
.db
.tokenGetByCodeId
.resolves({
1787 refreshExpires: undefined,
1789 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1791 it('requires refresh of token not be expired', async
function () {
1792 manager
.db
.tokenGetByCodeId
.resolves({
1793 refreshExpires: 1000,
1795 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1797 it('requires token not to have been already refreshed', async
function () {
1798 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
1799 manager
.mysteryBox
.unpack
.resolves({
1800 rc: '03bb8ab0-1dc7-11ed-99f2-0025905f714a',
1801 ts: nowEpoch
- 864000,
1802 exp: nowEpoch
- 86400,
1804 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1806 it('requires client_id requesting refresh match', async
function () {
1807 ctx
.parsedBody
['client_id'] = 'https://wrong.example.com/';
1808 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1810 it('succeeds', async
function () {
1811 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1812 assert(res
.end
.called
);
1814 it('covers non-expiring', async
function () {
1815 manager
.db
.tokenGetByCodeId
.resolves({
1816 refreshExpires: new Date(Date
.now() + 86400000),
1818 clientId: 'https://client.example.com/',
1819 scopes: ['profile', 'create'],
1821 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1822 assert(res
.end
.called
);
1824 it('covers profile and email', async
function () {
1825 manager
.db
.tokenGetByCodeId
.resolves({
1826 refreshExpires: new Date(Date
.now() + 86400000),
1828 clientId: 'https://client.example.com/',
1829 scopes: ['profile', 'email', 'create'],
1831 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1832 assert(res
.end
.called
);
1834 it('succeeds with scope reduction', async
function () {
1835 ctx
.parsedBody
['scope'] = 'profile fancy';
1836 manager
.db
.tokenGetByCodeId
.resolves({
1837 refreshExpires: new Date(Date
.now() + 86400000),
1838 clientId: 'https://client.example.com/',
1839 scopes: ['profile', 'create'],
1841 await manager
._refreshToken(dbCtx
, req
, res
, ctx
);
1842 assert(res
.end
.called
);
1844 it('covers refresh failed', async
function () {
1845 manager
.db
.refreshCode
.resolves();
1846 await assert
.rejects(() => manager
._refreshToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1848 }); // _refreshToken
1850 describe('_mintTicket', function () {
1852 beforeEach(function () {
1855 subject: 'https://third-party.example.com/',
1856 resource: 'https://private.example.com/feed',
1858 identifier: 'account',
1859 profile: 'https://profile.example.com/',
1860 ticketLifespanSeconds: 86400,
1863 it('covers', async
function () {
1864 const expected
= 'xxx';
1865 sinon
.stub(manager
.mysteryBox
, 'pack').resolves(expected
);
1866 const result
= await manager
._mintTicket(dbCtx
, payload
);
1867 assert
.strictEqual(result
, expected
);
1871 describe('_ticketAuthToken', function () {
1872 let dbCtx
, ticketPayload
, nowEpoch
;
1873 beforeEach(function () {
1875 nowEpoch
= Math
.ceil(Date
.now() / 1000);
1877 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1878 iss: nowEpoch
- 86400,
1879 exp: nowEpoch
+ 86400,
1880 sub: 'https://third-party.example.com/',
1881 res: 'https://private.example.com/feed',
1882 scope: ['read', 'flap'],
1884 profile: 'https://profile.example.com/',
1886 sinon
.stub(manager
.mysteryBox
, 'unpack').resolves(ticketPayload
);
1887 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
1889 it('covers invalid ticket', async
function () {
1890 manager
.mysteryBox
.unpack
.resolves();
1891 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1893 it('covers expired ticket', async
function () {
1894 manager
.mysteryBox
.unpack
.resolves({
1895 c: '5ec17f78-5576-11ed-b444-0025905f714a',
1896 iss: nowEpoch
- 172800,
1897 exp: nowEpoch
- 86400,
1898 sub: 'https://third-party.example.com/',
1899 res: 'https://private.example.com/feed',
1900 scope: ['read', 'flap'],
1902 profile: 'https://profile.example.com/',
1904 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1906 it('covers success', async
function () {
1907 manager
.db
.redeemCode
.resolves(true);
1908 await manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
);
1909 assert(res
.end
.called
);
1911 it('covers invalid redeem', async
function () {
1912 manager
.db
.redeemCode
.resolves(false);
1913 await assert
.rejects(() => manager
._ticketAuthToken(dbCtx
, req
, res
, ctx
), ResponseError
);
1915 }); // _ticketAuthToken
1917 describe('postRevocation', function () {
1918 beforeEach(function () {
1919 sinon
.stub(manager
, '_revokeToken');
1921 it('covers success', async
function () {
1922 manager
._revokeToken
.resolves();
1923 await manager
.postRevocation(res
, ctx
);
1924 assert(manager
._revokeToken
.called
);
1926 it('covers failure', async
function () {
1927 manager
._revokeToken
.rejects(expectedException
);
1928 await assert
.rejects(manager
.postRevocation(res
, ctx
));
1930 }); // postRevocation
1932 describe('postUserInfo', function () {
1933 beforeEach(function () {
1934 ctx
.parsedBody
['token'] = 'XXX';
1935 sinon
.stub(manager
.mysteryBox
, 'unpack');
1937 it('requires a token', async
function () {
1938 delete ctx
.parsedBody
.token
;
1939 await manager
.postUserInfo(res
, ctx
);
1940 assert(res
.end
.called
);
1941 assert
.strictEqual(res
.statusCode
, 400);
1943 it('requires a valid token', async
function () {
1944 manager
.mysteryBox
.unpack
.rejects(expectedException
);
1945 await manager
.postUserInfo(res
, ctx
);
1946 assert(res
.end
.called
);
1947 assert
.strictEqual(res
.statusCode
, 401);
1949 it('requires token to have profile scope', async
function () {
1950 manager
.mysteryBox
.unpack
.resolves({});
1951 manager
.db
.tokenGetByCodeId
.resolves({
1954 await manager
.postUserInfo(res
, ctx
);
1955 assert(res
.end
.called
);
1956 assert
.strictEqual(res
.statusCode
, 403);
1958 it('succeeds', async
function () {
1959 manager
.mysteryBox
.unpack
.resolves({});
1960 manager
.db
.tokenGetByCodeId
.resolves({
1961 scopes: ['profile', 'email'],
1963 url: 'https://example.com/',
1964 email: 'user@example.com',
1967 await manager
.postUserInfo(res
, ctx
);
1968 assert(res
.end
.called
);
1969 assert
.strictEqual(res
.statusCode
, 200);
1971 it('succeeds, and does not include email without scope', async
function () {
1972 manager
.mysteryBox
.unpack
.resolves({});
1973 manager
.db
.tokenGetByCodeId
.resolves({
1974 scopes: ['profile'],
1976 url: 'https://example.com/',
1977 email: 'user@example.com',
1980 await manager
.postUserInfo(res
, ctx
);
1981 assert(res
.end
.called
);
1982 assert
.strictEqual(res
.statusCode
, 200);
1983 const response
= JSON
.parse(res
.end
.args
[0][0]);
1984 assert(!('email' in response
));
1988 describe('getAdmin', function () {
1989 beforeEach(function () {
1990 manager
.db
.profilesScopesByIdentifier
.resolves({
1992 'https://profile.example.com/': {
1995 description: 'a scope',
1996 profiles: ['https://profile.example.com/'],
2003 description: 'a scope',
2004 profiles: ['https://profile.example.com/'],
2007 profiles: ['https://profile.example.com/'],
2009 manager
.db
.tokensGetByIdentifier
.resolves();
2011 it('covers', async
function () {
2012 await manager
.getAdmin(res
, ctx
);
2016 describe('postAdmin', function () {
2017 beforeEach(function () {
2018 manager
.db
.profilesScopesByIdentifier
.resolves({
2020 'https://profile.example.com/': {
2023 description: 'a scope',
2024 profiles: ['https://profile.example.com/'],
2031 description: 'a scope',
2032 profiles: ['https://profile.example.com/'],
2035 profiles: ['https://profile.example.com/'],
2037 manager
.db
.tokensGetByIdentifier
.resolves([]);
2038 manager
.db
.tokenRevokeByCodeId
.resolves();
2039 manager
.db
.profileIdentifierInsert
.resolves();
2040 manager
.db
.profileScopesSetAll
.resolves();
2041 manager
.communication
.fetchProfile
.resolves({
2043 authorizationEndpoint: manager
.selfAuthorizationEndpoint
,
2047 describe('save-scopes action', function () {
2048 beforeEach(function () {
2049 ctx
.parsedBody
['action'] = 'save-scopes';
2050 ctx
.parsedBody
['scopes-https://profile/example.com/'] = ['scope1', 'scope2'];
2052 it('covers saving scopes', async
function () {
2053 await manager
.postAdmin(res
, ctx
);
2054 assert(ctx
.notifications
.length
);
2055 assert(manager
.db
.profileScopesSetAll
.called
);
2057 it('covers saving scopes error', async
function () {
2058 manager
.db
.profileScopesSetAll
.rejects();
2059 await manager
.postAdmin(res
, ctx
);
2060 assert(ctx
.errors
.length
);
2062 }); // save-scopes action
2063 describe('new-profile action', function () {
2064 beforeEach(function () {
2065 ctx
.parsedBody
['action'] = 'new-profile';
2067 it('covers new profile', async
function () {
2068 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2069 await manager
.postAdmin(res
, ctx
);
2070 assert(ctx
.notifications
.length
);
2071 assert(manager
.db
.profileIdentifierInsert
.called
);
2072 assert(manager
.db
.profileScopesSetAll
.called
);
2074 it('covers invalid profile', async
function () {
2075 ctx
.parsedBody
['action'] = 'new-profile';
2076 ctx
.parsedBody
['profile'] = 'not a url';
2077 await manager
.postAdmin(res
, ctx
);
2078 assert(ctx
.errors
.length
);
2080 it('covers other validation failure', async
function () {
2081 sinon
.stub(manager
.communication
, 'validateProfile').rejects(expectedException
);
2082 ctx
.parsedBody
['action'] = 'new-profile';
2083 ctx
.parsedBody
['profile'] = 'not a url';
2084 await manager
.postAdmin(res
, ctx
);
2085 assert(ctx
.errors
.length
);
2087 it('covers mismatched profile', async
function () {
2088 ctx
.parsedBody
['action'] = 'new-profile';
2089 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2090 manager
.communication
.fetchProfile
.resolves({
2092 authorizationEndpoint: 'https://other.example.com/auth',
2095 await manager
.postAdmin(res
, ctx
);
2096 assert(ctx
.errors
.length
);
2098 it('covers new profile error', async
function () {
2099 ctx
.parsedBody
['action'] = 'new-profile';
2100 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2101 manager
.db
.profileIdentifierInsert
.rejects();
2102 await manager
.postAdmin(res
, ctx
);
2103 assert(ctx
.errors
.length
);
2105 }); // new-profile action
2106 describe('new-scope action', function () {
2107 beforeEach(function () {
2108 ctx
.parsedBody
['action'] = 'new-scope';
2110 it('covers new scope', async
function () {
2111 ctx
.parsedBody
['scope'] = 'newscope';
2112 await manager
.postAdmin(res
, ctx
);
2113 assert(ctx
.notifications
.length
);
2114 assert(manager
.db
.scopeUpsert
.called
);
2116 it('covers bad scope', async
function () {
2117 ctx
.parsedBody
['scope'] = 'bad scope';
2118 await manager
.postAdmin(res
, ctx
);
2119 assert(ctx
.errors
.length
);
2121 it('covers new scope error', async
function () {
2122 ctx
.parsedBody
['scope'] = 'newscope';
2123 manager
.db
.scopeUpsert
.rejects();
2124 await manager
.postAdmin(res
, ctx
);
2125 assert(ctx
.errors
.length
);
2127 it('covers empty scope', async
function () {
2128 delete ctx
.parsedBody
.scope
;
2129 await manager
.postAdmin(res
, ctx
);
2130 assert(!ctx
.errors
.length
);
2132 }); // new-scope action
2133 describe('delete-scope-* action', function () {
2134 beforeEach(function () {
2135 ctx
.parsedBody
['action'] = 'delete-scope-food%3Ayum';
2137 it('covers delete', async
function () {
2138 manager
.db
.scopeDelete
.resolves(true);
2139 await manager
.postAdmin(res
, ctx
);
2140 assert(ctx
.notifications
.length
);
2141 assert(manager
.db
.scopeDelete
.called
);
2143 it('covers no delete', async
function () {
2144 manager
.db
.scopeDelete
.resolves(false);
2145 await manager
.postAdmin(res
, ctx
);
2146 assert(ctx
.notifications
.length
);
2147 assert(manager
.db
.scopeDelete
.called
);
2149 it('covers delete error', async
function () {
2150 manager
.db
.scopeDelete
.rejects();
2151 await manager
.postAdmin(res
, ctx
);
2152 assert(ctx
.errors
.length
);
2153 assert(manager
.db
.scopeDelete
.called
);
2155 it('ignores empty scope', async
function () {
2156 ctx
.parsedBody
['action'] = 'delete-scope-';
2157 await manager
.postAdmin(res
, ctx
);
2158 assert(manager
.db
.scopeDelete
.notCalled
);
2159 assert(!ctx
.notifications
.length
);
2160 assert(!ctx
.errors
.length
);
2162 }); // delete-scope-* action
2163 describe('revoke-* action', function () {
2164 beforeEach(function () {
2165 ctx
.parsedBody
['action'] = 'revoke-b1591c00-9cb7-11ec-a05c-0025905f714a';
2167 it('covers revocation', async
function () {
2168 await manager
.postAdmin(res
, ctx
);
2169 assert(ctx
.notifications
.length
);
2170 assert(manager
.db
.tokenRevokeByCodeId
.called
);
2172 it('covers revocation error', async
function () {
2173 manager
.db
.tokenRevokeByCodeId
.rejects();
2174 await manager
.postAdmin(res
, ctx
);
2175 assert(ctx
.errors
.length
);
2177 it('covers no code', async
function () {
2178 ctx
.parsedBody
['action'] = 'revoke-';
2179 await manager
.postAdmin(res
, ctx
);
2180 assert(!ctx
.notifications
.length
);
2181 assert(!ctx
.errors
.length
);
2182 assert(manager
.db
.tokenRevokeByCodeId
.notCalled
);
2184 }); // revoke-* action
2185 it('covers empty action', async
function () {
2186 delete ctx
.parsedBody
.action
;
2187 await manager
.postAdmin(res
, ctx
);
2188 assert(!ctx
.errors
.length
);
2190 it('covers unknown action', async
function () {
2191 ctx
.parsedBody
['action'] = 'unsupported-action';
2192 await manager
.postAdmin(res
, ctx
);
2193 assert(ctx
.errors
.length
);
2197 describe('getAdminTicket', function () {
2198 it('covers', async
function () {
2199 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2200 await manager
.getAdminTicket(res
, ctx
);
2201 assert(res
.end
.called
);
2203 }); // getAdminTicket
2205 describe('postAdminTicket', function () {
2206 beforeEach(function () {
2207 ctx
.parsedBody
['action'] = 'proffer-ticket';
2208 ctx
.parsedBody
['scopes'] = ['read', 'role:private'];
2209 ctx
.parsedBody
['adhoc'] = 'adhoc_scope';
2210 ctx
.parsedBody
['profile'] = 'https://profile.example.com/';
2211 ctx
.parsedBody
['resource'] = 'https://profile.example.com/feed';
2212 ctx
.parsedBody
['subject'] = 'https://subject.example.com/';
2213 manager
.db
.profilesScopesByIdentifier
.resolves({ scopeIndex: {} });
2214 sinon
.stub(manager
.mysteryBox
, 'pack').resolves('ticket');
2215 manager
.communication
.fetchProfile
.resolves({
2217 ticketEndpoint: 'https://example.com/ticket',
2221 it('covers success', async
function () {
2222 await manager
.postAdminTicket(res
, ctx
);
2223 assert(res
.end
.called
);
2224 assert
.strictEqual(ctx
.errors
.length
, 0);
2225 assert
.strictEqual(ctx
.notifications
.length
, 1);
2227 it('requires params', async
function () {
2228 delete ctx
.parsedBody
['adhoc'];
2229 ctx
.parsedBody
['profile'] = 'bad url';
2230 ctx
.parsedBody
['resource'] = 'bad url';
2231 ctx
.parsedBody
['subject'] = 'bad url';
2232 ctx
.parsedBody
['scopes'] = ['fl"hrgl', 'email'];
2233 await manager
.postAdminTicket(res
, ctx
);
2234 assert(res
.end
.called
);
2235 assert
.strictEqual(ctx
.errors
.length
, 5);
2236 assert
.strictEqual(ctx
.notifications
.length
, 0);
2238 it('ignores unknown action', async
function () {
2239 ctx
.parsedBody
['action'] = 'prove-dough';
2240 await manager
.postAdminTicket(res
, ctx
);
2241 assert(res
.end
.called
);
2243 it('covers delivery failure', async
function () {
2244 manager
.communication
.deliverTicket
.rejects(expectedException
);
2245 await manager
.postAdminTicket(res
, ctx
);
2246 assert(res
.end
.called
);
2247 assert
.strictEqual(ctx
.errors
.length
, 1);
2248 assert
.strictEqual(ctx
.notifications
.length
, 0);
2250 it('covers no ticket endpoint', async
function () {
2251 manager
.communication
.fetchProfile
.resolves({
2255 await manager
.postAdminTicket(res
, ctx
);
2256 assert(res
.end
.called
);
2257 assert
.strictEqual(ctx
.errors
.length
, 1);
2258 assert
.strictEqual(ctx
.notifications
.length
, 0);
2260 it('covers bad ticket endpoint', async
function () {
2261 manager
.communication
.fetchProfile
.resolves({
2263 ticketEndpoint: 'not a url',
2266 await manager
.postAdminTicket(res
, ctx
);
2267 assert(res
.end
.called
);
2268 assert
.strictEqual(ctx
.errors
.length
, 1);
2269 assert
.strictEqual(ctx
.notifications
.length
, 0);
2271 }); // postAdminTicket
2273 describe('postTicket', function () {
2274 beforeEach(function () {
2276 ticket: 'ticket123',
2277 resource: 'https://blog.example.com/',
2278 subject: 'https://otheruser.example.com/',
2281 it('accepts a ticket for a known profile', async
function () {
2282 manager
.db
.profileIsValid
.resolves(true);
2283 await manager
.postTicket(req
, res
, ctx
);
2284 assert(res
.end
.called
);
2285 assert
.strictEqual(res
.statusCode
, 202);
2287 it('rejects invalid resource', async
function () {
2288 ctx
.parsedBody
.resource
= 'invalid url';
2289 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2291 it('rejects invalid subject', async
function () {
2292 manager
.db
.profileIsValid(false);
2293 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2295 it('covers queue publish failure', async
function () {
2296 manager
.db
.profileIsValid
.resolves(true);
2297 manager
.queuePublisher
.publish
.rejects(expectedException
);
2298 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), expectedException
);
2300 it('covers no ticket queue', async
function () {
2301 delete options
.queues
.amqp
.url
;
2302 manager
= new Manager(logger
, stubDb
, options
);
2304 await assert
.rejects(() => manager
.postTicket(req
, res
, ctx
), ResponseError
);
2309 describe('getAdminMaintenance', function () {
2310 it('covers information', async
function () {
2311 await manager
.getAdminMaintenance(res
, ctx
);
2312 assert(res
.end
.called
);
2314 it('covers tasks', async
function () {
2316 [Enum
.Chore
.CleanTokens
]: '',
2318 await manager
.getAdminMaintenance(res
, ctx
);
2319 assert(res
.end
.called
);
2321 }); // getAdminMaintenance