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 const noExpectedException
= 'did not get expected exception';
18 describe('Communication', function () {
19 let communication
, options
;
21 beforeEach(function () {
23 communication
= new Communication(stubLogger
, options
);
25 sinon
.stub(communication
, 'axios');
27 afterEach(function () {
31 it('instantiates', function () {
32 assert(communication
);
35 it('covers no config', function () {
36 communication
= new Communication(stubLogger
);
39 describe('Axios timing coverage', function () {
44 it('tags request', function () {
45 communication
.axios
.interceptors
.request
.handlers
[0].fulfilled(request
);
46 assert(request
.startTimestampMs
);
48 it('tags response', function () {
49 communication
.axios
.interceptors
.response
.handlers
[0].fulfilled(response
);
50 assert(response
.elapsedTimeMs
);
52 }); // Axios timing coverage
54 describe('_challengeFromVerifier', function () {
55 it('covers', function () {
56 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
57 const expected
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
58 const result
= Communication
._challengeFromVerifier(verifier
);
59 assert
.strictEqual(result
, expected
);
61 }); // _challengeFromVerifier
63 describe('generatePKCE', function () {
64 it('covers', async
function () {
65 const result
= await Communication
.generatePKCE();
66 assert(result
.codeVerifier
);
67 assert(result
.codeChallenge
);
68 assert(result
.codeChallengeMethod
);
69 assert
.strictEqual(result
.codeChallengeMethod
, 'S256');
71 it('covers error', async
function () {
73 await Communication
.generatePKCE(1);
74 assert
.fail(noExpectedException
);
76 assert(e
instanceof RangeError
);
81 describe('verifyChallenge', function () {
82 it('covers success', function () {
83 const method
= 'S256';
84 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
85 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
86 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
87 assert
.strictEqual(result
, true);
89 it('also covers success', function () {
90 const method
= 'SHA256';
91 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
92 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
93 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
94 assert
.strictEqual(result
, true);
96 it('covers failure', function () {
97 const method
= 'S256';
98 const challenge
= 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
99 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
100 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
101 assert
.strictEqual(result
, false);
103 it('covers unhandled method', function () {
104 const method
= 'MD5';
105 const challenge
= 'xkfP7DUYDsnu07Kg6ogc8A';
106 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
108 Communication
.verifyChallenge(challenge
, verifier
, method
);
109 assert
.fail(noExpectedException
);
111 assert(e
.message
.includes('unsupported'));
114 }); // verifyChallenge
116 describe('_userAgentString', function () {
117 it('has default behavior', function () {
118 const result
= Communication
._userAgentString();
120 assert(result
.length
> 30);
122 it('is settable', function () {
123 const result
= Communication
._userAgentString({
126 implementation: 'custom',
129 assert
.strictEqual(result
, 'myClient/9.9.9 (custom)');
131 it('covers branches', function () {
132 const result
= Communication
._userAgentString({
138 assert
.strictEqual(result
, 'myClient/9.9.9');
140 }); // userAgentString
142 describe('Axios Configurations', function () {
143 let requestUrl
, expectedUrl
;
144 beforeEach(function () {
145 requestUrl
= 'https://example.com/client_id';
146 expectedUrl
= 'https://example.com/client_id';
148 it('_axiosConfig', function () {
149 const method
= 'GET';
150 const contentType
= 'text/plain';
151 const body
= undefined;
153 'extra_parameter': 'foobar',
155 const urlObj
= new URL(requestUrl
);
156 const expectedUrlObj
= new URL(`${requestUrl}?extra_parameter=foobar`);
159 url: 'https://example.com/client_id',
161 'Content-Type': 'text/plain',
163 params: expectedUrlObj
.searchParams
,
164 responseType: 'text',
166 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {
167 'Content-Type': contentType
,
169 delete result
.transformResponse
;
170 assert
.deepStrictEqual(result
, expected
);
172 it('_axiosConfig covers defaults', function () {
173 const method
= 'OPTIONS';
174 const urlObj
= new URL(requestUrl
);
175 const expectedUrlObj
= new URL(requestUrl
);
180 params: expectedUrlObj
.searchParams
,
181 responseType: 'text',
183 const result
= Communication
._axiosConfig(method
, urlObj
);
184 delete result
.transformResponse
;
185 assert
.deepStrictEqual(result
, expected
);
187 it('covers data', function () {
188 const method
= 'POST';
189 const body
= Buffer
.from('some data');
191 const urlObj
= new URL(requestUrl
);
194 url: 'https://example.com/client_id',
197 params: urlObj
.searchParams
,
198 responseType: 'text',
200 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {});
201 delete result
.transformResponse
;
202 assert
.deepStrictEqual(result
, expected
);
205 it('covers null response transform', function () {
206 const urlObj
= new URL(requestUrl
);
207 const result
= Communication
._axiosConfig('GET', urlObj
, undefined, {}, {});
208 result
.transformResponse
[0]();
210 }); // Axios Configurations
212 describe('_baseUrlString', function () {
213 it('covers no path', function () {
214 const urlObj
= new URL('https://example.com');
215 const expected
= 'https://example.com/';
216 const result
= Communication
._baseUrlString(urlObj
);
217 assert
.strictEqual(result
, expected
);
219 it('covers paths', function () {
220 const urlObj
= new URL('https://example.com/path/blah');
221 const expected
= 'https://example.com/path/';
222 const result
= Communication
._baseUrlString(urlObj
);
223 assert
.strictEqual(result
, expected
);
225 }); // _baseUrlString
227 describe('_parseContentType', function () {
228 let contentTypeHeader
, expected
, result
;
229 it('covers undefined', function () {
230 contentTypeHeader
= undefined;
232 mediaType: 'application/octet-stream',
235 result
= Communication
._parseContentType(contentTypeHeader
);
236 assert
.deepStrictEqual(result
, expected
);
238 it('covers empty', function () {
239 contentTypeHeader
= '';
241 mediaType: 'application/octet-stream',
244 result
= Communication
._parseContentType(contentTypeHeader
);
245 assert
.deepStrictEqual(result
, expected
);
247 it('covers extra parameters', function () {
248 contentTypeHeader
= 'text/plain; CharSet="UTF-8"; WeirdParam';
250 mediaType: 'text/plain',
253 'weirdparam': undefined,
256 result
= Communication
._parseContentType(contentTypeHeader
);
257 assert
.deepStrictEqual(result
, expected
);
259 }); // parseContentType
261 describe('_mergeLinkHeader', function () {
262 let microformat
, response
, expected
;
263 beforeEach(function () {
267 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
272 it('covers', function () {
276 'hub': ['https://hub.example.com/'],
277 'self': ['https://example.com/'],
280 'https://example.com/': {
284 'https://hub.example.com/': {
290 communication
._mergeLinkHeader(microformat
, response
);
291 assert
.deepStrictEqual(microformat
, expected
);
293 it('covers existing', function () {
297 'preload': ['https://example.com/style'],
298 'hub': ['https://hub.example.com/'],
301 'https://hub.example.com/': {
305 'https://example.com/style': {
314 'preload': ['https://example.com/style'],
315 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
316 'self': ['https://example.com/'],
319 'https://example.com/': {
323 'https://hub.example.com/': {
324 rels: ['hub', 'hub'],
327 'https://example.com/style': {
333 communication
._mergeLinkHeader(microformat
, response
);
334 assert
.deepStrictEqual(microformat
, expected
);
336 it('ignores bad header', function () {
337 response
.headers
.link
= 'not really a link header';
343 communication
._mergeLinkHeader(microformat
, response
);
344 assert
.deepStrictEqual(microformat
, expected
);
346 }); // _mergeLinkHeader
348 describe('fetchMicroformat', function () {
349 let expected
, response
, result
, urlObj
;
350 beforeEach(function () {
351 expected
= undefined;
353 urlObj
= new URL('https://thuza.ratfeathers.com/');
355 headers: Object
.assign({}, testData
.linkHeaders
),
356 data: testData
.hCardHtml
,
359 it('covers', async
function () {
360 response
.data
= testData
.hCardHtml
;
361 communication
.axios
.resolves(response
);
364 'authorization_endpoint': ['https://ia.squeep.com/auth'],
365 'token_endpoint': ['https://ia.squeep.com/token'],
366 'canonical': ['https://thuza.ratfeathers.com/'],
367 'author': ['https://thuza.ratfeathers.com/'],
368 'me': ['https://thuza.ratfeathers.com/'],
369 'self': ['https://thuza.ratfeathers.com/'],
370 'hub': ['https://hub.squeep.com/'],
371 'preload': ['https://thuza.ratfeathers.com/image.png'],
374 'https://hub.squeep.com/': {
378 'https://ia.squeep.com/auth': {
379 rels: ['authorization_endpoint'],
382 'https://ia.squeep.com/token': {
383 rels: ['token_endpoint'],
386 'https://thuza.ratfeathers.com/': {
387 rels: ['self', 'canonical', 'author', 'me'],
390 'https://thuza.ratfeathers.com/image.png': {
398 photo: ['https://thuza.ratfeathers.com/image.png'],
399 url: ['https://thuza.ratfeathers.com/'],
405 result
= await communication
.fetchMicroformat(urlObj
);
406 assert
.deepStrictEqual(result
, expected
);
408 it('covers axios error', async
function () {
409 communication
.axios
.rejects(new Error('blah'));
410 expected
= undefined;
412 result
= await communication
.fetchMicroformat(urlObj
);
414 assert
.deepStrictEqual(result
, expected
);
416 it('covers non-parsable content', async
function () {
417 response
.data
= 'some bare text';
418 response
.headers
= {};
419 communication
.axios
.resolves(response
);
426 result
= await communication
.fetchMicroformat(urlObj
);
428 assert
.deepStrictEqual(result
, expected
);
430 it('covers non-utf8 content', async
function () {
431 response
.headers
['content-type'] = 'text/html; charset=ASCII';
432 communication
.axios
.resolves(response
);
435 'authorization_endpoint': ['https://ia.squeep.com/auth'],
436 'token_endpoint': ['https://ia.squeep.com/token'],
437 'canonical': ['https://thuza.ratfeathers.com/'],
438 'author': ['https://thuza.ratfeathers.com/'],
439 'me': ['https://thuza.ratfeathers.com/'],
440 'self': ['https://thuza.ratfeathers.com/'],
441 'hub': ['https://hub.squeep.com/'],
442 'preload': ['https://thuza.ratfeathers.com/image.png'],
445 'https://hub.squeep.com/': {
449 'https://ia.squeep.com/auth': {
450 rels: ['authorization_endpoint'],
453 'https://ia.squeep.com/token': {
454 rels: ['token_endpoint'],
457 'https://thuza.ratfeathers.com/': {
458 rels: ['self', 'canonical', 'author', 'me'],
461 'https://thuza.ratfeathers.com/image.png': {
469 photo: ['https://thuza.ratfeathers.com/image.png'],
470 url: ['https://thuza.ratfeathers.com/'],
476 result
= await communication
.fetchMicroformat(urlObj
);
478 assert
.deepStrictEqual(result
, expected
);
480 }); // fetchMicroformat
482 describe('fetchJSON', function () {
483 let expected
, response
, result
, urlObj
;
484 beforeEach(function () {
485 expected
= undefined;
487 urlObj
= new URL('https://thuza.ratfeathers.com/');
489 headers: Object
.assign({}, testData
.linkHeaders
),
490 data: testData
.hCardHtml
,
493 it('covers', async
function () {
494 communication
.axios
.resolves(response
);
495 expected
= { foo: 'bar', baz: 123 };
496 response
.data
= JSON
.stringify(expected
);
498 result
= await communication
.fetchJSON(urlObj
);
499 assert
.deepStrictEqual(result
, expected
);
501 it('covers axios error', async
function () {
502 communication
.axios
.rejects(new Error('blah'));
503 expected
= undefined;
505 result
= await communication
.fetchJSON(urlObj
);
507 assert
.deepStrictEqual(result
, expected
);
509 it('covers non-parsable content', async
function () {
510 response
.data
= 'some bare text';
511 response
.headers
= {};
512 communication
.axios
.resolves(response
);
513 expected
= undefined;
515 result
= await communication
.fetchJSON(urlObj
);
517 assert
.deepStrictEqual(result
, expected
);
521 describe('validateProfile', function () {
522 let url
, validationOptions
;
523 beforeEach(function () {
524 url
= 'https://example.com/';
526 sinon
.stub(dns
, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]);
528 it('rejects invalid url', async
function () {
530 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
532 it('covers success', async
function () {
533 const result
= await communication
.validateProfile(url
, validationOptions
);
534 assert
.strictEqual(result
.isLoopback
, false);
536 it('rejects invalid', async
function () {
537 url
= 'ftp://example.com/';
538 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
541 }); // validateProfile
543 describe('validateClientIdentifier', function () {
544 let url
, validationOptions
;
545 beforeEach(function () {
546 url
= 'https://example.com/';
548 sinon
.stub(dns
, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
550 it('rejects invalid url', async
function () {
552 await communication
.validateClientIdentifier('bad url');
553 assert
.fail(noExpectedException
);
555 assert(e
instanceof ValidationError
);
558 it('rejects invalid scheme', async
function () {
559 url
= 'ftp://example.com/';
561 await communication
.validateClientIdentifier(url
, validationOptions
);
562 assert
.fail(noExpectedException
);
564 assert(e
instanceof ValidationError
);
567 it('rejects fragment', async
function () {
568 url
= 'https://example.com/#foo';
570 await communication
.validateClientIdentifier(url
, validationOptions
);
571 assert
.fail(noExpectedException
);
573 assert(e
instanceof ValidationError
);
576 it('rejects username', async
function () {
577 url
= 'https://user@example.com/';
579 await communication
.validateClientIdentifier(url
, validationOptions
);
580 assert
.fail(noExpectedException
);
582 assert(e
instanceof ValidationError
);
585 it('rejects password', async
function () {
586 url
= 'https://:foo@example.com/';
588 await communication
.validateClientIdentifier(url
, validationOptions
);
589 assert
.fail(noExpectedException
);
591 assert(e
instanceof ValidationError
);
594 it('rejects relative path', async
function () {
595 url
= 'https://example.com/client/../sneaky';
597 await communication
.validateClientIdentifier(url
, validationOptions
);
598 assert
.fail(noExpectedException
);
600 assert(e
instanceof ValidationError
);
603 it('rejects ipv4', async
function () {
604 url
= 'https://10.11.12.13/';
606 await communication
.validateClientIdentifier(url
, validationOptions
);
607 assert
.fail(noExpectedException
);
609 assert(e
instanceof ValidationError
);
612 it('rejects ipv6', async
function () {
613 url
= 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
615 await communication
.validateClientIdentifier(url
, validationOptions
);
616 assert
.fail(noExpectedException
);
618 assert(e
instanceof ValidationError
);
621 it('accepts ipv4 loopback', async
function () {
622 url
= 'https://127.0.0.1/';
623 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
624 assert
.strictEqual(result
.isLoopback
, true);
626 it('accepts ipv6 loopback', async
function () {
627 url
= 'https://[::1]/';
628 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
629 assert
.strictEqual(result
.isLoopback
, true);
631 it('accepts resolved ipv4 loopback', async
function () {
632 dns
.lookupAsync
.resolves([{ family: 4, address: '127.0.0.1' }]);
633 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
634 assert
.strictEqual(result
.isLoopback
, true);
636 it('accepts resolved ipv6 loopback', async
function () {
637 dns
.lookupAsync
.resolves([{ family: 6, address: '::1' }]);
638 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
639 assert
.strictEqual(result
.isLoopback
, true);
641 it('covers success', async
function () {
642 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
643 assert
.strictEqual(result
.isLoopback
, false);
645 it('rejects resolution failure', async
function () {
646 dns
.lookupAsync
.rejects(new Error('oh no'));
648 await communication
.validateClientIdentifier(url
, validationOptions
);
649 assert
.fail(noExpectedException
);
651 assert(e
instanceof ValidationError
);
654 it('rejects mismatched resolutions', async
function () {
655 dns
.lookupAsync
.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
657 await communication
.validateClientIdentifier(url
, validationOptions
);
658 assert
.fail(noExpectedException
);
660 assert(e
instanceof ValidationError
);
663 it('ignores unknown dns family', async
function () {
664 dns
.lookupAsync
.resolves([{ family: 5, address: '10.9.8.7' }]);
665 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
666 assert
.strictEqual(result
.isLoopback
, false);
668 it('covers rooted hostname', async
function() {
669 url
= 'https://example.com./';
670 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
671 assert
.strictEqual(result
.isLoopback
, false);
673 it('covers unresolved', async
function () {
674 dns
.lookupAsync
.resolves();
675 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
676 assert
.strictEqual(result
.isLoopback
, false);
678 }); // validateClientIdentifier
680 describe('fetchClientIdentifier', function () {
681 let expected
, response
, result
, urlObj
;
682 beforeEach(function () {
683 expected
= undefined;
685 urlObj
= new URL('https://thuza.ratfeathers.com/');
688 data: testData
.multiMF2Html
,
691 it('covers', async
function () {
692 communication
.axios
.resolves(response
);
696 name: ['Also Some Client'],
697 url: ['https://thuza.ratfeathers.com/'],
702 'author': ['https://thuza.ratfeathers.com/'],
703 'authorization_endpoint': ['https://ia.squeep.com/auth'],
704 'canonical': ['https://thuza.ratfeathers.com/'],
705 'me': ['https://thuza.ratfeathers.com/'],
706 'token_endpoint': ['https://ia.squeep.com/token'],
709 result
= await communication
.fetchClientIdentifier(urlObj
);
710 assert
.deepStrictEqual(result
, expected
);
712 it('covers failed fetch', async
function () {
713 communication
.axios
.rejects();
714 expected
= undefined;
715 result
= await communication
.fetchClientIdentifier(urlObj
);
716 assert
.deepStrictEqual(result
, expected
);
718 it('covers no h-app data', async
function () {
719 response
.data
= testData
.noneMF2Html
;
720 communication
.axios
.resolves(response
);
725 result
= await communication
.fetchClientIdentifier(urlObj
);
726 assert
.deepStrictEqual(result
, expected
);
728 it('covers missing fields', async
function () {
729 sinon
.stub(communication
, 'fetchMicroformat').resolves({});
734 result
= await communication
.fetchClientIdentifier(urlObj
);
735 assert
.deepStrictEqual(result
, expected
);
737 it('covers other missing fields', async
function () {
738 sinon
.stub(communication
, 'fetchMicroformat').resolves({
744 url: ['https://example.com'],
753 result
= await communication
.fetchClientIdentifier(urlObj
);
754 assert
.deepStrictEqual(result
, expected
);
756 it('covers loopback', async
function () {
757 sinon
.spy(communication
, 'fetchMicroformat');
758 urlObj
.isLoopback
= true;
763 result
= await communication
.fetchClientIdentifier(urlObj
);
764 assert
.deepStrictEqual(result
, expected
);
765 assert(communication
.fetchMicroformat
.notCalled
);
767 }); // fetchClientIdentifier
769 describe('fetchProfile', function () {
770 let expected
, response
, result
, urlObj
;
771 beforeEach(function () {
772 expected
= undefined;
774 urlObj
= new URL('https://thuza.ratfeathers.com/');
777 data: testData
.hCardHtml
,
779 sinon
.stub(communication
, 'fetchJSON');
781 describe('legacy without indieauth-metadata', function () {
782 it('covers', async
function () {
783 communication
.axios
.resolves(response
);
786 photo: 'https://thuza.ratfeathers.com/image.png',
787 url: 'https://thuza.ratfeathers.com/',
789 authorizationEndpoint: 'https://ia.squeep.com/auth',
790 tokenEndpoint: 'https://ia.squeep.com/token',
792 authorizationEndpoint: 'https://ia.squeep.com/auth',
793 tokenEndpoint: 'https://ia.squeep.com/token',
796 result
= await communication
.fetchProfile(urlObj
);
797 assert
.deepStrictEqual(result
, expected
);
799 it('covers multiple hCards', async
function () {
800 response
.data
= testData
.multiMF2Html
;
801 communication
.axios
.resolves(response
);
805 photo: 'https://thuza.ratfeathers.com/image.png',
806 url: 'https://thuza.ratfeathers.com/',
807 authorizationEndpoint: 'https://ia.squeep.com/auth',
808 tokenEndpoint: 'https://ia.squeep.com/token',
810 authorizationEndpoint: 'https://ia.squeep.com/auth',
811 tokenEndpoint: 'https://ia.squeep.com/token',
814 result
= await communication
.fetchProfile(urlObj
);
815 assert
.deepStrictEqual(result
, expected
);
817 it('covers failed fetch', async
function () {
818 communication
.axios
.rejects();
826 result
= await communication
.fetchProfile(urlObj
);
827 assert
.deepStrictEqual(result
, expected
);
830 it('covers', async
function () {
831 response
.data
= testData
.hCardMetadataHtml
;
832 communication
.axios
.resolves(response
);
833 communication
.fetchJSON
.resolves({
834 'issuer': 'https://ia.squeep.com/',
835 'authorization_endpoint': 'https://ia.squeep.com/auth',
836 'token_endpoint': 'https://ia.squeep.com/token',
837 'introspection_endpoint': 'https://ia.squeep.com/introspect',
838 'introspection_endpoint_auth_methods_supported': [ '' ],
839 'revocation_endpoint': 'https://ia.squeep.com/revoke',
840 'revocation_endpoint_auth_methods_supported': [ 'none' ],
841 'scopes_supported': [ 'profile', 'email' ],
842 'service_documentation': 'https://indieauth.spec.indieweb.org/',
843 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
844 'authorization_response_iss_parameter_supported': true,
845 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
849 photo: 'https://thuza.ratfeathers.com/image.png',
850 url: 'https://thuza.ratfeathers.com/',
853 authorizationEndpoint: 'https://ia.squeep.com/auth',
854 tokenEndpoint: 'https://ia.squeep.com/token',
855 issuer: 'https://ia.squeep.com/',
856 introspectionEndpoint: 'https://ia.squeep.com/introspect',
857 introspectionEndpointAuthMethodsSupported: [ '' ],
858 revocationEndpoint: 'https://ia.squeep.com/revoke',
859 revocationEndpointAuthMethodsSupported: [ 'none' ],
860 scopesSupported: [ 'profile', 'email' ],
861 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
862 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
863 authorizationResponseIssParameterSupported: true,
864 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
866 authorizationEndpoint: 'https://ia.squeep.com/auth',
867 tokenEndpoint: 'https://ia.squeep.com/token',
868 indieauthMetadata: 'https://ia.squeep.com/meta',
871 result
= await communication
.fetchProfile(urlObj
);
873 assert
.deepStrictEqual(result
, expected
);
875 it('covers metadata missing fields', async
function () {
876 response
.data
= testData
.hCardMetadataHtml
;
877 communication
.axios
.resolves(response
);
878 communication
.fetchJSON
.resolves({
879 'issuer': 'https://ia.squeep.com/',
883 photo: 'https://thuza.ratfeathers.com/image.png',
884 url: 'https://thuza.ratfeathers.com/',
887 issuer: 'https://ia.squeep.com/',
889 indieauthMetadata: 'https://ia.squeep.com/meta',
892 result
= await communication
.fetchProfile(urlObj
);
894 assert
.deepStrictEqual(result
, expected
);
896 it('covers metadata response failure', async
function () {
897 const jsonError
= new Error('oh no');
898 response
.data
= testData
.hCardMetadataHtml
;
900 .onCall(0).resolves(response
)
901 .onCall(1).rejects(jsonError
);
902 communication
.fetchJSON
.restore();
905 photo: 'https://thuza.ratfeathers.com/image.png',
906 url: 'https://thuza.ratfeathers.com/',
909 indieauthMetadata: 'https://ia.squeep.com/meta',
912 result
= await communication
.fetchProfile(urlObj
);
914 assert
.deepStrictEqual(result
, expected
);
918 describe('redeemProfileCode', function () {
919 let expected
, urlObj
, code
, codeVerifier
, clientId
, redirectURI
;
920 this.beforeEach(function () {
921 urlObj
= new URL('https://example.com/auth');
922 code
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
923 codeVerifier
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
924 clientId
= 'https://example.com/';
925 redirectURI
= 'https://example.com/_ia';
927 it('covers', async
function () {
928 communication
.axios
.resolves({
929 data: '{"me":"https://profile.example.com/"}',
932 me: 'https://profile.example.com/',
935 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
937 assert
.deepStrictEqual(result
, expected
);
939 it('covers failure', async
function () {
940 communication
.axios
.resolves('Not a JSON payload.');
942 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
944 assert
.strictEqual(result
, undefined);