2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string */
6 const assert
= require('assert');
7 const sinon
= require('sinon'); // eslint-disable-line node/no-unpublished-require
9 const Communication
= require('../../lib/communication');
10 const { ValidationError
} = require('../../lib/errors');
11 const dns
= require('dns');
13 const stubLogger
= require('../stub-logger');
14 const testData
= require('../test-data/communication');
16 describe('Communication', function () {
17 let communication
, options
;
19 beforeEach(async
function () {
21 communication
= new Communication(stubLogger
, options
);
22 await communication
._init();
24 sinon
.stub(communication
, 'got');
26 afterEach(function () {
30 it('instantiates', function () {
31 assert(communication
);
34 it('covers no config', function () {
35 communication
= new Communication(stubLogger
);
38 describe('_init', function () {
39 it('covers first use', async
function () {
40 await communication
._init({});
41 assert(communication
.got
.called
);
45 describe('_onRetry', function () {
46 it('covers', function () {
47 communication
._onRetry(new Error('oh no'), 1);
48 assert(communication
.logger
.debug
.called
);
52 describe('_challengeFromVerifier', function () {
53 it('covers', function () {
54 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
55 const expected
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
56 const result
= Communication
._challengeFromVerifier(verifier
);
57 assert
.strictEqual(result
, expected
);
59 }); // _challengeFromVerifier
61 describe('generatePKCE', function () {
62 it('covers', async
function () {
63 const result
= await Communication
.generatePKCE();
64 assert(result
.codeVerifier
);
65 assert(result
.codeChallenge
);
66 assert(result
.codeChallengeMethod
);
67 assert
.strictEqual(result
.codeChallengeMethod
, 'S256');
69 it('covers error', async
function () {
70 await assert
.rejects(() => Communication
.generatePKCE(1));
74 describe('verifyChallenge', function () {
75 it('covers success', function () {
76 const method
= 'S256';
77 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
78 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
79 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
80 assert
.strictEqual(result
, true);
82 it('also covers success', function () {
83 const method
= 'SHA256';
84 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
85 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
86 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
87 assert
.strictEqual(result
, true);
89 it('covers failure', function () {
90 const method
= 'S256';
91 const challenge
= 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
92 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
93 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
94 assert
.strictEqual(result
, false);
96 it('covers unhandled method', function () {
98 const challenge
= 'xkfP7DUYDsnu07Kg6ogc8A';
99 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
100 assert
.throws(() => Communication
.verifyChallenge(challenge
, verifier
, method
));
102 }); // verifyChallenge
104 describe('_userAgentString', function () {
105 it('has default behavior', function () {
106 const result
= Communication
._userAgentString();
108 assert(result
.length
> 30);
110 it('is settable', function () {
111 const result
= Communication
._userAgentString({
114 implementation: 'custom',
117 assert
.strictEqual(result
, 'myClient/9.9.9 (custom)');
119 it('covers branches', function () {
120 const result
= Communication
._userAgentString({
126 assert
.strictEqual(result
, 'myClient/9.9.9');
128 }); // userAgentString
130 describe('_baseUrlString', function () {
131 it('covers no path', function () {
132 const urlObj
= new URL('https://example.com');
133 const expected
= 'https://example.com/';
134 const result
= Communication
._baseUrlString(urlObj
);
135 assert
.strictEqual(result
, expected
);
137 it('covers paths', function () {
138 const urlObj
= new URL('https://example.com/path/blah');
139 const expected
= 'https://example.com/path/';
140 const result
= Communication
._baseUrlString(urlObj
);
141 assert
.strictEqual(result
, expected
);
143 }); // _baseUrlString
145 describe('_parseContentType', function () {
146 let contentTypeHeader
, expected
, result
;
147 it('covers undefined', function () {
148 contentTypeHeader
= undefined;
150 mediaType: 'application/octet-stream',
153 result
= Communication
._parseContentType(contentTypeHeader
);
154 assert
.deepStrictEqual(result
, expected
);
156 it('covers empty', function () {
157 contentTypeHeader
= '';
159 mediaType: 'application/octet-stream',
162 result
= Communication
._parseContentType(contentTypeHeader
);
163 assert
.deepStrictEqual(result
, expected
);
165 it('covers extra parameters', function () {
166 contentTypeHeader
= 'text/plain; CharSet="UTF-8"; WeirdParam';
168 mediaType: 'text/plain',
171 'weirdparam': undefined,
174 result
= Communication
._parseContentType(contentTypeHeader
);
175 assert
.deepStrictEqual(result
, expected
);
177 }); // parseContentType
179 describe('_mergeLinkHeader', function () {
180 let microformat
, response
, expected
;
181 beforeEach(function () {
185 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
190 it('covers', function () {
194 'hub': ['https://hub.example.com/'],
195 'self': ['https://example.com/'],
198 'https://example.com/': {
202 'https://hub.example.com/': {
208 communication
._mergeLinkHeader(microformat
, response
);
209 assert
.deepStrictEqual(microformat
, expected
);
211 it('covers existing', function () {
215 'preload': ['https://example.com/style'],
216 'hub': ['https://hub.example.com/'],
219 'https://hub.example.com/': {
223 'https://example.com/style': {
232 'preload': ['https://example.com/style'],
233 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
234 'self': ['https://example.com/'],
237 'https://example.com/': {
241 'https://hub.example.com/': {
242 rels: ['hub', 'hub'],
245 'https://example.com/style': {
251 communication
._mergeLinkHeader(microformat
, response
);
252 assert
.deepStrictEqual(microformat
, expected
);
254 it('ignores bad header', function () {
255 response
.headers
.link
= 'not really a link header';
261 communication
._mergeLinkHeader(microformat
, response
);
262 assert
.deepStrictEqual(microformat
, expected
);
264 }); // _mergeLinkHeader
266 describe('fetchMicroformat', function () {
267 let expected
, response
, result
, urlObj
;
268 beforeEach(function () {
269 expected
= undefined;
271 urlObj
= new URL('https://thuza.ratfeathers.com/');
273 headers: Object
.assign({}, testData
.linkHeaders
),
274 body: Buffer
.from(testData
.hCardHtml
),
277 it('covers', async
function () {
278 response
.body
= testData
.hCardHtml
;
279 communication
.got
.resolves(response
);
282 'authorization_endpoint': ['https://ia.squeep.com/auth'],
283 'token_endpoint': ['https://ia.squeep.com/token'],
284 'canonical': ['https://thuza.ratfeathers.com/'],
285 'author': ['https://thuza.ratfeathers.com/'],
286 'me': ['https://thuza.ratfeathers.com/'],
287 'self': ['https://thuza.ratfeathers.com/'],
288 'hub': ['https://hub.squeep.com/'],
289 'preload': ['https://thuza.ratfeathers.com/image.png'],
292 'https://hub.squeep.com/': {
296 'https://ia.squeep.com/auth': {
297 rels: ['authorization_endpoint'],
300 'https://ia.squeep.com/token': {
301 rels: ['token_endpoint'],
304 'https://thuza.ratfeathers.com/': {
305 rels: ['self', 'author', 'canonical', 'me'],
308 'https://thuza.ratfeathers.com/image.png': {
316 photo: ['https://thuza.ratfeathers.com/image.png'],
317 url: ['https://thuza.ratfeathers.com/'],
323 result
= await communication
.fetchMicroformat(urlObj
);
324 assert
.deepStrictEqual(result
, expected
);
326 it('covers got error', async
function () {
327 communication
.got
.rejects(new Error('blah'));
328 expected
= undefined;
330 result
= await communication
.fetchMicroformat(urlObj
);
332 assert
.deepStrictEqual(result
, expected
);
334 it('covers non-parsable content', async
function () {
335 response
.body
= 'some bare text';
336 response
.headers
= {};
337 communication
.got
.resolves(response
);
344 result
= await communication
.fetchMicroformat(urlObj
);
346 assert
.deepStrictEqual(result
, expected
);
348 it('covers non-utf8 content', async
function () {
349 response
.headers
['content-type'] = 'text/html; charset=ASCII';
350 communication
.got
.resolves(response
);
353 'authorization_endpoint': ['https://ia.squeep.com/auth'],
354 'token_endpoint': ['https://ia.squeep.com/token'],
355 'canonical': ['https://thuza.ratfeathers.com/'],
356 'author': ['https://thuza.ratfeathers.com/'],
357 'me': ['https://thuza.ratfeathers.com/'],
358 'self': ['https://thuza.ratfeathers.com/'],
359 'hub': ['https://hub.squeep.com/'],
360 'preload': ['https://thuza.ratfeathers.com/image.png'],
363 'https://hub.squeep.com/': {
367 'https://ia.squeep.com/auth': {
368 rels: ['authorization_endpoint'],
371 'https://ia.squeep.com/token': {
372 rels: ['token_endpoint'],
375 'https://thuza.ratfeathers.com/': {
376 rels: ['self', 'author', 'canonical', 'me'],
379 'https://thuza.ratfeathers.com/image.png': {
387 photo: ['https://thuza.ratfeathers.com/image.png'],
388 url: ['https://thuza.ratfeathers.com/'],
394 result
= await communication
.fetchMicroformat(urlObj
);
396 assert
.deepStrictEqual(result
, expected
);
398 }); // fetchMicroformat
400 describe('fetchJSON', function () {
401 let expected
, response
, result
, urlObj
;
402 beforeEach(function () {
403 expected
= undefined;
405 urlObj
= new URL('https://thuza.ratfeathers.com/');
407 headers: Object
.assign({}, testData
.linkHeaders
),
408 body: testData
.hCardHtml
,
411 it('covers', async
function () {
412 communication
.got
.resolves(response
);
413 expected
= { foo: 'bar', baz: 123 };
414 response
.body
= expected
;
416 result
= await communication
.fetchJSON(urlObj
);
417 assert
.deepStrictEqual(result
, expected
);
419 it('covers got error', async
function () {
420 communication
.got
.rejects(new Error('blah'));
421 expected
= undefined;
423 result
= await communication
.fetchJSON(urlObj
);
425 assert
.deepStrictEqual(result
, expected
);
427 it('covers non-parsable content', async
function () {
428 response
.body
= 'some bare text';
429 response
.headers
= {};
430 const error
= new Error('oh no');
431 response
.request
= { options: { url: new URL('https://example.com/') } };
432 communication
.got
.rejects(new communication
.Got
.ParseError(error
, response
));
433 expected
= undefined;
435 result
= await communication
.fetchJSON(urlObj
);
437 assert
.deepStrictEqual(result
, expected
);
441 describe('validateProfile', function () {
442 let url
, validationOptions
;
443 beforeEach(function () {
444 url
= 'https://example.com/';
445 validationOptions
= {};
446 sinon
.stub(dns
.promises
, 'lookup').resolves([{ family: 4, address: '10.11.12.14' }]);
448 it('rejects invalid url', async
function () {
450 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
452 it('covers success', async
function () {
453 const result
= await communication
.validateProfile(url
, validationOptions
);
454 assert
.strictEqual(result
.isLoopback
, false);
456 it('rejects invalid', async
function () {
457 url
= 'ftp://example.com/';
458 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
461 }); // validateProfile
463 describe('validateClientIdentifier', function () {
464 let url
, validationOptions
;
465 beforeEach(function () {
466 url
= 'https://example.com/';
467 validationOptions
= {};
468 sinon
.stub(dns
.promises
, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]);
470 it('rejects invalid url', async
function () {
471 await assert
.rejects(() => communication
.validateClientIdentifier('bad url'), ValidationError
);
473 it('rejects invalid scheme', async
function () {
474 url
= 'ftp://example.com/';
475 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
477 it('rejects fragment', async
function () {
478 url
= 'https://example.com/#foo';
479 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
481 it('rejects username', async
function () {
482 url
= 'https://user@example.com/';
483 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
485 it('rejects password', async
function () {
486 url
= 'https://:foo@example.com/';
487 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
489 it('rejects relative path', async
function () {
490 url
= 'https://example.com/client/../sneaky';
491 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
493 it('rejects ipv4', async
function () {
494 url
= 'https://10.11.12.13/';
495 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
497 it('rejects ipv6', async
function () {
498 url
= 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
499 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
501 it('accepts ipv4 loopback', async
function () {
502 url
= 'https://127.0.0.1/';
503 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
504 assert
.strictEqual(result
.isLoopback
, true);
506 it('accepts ipv6 loopback', async
function () {
507 url
= 'https://[::1]/';
508 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
509 assert
.strictEqual(result
.isLoopback
, true);
511 it('accepts resolved ipv4 loopback', async
function () {
512 dns
.promises
.lookup
.resolves([{ family: 4, address: '127.0.0.1' }]);
513 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
514 assert
.strictEqual(result
.isLoopback
, true);
516 it('accepts resolved ipv6 loopback', async
function () {
517 dns
.promises
.lookup
.resolves([{ family: 6, address: '::1' }]);
518 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
519 assert
.strictEqual(result
.isLoopback
, true);
521 it('covers success', async
function () {
522 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
523 assert
.strictEqual(result
.isLoopback
, false);
525 it('rejects resolution failure', async
function () {
526 dns
.promises
.lookup
.rejects(new Error('oh no'));
527 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
529 it('rejects mismatched resolutions', async
function () {
530 dns
.promises
.lookup
.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
531 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
533 it('ignores unknown dns family', async
function () {
534 dns
.promises
.lookup
.resolves([{ family: 5, address: '10.9.8.7' }]);
535 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
536 assert
.strictEqual(result
.isLoopback
, false);
538 it('covers rooted hostname', async
function() {
539 url
= 'https://example.com./';
540 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
541 assert
.strictEqual(result
.isLoopback
, false);
543 it('covers unresolved', async
function () {
544 dns
.promises
.lookup
.resolves();
545 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
546 assert
.strictEqual(result
.isLoopback
, false);
548 }); // validateClientIdentifier
550 describe('fetchClientIdentifier', function () {
551 let expected
, response
, result
, urlObj
;
552 beforeEach(function () {
553 expected
= undefined;
555 urlObj
= new URL('https://thuza.ratfeathers.com/');
558 body: testData
.multiMF2Html
,
561 it('covers', async
function () {
562 communication
.got
.resolves(response
);
566 name: ['Also Some Client'],
567 url: ['https://thuza.ratfeathers.com/'],
572 'author': ['https://thuza.ratfeathers.com/'],
573 'authorization_endpoint': ['https://ia.squeep.com/auth'],
574 'canonical': ['https://thuza.ratfeathers.com/'],
575 'me': ['https://thuza.ratfeathers.com/'],
576 'token_endpoint': ['https://ia.squeep.com/token'],
579 result
= await communication
.fetchClientIdentifier(urlObj
);
580 assert
.deepStrictEqual(result
, expected
);
582 it('covers failed fetch', async
function () {
583 communication
.got
.rejects();
584 expected
= undefined;
585 result
= await communication
.fetchClientIdentifier(urlObj
);
586 assert
.deepStrictEqual(result
, expected
);
588 it('covers no h-app data', async
function () {
589 response
.body
= testData
.noneMF2Html
;
590 communication
.got
.resolves(response
);
595 result
= await communication
.fetchClientIdentifier(urlObj
);
596 assert
.deepStrictEqual(result
, expected
);
598 it('covers missing fields', async
function () {
599 sinon
.stub(communication
, 'fetchMicroformat').resolves({});
604 result
= await communication
.fetchClientIdentifier(urlObj
);
605 assert
.deepStrictEqual(result
, expected
);
607 it('covers other missing fields', async
function () {
608 sinon
.stub(communication
, 'fetchMicroformat').resolves({
614 url: ['https://example.com'],
623 result
= await communication
.fetchClientIdentifier(urlObj
);
624 assert
.deepStrictEqual(result
, expected
);
626 it('covers loopback', async
function () {
627 sinon
.spy(communication
, 'fetchMicroformat');
628 urlObj
.isLoopback
= true;
633 result
= await communication
.fetchClientIdentifier(urlObj
);
634 assert
.deepStrictEqual(result
, expected
);
635 assert(communication
.fetchMicroformat
.notCalled
);
637 }); // fetchClientIdentifier
639 describe('fetchProfile', function () {
640 let expected
, response
, result
, urlObj
;
641 beforeEach(function () {
642 expected
= undefined;
644 urlObj
= new URL('https://thuza.ratfeathers.com/');
647 body: testData
.hCardHtml
,
649 sinon
.stub(communication
, 'fetchJSON');
651 describe('legacy without indieauth-metadata', function () {
652 it('covers', async
function () {
653 communication
.got
.resolves(response
);
656 photo: 'https://thuza.ratfeathers.com/image.png',
657 url: 'https://thuza.ratfeathers.com/',
659 authorizationEndpoint: 'https://ia.squeep.com/auth',
660 tokenEndpoint: 'https://ia.squeep.com/token',
662 authorizationEndpoint: 'https://ia.squeep.com/auth',
663 tokenEndpoint: 'https://ia.squeep.com/token',
666 result
= await communication
.fetchProfile(urlObj
);
667 assert
.deepStrictEqual(result
, expected
);
669 it('covers multiple hCards', async
function () {
670 response
.body
= testData
.multiMF2Html
;
671 communication
.got
.resolves(response
);
675 photo: 'https://thuza.ratfeathers.com/image.png',
676 url: 'https://thuza.ratfeathers.com/',
677 authorizationEndpoint: 'https://ia.squeep.com/auth',
678 tokenEndpoint: 'https://ia.squeep.com/token',
680 authorizationEndpoint: 'https://ia.squeep.com/auth',
681 tokenEndpoint: 'https://ia.squeep.com/token',
684 result
= await communication
.fetchProfile(urlObj
);
685 assert
.deepStrictEqual(result
, expected
);
687 it('covers failed fetch', async
function () {
688 communication
.got
.rejects();
696 result
= await communication
.fetchProfile(urlObj
);
697 assert
.deepStrictEqual(result
, expected
);
700 it('covers', async
function () {
701 response
.body
= testData
.hCardMetadataHtml
;
702 communication
.got
.resolves(response
);
703 communication
.fetchJSON
.resolves({
704 'issuer': 'https://ia.squeep.com/',
705 'authorization_endpoint': 'https://ia.squeep.com/auth',
706 'token_endpoint': 'https://ia.squeep.com/token',
707 'introspection_endpoint': 'https://ia.squeep.com/introspect',
708 'introspection_endpoint_auth_methods_supported': [ '' ],
709 'revocation_endpoint': 'https://ia.squeep.com/revoke',
710 'revocation_endpoint_auth_methods_supported': [ 'none' ],
711 'scopes_supported': [ 'profile', 'email' ],
712 'service_documentation': 'https://indieauth.spec.indieweb.org/',
713 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
714 'authorization_response_iss_parameter_supported': true,
715 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
719 photo: 'https://thuza.ratfeathers.com/image.png',
720 url: 'https://thuza.ratfeathers.com/',
723 authorizationEndpoint: 'https://ia.squeep.com/auth',
724 tokenEndpoint: 'https://ia.squeep.com/token',
725 issuer: 'https://ia.squeep.com/',
726 introspectionEndpoint: 'https://ia.squeep.com/introspect',
727 introspectionEndpointAuthMethodsSupported: [ '' ],
728 revocationEndpoint: 'https://ia.squeep.com/revoke',
729 revocationEndpointAuthMethodsSupported: [ 'none' ],
730 scopesSupported: [ 'profile', 'email' ],
731 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
732 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
733 authorizationResponseIssParameterSupported: true,
734 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
736 authorizationEndpoint: 'https://ia.squeep.com/auth',
737 tokenEndpoint: 'https://ia.squeep.com/token',
738 indieauthMetadata: 'https://ia.squeep.com/meta',
741 result
= await communication
.fetchProfile(urlObj
);
743 assert
.deepStrictEqual(result
, expected
);
745 it('covers metadata missing fields', async
function () {
746 response
.body
= testData
.hCardMetadataHtml
;
747 communication
.got
.resolves(response
);
748 communication
.fetchJSON
.resolves({
749 'issuer': 'https://ia.squeep.com/',
753 photo: 'https://thuza.ratfeathers.com/image.png',
754 url: 'https://thuza.ratfeathers.com/',
757 issuer: 'https://ia.squeep.com/',
759 indieauthMetadata: 'https://ia.squeep.com/meta',
762 result
= await communication
.fetchProfile(urlObj
);
764 assert
.deepStrictEqual(result
, expected
);
766 it('covers metadata response failure', async
function () {
767 const jsonError
= new Error('oh no');
768 response
.body
= testData
.hCardMetadataHtml
;
770 .onCall(0).resolves(response
)
771 .onCall(1).rejects(jsonError
);
772 communication
.fetchJSON
.restore();
775 photo: 'https://thuza.ratfeathers.com/image.png',
776 url: 'https://thuza.ratfeathers.com/',
779 indieauthMetadata: 'https://ia.squeep.com/meta',
782 result
= await communication
.fetchProfile(urlObj
);
784 assert
.deepStrictEqual(result
, expected
);
788 describe('fetchMetadata', function () {
790 beforeEach(function () {
791 metadataUrl
= new URL('https://thuza.ratfeathers.com/');
792 sinon
.stub(communication
, 'fetchJSON');
794 it('covers success', async
function () {
795 communication
.fetchJSON
.resolves({
796 'issuer': 'https://ia.squeep.com/',
797 'authorization_endpoint': 'https://ia.squeep.com/auth',
798 'token_endpoint': 'https://ia.squeep.com/token',
799 'introspection_endpoint': 'https://ia.squeep.com/introspect',
800 'introspection_endpoint_auth_methods_supported': [ '' ],
801 'revocation_endpoint': 'https://ia.squeep.com/revoke',
802 'revocation_endpoint_auth_methods_supported': [ 'none' ],
803 'scopes_supported': [ 'profile', 'email' ],
804 'service_documentation': 'https://indieauth.spec.indieweb.org/',
805 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
806 'authorization_response_iss_parameter_supported': true,
807 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
810 authorizationEndpoint: 'https://ia.squeep.com/auth',
811 tokenEndpoint: 'https://ia.squeep.com/token',
812 issuer: 'https://ia.squeep.com/',
813 introspectionEndpoint: 'https://ia.squeep.com/introspect',
814 introspectionEndpointAuthMethodsSupported: [ '' ],
815 revocationEndpoint: 'https://ia.squeep.com/revoke',
816 revocationEndpointAuthMethodsSupported: [ 'none' ],
817 scopesSupported: [ 'profile', 'email' ],
818 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
819 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
820 authorizationResponseIssParameterSupported: true,
821 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
823 const result
= await communication
.fetchMetadata(metadataUrl
);
824 assert
.deepStrictEqual(result
, expected
);
826 it('covers failure', async
function () {
827 communication
.fetchJSON
.resolves(undefined);
829 const result
= await communication
.fetchMetadata(metadataUrl
);
830 assert
.deepStrictEqual(result
, expected
);
834 describe('redeemCode', function () {
835 let expected
, urlObj
, code
, codeVerifier
, clientId
, redirectURI
;
836 beforeEach(function () {
837 urlObj
= new URL('https://example.com/auth');
838 code
= Buffer
.allocUnsafe(42).toString('base64url');
839 codeVerifier
= Buffer
.allocUnsafe(42).toString('base64url');
840 clientId
= 'https://example.com/';
841 redirectURI
= 'https://example.com/_ia';
843 it('covers', async
function () {
844 communication
.got
.resolves({
846 me: 'https://profile.example.com/',
850 me: 'https://profile.example.com/',
853 const result
= await communication
.redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
855 assert
.deepStrictEqual(result
, expected
);
857 it('covers deprecated method name', async
function () {
858 communication
.got
.resolves({
860 me: 'https://profile.example.com/',
864 me: 'https://profile.example.com/',
867 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
869 assert
.deepStrictEqual(result
, expected
);
871 it('covers failure', async
function () {
872 const error
= new Error('oh no');
876 url: new URL('https://example.com'),
880 const parseError
= new communication
.Got
.ParseError(error
, response
);
881 communication
.got
.rejects(parseError
);
883 const result
= await communication
.redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
885 assert
.strictEqual(result
, undefined);
889 describe('introspectToken', function () {
890 let introspectionUrlObj
, authenticationHeader
, token
;
891 beforeEach(function () {
892 introspectionUrlObj
= new URL('https://ia.example.com/introspect');
893 authenticationHeader
= 'Bearer XXX';
896 it('covers success active', async
function () {
897 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
898 communication
.got
.resolves({
901 me: 'https://profile.example.com/',
902 'client_id': 'https://app.example.com/',
903 scope: 'create profile email',
904 exp: nowEpoch
+ 86400,
908 const result
= await communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
);
909 assert
.strictEqual(result
.active
, true);
911 it('covers success inactive', async
function () {
912 communication
.got
.resolves({
917 const result
= await communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
);
918 assert
.strictEqual(result
.active
, false);
920 it('covers failure', async
function () {
921 communication
.got
.resolves({ body: 'what kind of response is this?' });
922 await assert
.rejects(() => communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
));
924 }); // introspectToken
926 describe('deliverTicket', function () {
927 let ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
, issuerUrlObj
;
928 beforeEach(function () {
929 ticketEndpointUrlObj
= new URL('https://ticket.example.com/');
930 resourceUrlObj
= new URL('https://resource.example.com/');
931 subjectUrlObj
= new URL('https://subject.example.com/');
932 issuerUrlObj
= new URL('https://idp.example.com/');
933 ticket
= 'XXXThisIsATicketXXX';
935 it('covers success', async
function () {
936 const expected
= { body: 'blah', statusCode: 200 };
937 communication
.got
.resolves(expected
);
938 const result
= await communication
.deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
, issuerUrlObj
);
939 assert
.deepStrictEqual(result
, expected
);
941 it('covers success, no issuer', async
function () {
942 const expected
= { body: 'blah', statusCode: 200 };
943 communication
.got
.resolves(expected
);
944 const result
= await communication
.deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
);
945 assert
.deepStrictEqual(result
, expected
);
947 it('covers failure', async
function () {
948 const expectedException
= new Error('oh no');
949 communication
.got
.rejects(expectedException
);
950 await assert
.rejects(() => communication
.deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
), expectedException
);
954 describe('_fetchMetadataOrTokenEndpoint', function () {
955 let urlObj
, metadataUrl
, tokenUrl
;
956 beforeEach(function () {
957 urlObj
= new URL('https://idp.example.com/');
958 metadataUrl
= new URL('https://idp.example.com/meta');
959 tokenUrl
= new URL('https://idp.example.com/token');
960 sinon
.stub(communication
, 'fetchMicroformat').resolves({
962 'indieauth-metadata': [ metadataUrl
.href
],
963 'token_endpoint': [ tokenUrl
.href
],
967 it('covers success', async
function () {
972 const result
= await communication
._fetchMetadataOrTokenEndpoint(urlObj
);
973 assert
.deepStrictEqual(result
, expected
);
975 it('covers bad metadata url', async
function () {
976 communication
.fetchMicroformat
.resolves({
978 'indieauth-metadata': [ 'not a url' ],
979 'token_endpoint': [ tokenUrl
.href
],
983 metadataUrl: undefined,
986 const result
= await communication
._fetchMetadataOrTokenEndpoint(urlObj
);
987 assert
.deepStrictEqual(result
, expected
);
989 it('covers bad token url', async
function () {
990 communication
.fetchMicroformat
.resolves({
992 'indieauth-metadata': [],
993 'token_endpoint': [ 'not a url' ],
997 metadataUrl: undefined,
1000 const result
= await communication
._fetchMetadataOrTokenEndpoint(urlObj
);
1001 assert
.deepStrictEqual(result
, expected
);
1003 it('covers no rels', async
function () {
1004 communication
.fetchMicroformat
.resolves({
1006 'indieauth-metadata': [],
1007 'token_endpoint': [],
1011 metadataUrl: undefined,
1012 tokenUrl: undefined,
1014 const result
= await communication
._fetchMetadataOrTokenEndpoint(urlObj
);
1015 assert
.deepStrictEqual(result
, expected
);
1017 it('covers no url', async
function () {
1019 metadataUrl: undefined,
1020 tokenUrl: undefined,
1022 const result
= await communication
._fetchMetadataOrTokenEndpoint();
1023 assert
.deepStrictEqual(result
, expected
);
1025 }); // _fetchMetadataOrTokenEndpoint
1027 describe('redeemTicket', function () {
1028 let ticket
, resourceUrlObj
, issuerUrlObj
;
1029 beforeEach(function () {
1030 resourceUrlObj
= new URL('https://resource.example.com/');
1031 issuerUrlObj
= new URL('https://idp.example.com/');
1032 ticket
= 'XXXThisIsATicketXXX';
1033 sinon
.stub(communication
, '_fetchMetadataOrTokenEndpoint').resolves({
1034 metadataUrl: new URL('https://example.com'),
1035 tokenUrl: undefined,
1037 sinon
.stub(communication
, 'fetchMetadata').resolves({ tokenEndpoint: 'https://idp.example.com/' });
1039 it('covers success', async
function () {
1040 const expected
= { 'access_token': 'XXXThisIsAnAccessTokenXXX' };
1041 const response
= { body: expected
, headers: {}, statusCode: 200 };
1042 communication
.got
.resolves(response
);
1043 const result
= await communication
.redeemTicket(ticket
, resourceUrlObj
, issuerUrlObj
);
1044 assert
.deepStrictEqual(result
, expected
);
1046 it('covers success without issuer', async
function () {
1047 const expected
= { 'access_token': 'XXXThisIsAnAccessTokenXXX' };
1048 const response
= { body: expected
, headers: {}, statusCode: 200 };
1049 communication
.got
.resolves(response
);
1050 const result
= await communication
.redeemTicket(ticket
, resourceUrlObj
);
1051 assert
.deepStrictEqual(result
, expected
);
1053 it('covers got failure', async
function () {
1054 const expectedException
= new Error('oh no');
1055 communication
.got
.rejects(expectedException
);
1056 await assert
.rejects(() => communication
.redeemTicket(ticket
, resourceUrlObj
, issuerUrlObj
), expectedException
);
1058 it('covers no metadata url', async
function () {
1059 communication
._fetchMetadataOrTokenEndpoint
.resolves({ metadataUrl: undefined, tokenUrl: undefined });
1060 await assert
.rejects(() => communication
.redeemTicket(ticket
, resourceUrlObj
, issuerUrlObj
), ValidationError
);
1062 it('covers bad token url', async
function () {
1063 communication
.fetchMetadata
.resolves({ tokenEndpoint: 'not a url' });
1064 await assert
.rejects(() => communication
.redeemTicket(ticket
, resourceUrlObj
, issuerUrlObj
), ValidationError
);
1068 }); // Communication