a94bf90f6dfed98063916f5500af4430e6d64a34
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(function () {
21 communication
= new Communication(stubLogger
, options
);
23 sinon
.stub(communication
, 'axios');
25 afterEach(function () {
29 it('instantiates', function () {
30 assert(communication
);
33 it('covers no config', function () {
34 communication
= new Communication(stubLogger
);
37 describe('Axios timing coverage', function () {
42 it('tags request', function () {
43 communication
.axios
.interceptors
.request
.handlers
[0].fulfilled(request
);
44 assert(request
.startTimestampMs
);
46 it('tags response', function () {
47 communication
.axios
.interceptors
.response
.handlers
[0].fulfilled(response
);
48 assert(response
.elapsedTimeMs
);
50 }); // Axios timing coverage
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('Axios Configurations', function () {
131 let requestUrl
, expectedUrl
;
132 beforeEach(function () {
133 requestUrl
= 'https://example.com/client_id';
134 expectedUrl
= 'https://example.com/client_id';
136 it('_axiosConfig', function () {
137 const method
= 'GET';
138 const contentType
= 'text/plain';
139 const body
= undefined;
141 'extra_parameter': 'foobar',
143 const urlObj
= new URL(requestUrl
);
144 const expectedUrlObj
= new URL(`${requestUrl}?extra_parameter=foobar`);
147 url: 'https://example.com/client_id',
149 'Content-Type': 'text/plain',
151 params: expectedUrlObj
.searchParams
,
152 responseType: 'text',
153 validateStatus: Communication
._validateStatus
,
155 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {
156 'Content-Type': contentType
,
158 delete result
.transformResponse
;
159 assert
.deepStrictEqual(result
, expected
);
161 it('_axiosConfig covers defaults', function () {
162 const method
= 'OPTIONS';
163 const urlObj
= new URL(requestUrl
);
164 const expectedUrlObj
= new URL(requestUrl
);
169 params: expectedUrlObj
.searchParams
,
170 responseType: 'text',
171 validateStatus: Communication
._validateStatus
,
173 const result
= Communication
._axiosConfig(method
, urlObj
);
174 delete result
.transformResponse
;
175 assert
.deepStrictEqual(result
, expected
);
177 it('covers data', function () {
178 const method
= 'POST';
179 const body
= Buffer
.from('some data');
181 const urlObj
= new URL(requestUrl
);
184 url: 'https://example.com/client_id',
187 params: urlObj
.searchParams
,
188 responseType: 'text',
189 validateStatus: Communication
._validateStatus
,
191 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {});
192 delete result
.transformResponse
;
193 assert
.deepStrictEqual(result
, expected
);
195 it('covers null response transform', function () {
196 const urlObj
= new URL(requestUrl
);
197 const result
= Communication
._axiosConfig('GET', urlObj
, undefined, {}, {});
198 result
.transformResponse
[0]();
201 describe('_validateStatus', function () {
202 it('allows normal valid', function () {
203 const result
= Communication
._validateStatus(200);
204 assert
.strictEqual(result
, true);
206 it('allows unauthorized', function () {
207 const result
= Communication
._validateStatus(401);
208 assert
.strictEqual(result
, true);
210 it('rejects invalid', function () {
211 const result
= Communication
._validateStatus(400);
212 assert
.strictEqual(result
, false);
214 }); // _validateStatus
215 }); // Axios Configurations
217 describe('_baseUrlString', function () {
218 it('covers no path', function () {
219 const urlObj
= new URL('https://example.com');
220 const expected
= 'https://example.com/';
221 const result
= Communication
._baseUrlString(urlObj
);
222 assert
.strictEqual(result
, expected
);
224 it('covers paths', function () {
225 const urlObj
= new URL('https://example.com/path/blah');
226 const expected
= 'https://example.com/path/';
227 const result
= Communication
._baseUrlString(urlObj
);
228 assert
.strictEqual(result
, expected
);
230 }); // _baseUrlString
232 describe('_parseContentType', function () {
233 let contentTypeHeader
, expected
, result
;
234 it('covers undefined', function () {
235 contentTypeHeader
= undefined;
237 mediaType: 'application/octet-stream',
240 result
= Communication
._parseContentType(contentTypeHeader
);
241 assert
.deepStrictEqual(result
, expected
);
243 it('covers empty', function () {
244 contentTypeHeader
= '';
246 mediaType: 'application/octet-stream',
249 result
= Communication
._parseContentType(contentTypeHeader
);
250 assert
.deepStrictEqual(result
, expected
);
252 it('covers extra parameters', function () {
253 contentTypeHeader
= 'text/plain; CharSet="UTF-8"; WeirdParam';
255 mediaType: 'text/plain',
258 'weirdparam': undefined,
261 result
= Communication
._parseContentType(contentTypeHeader
);
262 assert
.deepStrictEqual(result
, expected
);
264 }); // parseContentType
266 describe('_mergeLinkHeader', function () {
267 let microformat
, response
, expected
;
268 beforeEach(function () {
272 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
277 it('covers', function () {
281 'hub': ['https://hub.example.com/'],
282 'self': ['https://example.com/'],
285 'https://example.com/': {
289 'https://hub.example.com/': {
295 communication
._mergeLinkHeader(microformat
, response
);
296 assert
.deepStrictEqual(microformat
, expected
);
298 it('covers existing', function () {
302 'preload': ['https://example.com/style'],
303 'hub': ['https://hub.example.com/'],
306 'https://hub.example.com/': {
310 'https://example.com/style': {
319 'preload': ['https://example.com/style'],
320 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
321 'self': ['https://example.com/'],
324 'https://example.com/': {
328 'https://hub.example.com/': {
329 rels: ['hub', 'hub'],
332 'https://example.com/style': {
338 communication
._mergeLinkHeader(microformat
, response
);
339 assert
.deepStrictEqual(microformat
, expected
);
341 it('ignores bad header', function () {
342 response
.headers
.link
= 'not really a link header';
348 communication
._mergeLinkHeader(microformat
, response
);
349 assert
.deepStrictEqual(microformat
, expected
);
351 }); // _mergeLinkHeader
353 describe('fetchMicroformat', function () {
354 let expected
, response
, result
, urlObj
;
355 beforeEach(function () {
356 expected
= undefined;
358 urlObj
= new URL('https://thuza.ratfeathers.com/');
360 headers: Object
.assign({}, testData
.linkHeaders
),
361 data: testData
.hCardHtml
,
364 it('covers', async
function () {
365 response
.data
= testData
.hCardHtml
;
366 communication
.axios
.resolves(response
);
369 'authorization_endpoint': ['https://ia.squeep.com/auth'],
370 'token_endpoint': ['https://ia.squeep.com/token'],
371 'canonical': ['https://thuza.ratfeathers.com/'],
372 'author': ['https://thuza.ratfeathers.com/'],
373 'me': ['https://thuza.ratfeathers.com/'],
374 'self': ['https://thuza.ratfeathers.com/'],
375 'hub': ['https://hub.squeep.com/'],
376 'preload': ['https://thuza.ratfeathers.com/image.png'],
379 'https://hub.squeep.com/': {
383 'https://ia.squeep.com/auth': {
384 rels: ['authorization_endpoint'],
387 'https://ia.squeep.com/token': {
388 rels: ['token_endpoint'],
391 'https://thuza.ratfeathers.com/': {
392 rels: ['self', 'canonical', 'author', 'me'],
395 'https://thuza.ratfeathers.com/image.png': {
403 photo: ['https://thuza.ratfeathers.com/image.png'],
404 url: ['https://thuza.ratfeathers.com/'],
410 result
= await communication
.fetchMicroformat(urlObj
);
411 assert
.deepStrictEqual(result
, expected
);
413 it('covers axios error', async
function () {
414 communication
.axios
.rejects(new Error('blah'));
415 expected
= undefined;
417 result
= await communication
.fetchMicroformat(urlObj
);
419 assert
.deepStrictEqual(result
, expected
);
421 it('covers non-parsable content', async
function () {
422 response
.data
= 'some bare text';
423 response
.headers
= {};
424 communication
.axios
.resolves(response
);
431 result
= await communication
.fetchMicroformat(urlObj
);
433 assert
.deepStrictEqual(result
, expected
);
435 it('covers non-utf8 content', async
function () {
436 response
.headers
['content-type'] = 'text/html; charset=ASCII';
437 communication
.axios
.resolves(response
);
440 'authorization_endpoint': ['https://ia.squeep.com/auth'],
441 'token_endpoint': ['https://ia.squeep.com/token'],
442 'canonical': ['https://thuza.ratfeathers.com/'],
443 'author': ['https://thuza.ratfeathers.com/'],
444 'me': ['https://thuza.ratfeathers.com/'],
445 'self': ['https://thuza.ratfeathers.com/'],
446 'hub': ['https://hub.squeep.com/'],
447 'preload': ['https://thuza.ratfeathers.com/image.png'],
450 'https://hub.squeep.com/': {
454 'https://ia.squeep.com/auth': {
455 rels: ['authorization_endpoint'],
458 'https://ia.squeep.com/token': {
459 rels: ['token_endpoint'],
462 'https://thuza.ratfeathers.com/': {
463 rels: ['self', 'canonical', 'author', 'me'],
466 'https://thuza.ratfeathers.com/image.png': {
474 photo: ['https://thuza.ratfeathers.com/image.png'],
475 url: ['https://thuza.ratfeathers.com/'],
481 result
= await communication
.fetchMicroformat(urlObj
);
483 assert
.deepStrictEqual(result
, expected
);
485 }); // fetchMicroformat
487 describe('fetchJSON', function () {
488 let expected
, response
, result
, urlObj
;
489 beforeEach(function () {
490 expected
= undefined;
492 urlObj
= new URL('https://thuza.ratfeathers.com/');
494 headers: Object
.assign({}, testData
.linkHeaders
),
495 data: testData
.hCardHtml
,
498 it('covers', async
function () {
499 communication
.axios
.resolves(response
);
500 expected
= { foo: 'bar', baz: 123 };
501 response
.data
= JSON
.stringify(expected
);
503 result
= await communication
.fetchJSON(urlObj
);
504 assert
.deepStrictEqual(result
, expected
);
506 it('covers axios error', async
function () {
507 communication
.axios
.rejects(new Error('blah'));
508 expected
= undefined;
510 result
= await communication
.fetchJSON(urlObj
);
512 assert
.deepStrictEqual(result
, expected
);
514 it('covers non-parsable content', async
function () {
515 response
.data
= 'some bare text';
516 response
.headers
= {};
517 communication
.axios
.resolves(response
);
518 expected
= undefined;
520 result
= await communication
.fetchJSON(urlObj
);
522 assert
.deepStrictEqual(result
, expected
);
526 describe('validateProfile', function () {
527 let url
, validationOptions
;
528 beforeEach(function () {
529 url
= 'https://example.com/';
530 validationOptions
= {};
531 sinon
.stub(dns
, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]);
533 it('rejects invalid url', async
function () {
535 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
537 it('covers success', async
function () {
538 const result
= await communication
.validateProfile(url
, validationOptions
);
539 assert
.strictEqual(result
.isLoopback
, false);
541 it('rejects invalid', async
function () {
542 url
= 'ftp://example.com/';
543 await assert
.rejects(() => communication
.validateProfile(url
, validationOptions
), ValidationError
);
546 }); // validateProfile
548 describe('validateClientIdentifier', function () {
549 let url
, validationOptions
;
550 beforeEach(function () {
551 url
= 'https://example.com/';
552 validationOptions
= {};
553 sinon
.stub(dns
, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
555 it('rejects invalid url', async
function () {
556 await assert
.rejects(() => communication
.validateClientIdentifier('bad url'), ValidationError
);
558 it('rejects invalid scheme', async
function () {
559 url
= 'ftp://example.com/';
560 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
562 it('rejects fragment', async
function () {
563 url
= 'https://example.com/#foo';
564 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
566 it('rejects username', async
function () {
567 url
= 'https://user@example.com/';
568 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
570 it('rejects password', async
function () {
571 url
= 'https://:foo@example.com/';
572 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
574 it('rejects relative path', async
function () {
575 url
= 'https://example.com/client/../sneaky';
576 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
578 it('rejects ipv4', async
function () {
579 url
= 'https://10.11.12.13/';
580 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
582 it('rejects ipv6', async
function () {
583 url
= 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
584 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
586 it('accepts ipv4 loopback', async
function () {
587 url
= 'https://127.0.0.1/';
588 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
589 assert
.strictEqual(result
.isLoopback
, true);
591 it('accepts ipv6 loopback', async
function () {
592 url
= 'https://[::1]/';
593 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
594 assert
.strictEqual(result
.isLoopback
, true);
596 it('accepts resolved ipv4 loopback', async
function () {
597 dns
.lookupAsync
.resolves([{ family: 4, address: '127.0.0.1' }]);
598 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
599 assert
.strictEqual(result
.isLoopback
, true);
601 it('accepts resolved ipv6 loopback', async
function () {
602 dns
.lookupAsync
.resolves([{ family: 6, address: '::1' }]);
603 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
604 assert
.strictEqual(result
.isLoopback
, true);
606 it('covers success', async
function () {
607 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
608 assert
.strictEqual(result
.isLoopback
, false);
610 it('rejects resolution failure', async
function () {
611 dns
.lookupAsync
.rejects(new Error('oh no'));
612 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
614 it('rejects mismatched resolutions', async
function () {
615 dns
.lookupAsync
.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
616 await assert
.rejects(() => communication
.validateClientIdentifier(url
, validationOptions
), ValidationError
);
618 it('ignores unknown dns family', async
function () {
619 dns
.lookupAsync
.resolves([{ family: 5, address: '10.9.8.7' }]);
620 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
621 assert
.strictEqual(result
.isLoopback
, false);
623 it('covers rooted hostname', async
function() {
624 url
= 'https://example.com./';
625 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
626 assert
.strictEqual(result
.isLoopback
, false);
628 it('covers unresolved', async
function () {
629 dns
.lookupAsync
.resolves();
630 const result
= await communication
.validateClientIdentifier(url
, validationOptions
);
631 assert
.strictEqual(result
.isLoopback
, false);
633 }); // validateClientIdentifier
635 describe('fetchClientIdentifier', function () {
636 let expected
, response
, result
, urlObj
;
637 beforeEach(function () {
638 expected
= undefined;
640 urlObj
= new URL('https://thuza.ratfeathers.com/');
643 data: testData
.multiMF2Html
,
646 it('covers', async
function () {
647 communication
.axios
.resolves(response
);
651 name: ['Also Some Client'],
652 url: ['https://thuza.ratfeathers.com/'],
657 'author': ['https://thuza.ratfeathers.com/'],
658 'authorization_endpoint': ['https://ia.squeep.com/auth'],
659 'canonical': ['https://thuza.ratfeathers.com/'],
660 'me': ['https://thuza.ratfeathers.com/'],
661 'token_endpoint': ['https://ia.squeep.com/token'],
664 result
= await communication
.fetchClientIdentifier(urlObj
);
665 assert
.deepStrictEqual(result
, expected
);
667 it('covers failed fetch', async
function () {
668 communication
.axios
.rejects();
669 expected
= undefined;
670 result
= await communication
.fetchClientIdentifier(urlObj
);
671 assert
.deepStrictEqual(result
, expected
);
673 it('covers no h-app data', async
function () {
674 response
.data
= testData
.noneMF2Html
;
675 communication
.axios
.resolves(response
);
680 result
= await communication
.fetchClientIdentifier(urlObj
);
681 assert
.deepStrictEqual(result
, expected
);
683 it('covers missing fields', async
function () {
684 sinon
.stub(communication
, 'fetchMicroformat').resolves({});
689 result
= await communication
.fetchClientIdentifier(urlObj
);
690 assert
.deepStrictEqual(result
, expected
);
692 it('covers other missing fields', async
function () {
693 sinon
.stub(communication
, 'fetchMicroformat').resolves({
699 url: ['https://example.com'],
708 result
= await communication
.fetchClientIdentifier(urlObj
);
709 assert
.deepStrictEqual(result
, expected
);
711 it('covers loopback', async
function () {
712 sinon
.spy(communication
, 'fetchMicroformat');
713 urlObj
.isLoopback
= true;
718 result
= await communication
.fetchClientIdentifier(urlObj
);
719 assert
.deepStrictEqual(result
, expected
);
720 assert(communication
.fetchMicroformat
.notCalled
);
722 }); // fetchClientIdentifier
724 describe('fetchProfile', function () {
725 let expected
, response
, result
, urlObj
;
726 beforeEach(function () {
727 expected
= undefined;
729 urlObj
= new URL('https://thuza.ratfeathers.com/');
732 data: testData
.hCardHtml
,
734 sinon
.stub(communication
, 'fetchJSON');
736 describe('legacy without indieauth-metadata', function () {
737 it('covers', async
function () {
738 communication
.axios
.resolves(response
);
741 photo: 'https://thuza.ratfeathers.com/image.png',
742 url: 'https://thuza.ratfeathers.com/',
744 authorizationEndpoint: 'https://ia.squeep.com/auth',
745 tokenEndpoint: 'https://ia.squeep.com/token',
747 authorizationEndpoint: 'https://ia.squeep.com/auth',
748 tokenEndpoint: 'https://ia.squeep.com/token',
751 result
= await communication
.fetchProfile(urlObj
);
752 assert
.deepStrictEqual(result
, expected
);
754 it('covers multiple hCards', async
function () {
755 response
.data
= testData
.multiMF2Html
;
756 communication
.axios
.resolves(response
);
760 photo: 'https://thuza.ratfeathers.com/image.png',
761 url: 'https://thuza.ratfeathers.com/',
762 authorizationEndpoint: 'https://ia.squeep.com/auth',
763 tokenEndpoint: 'https://ia.squeep.com/token',
765 authorizationEndpoint: 'https://ia.squeep.com/auth',
766 tokenEndpoint: 'https://ia.squeep.com/token',
769 result
= await communication
.fetchProfile(urlObj
);
770 assert
.deepStrictEqual(result
, expected
);
772 it('covers failed fetch', async
function () {
773 communication
.axios
.rejects();
781 result
= await communication
.fetchProfile(urlObj
);
782 assert
.deepStrictEqual(result
, expected
);
785 it('covers', async
function () {
786 response
.data
= testData
.hCardMetadataHtml
;
787 communication
.axios
.resolves(response
);
788 communication
.fetchJSON
.resolves({
789 'issuer': 'https://ia.squeep.com/',
790 'authorization_endpoint': 'https://ia.squeep.com/auth',
791 'token_endpoint': 'https://ia.squeep.com/token',
792 'introspection_endpoint': 'https://ia.squeep.com/introspect',
793 'introspection_endpoint_auth_methods_supported': [ '' ],
794 'revocation_endpoint': 'https://ia.squeep.com/revoke',
795 'revocation_endpoint_auth_methods_supported': [ 'none' ],
796 'scopes_supported': [ 'profile', 'email' ],
797 'service_documentation': 'https://indieauth.spec.indieweb.org/',
798 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
799 'authorization_response_iss_parameter_supported': true,
800 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
804 photo: 'https://thuza.ratfeathers.com/image.png',
805 url: 'https://thuza.ratfeathers.com/',
808 authorizationEndpoint: 'https://ia.squeep.com/auth',
809 tokenEndpoint: 'https://ia.squeep.com/token',
810 issuer: 'https://ia.squeep.com/',
811 introspectionEndpoint: 'https://ia.squeep.com/introspect',
812 introspectionEndpointAuthMethodsSupported: [ '' ],
813 revocationEndpoint: 'https://ia.squeep.com/revoke',
814 revocationEndpointAuthMethodsSupported: [ 'none' ],
815 scopesSupported: [ 'profile', 'email' ],
816 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
817 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
818 authorizationResponseIssParameterSupported: true,
819 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
821 authorizationEndpoint: 'https://ia.squeep.com/auth',
822 tokenEndpoint: 'https://ia.squeep.com/token',
823 indieauthMetadata: 'https://ia.squeep.com/meta',
826 result
= await communication
.fetchProfile(urlObj
);
828 assert
.deepStrictEqual(result
, expected
);
830 it('covers metadata missing fields', async
function () {
831 response
.data
= testData
.hCardMetadataHtml
;
832 communication
.axios
.resolves(response
);
833 communication
.fetchJSON
.resolves({
834 'issuer': 'https://ia.squeep.com/',
838 photo: 'https://thuza.ratfeathers.com/image.png',
839 url: 'https://thuza.ratfeathers.com/',
842 issuer: 'https://ia.squeep.com/',
844 indieauthMetadata: 'https://ia.squeep.com/meta',
847 result
= await communication
.fetchProfile(urlObj
);
849 assert
.deepStrictEqual(result
, expected
);
851 it('covers metadata response failure', async
function () {
852 const jsonError
= new Error('oh no');
853 response
.data
= testData
.hCardMetadataHtml
;
855 .onCall(0).resolves(response
)
856 .onCall(1).rejects(jsonError
);
857 communication
.fetchJSON
.restore();
860 photo: 'https://thuza.ratfeathers.com/image.png',
861 url: 'https://thuza.ratfeathers.com/',
864 indieauthMetadata: 'https://ia.squeep.com/meta',
867 result
= await communication
.fetchProfile(urlObj
);
869 assert
.deepStrictEqual(result
, expected
);
873 describe('redeemCode', function () {
874 let expected
, urlObj
, code
, codeVerifier
, clientId
, redirectURI
;
875 beforeEach(function () {
876 urlObj
= new URL('https://example.com/auth');
877 code
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
878 codeVerifier
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
879 clientId
= 'https://example.com/';
880 redirectURI
= 'https://example.com/_ia';
882 it('covers', async
function () {
883 communication
.axios
.resolves({
884 data: '{"me":"https://profile.example.com/"}',
887 me: 'https://profile.example.com/',
890 const result
= await communication
.redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
892 assert
.deepStrictEqual(result
, expected
);
894 it('covers deprecated method name', async
function () {
895 communication
.axios
.resolves({
896 data: '{"me":"https://profile.example.com/"}',
899 me: 'https://profile.example.com/',
902 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
904 assert
.deepStrictEqual(result
, expected
);
906 it('covers failure', async
function () {
907 communication
.axios
.resolves('Not a JSON payload.');
909 const result
= await communication
.redeemCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
911 assert
.strictEqual(result
, undefined);
915 describe('introspectToken', function () {
916 let introspectionUrlObj
, authenticationHeader
, token
;
917 beforeEach(function () {
918 introspectionUrlObj
= new URL('https://ia.example.com/introspect');
919 authenticationHeader
= 'Bearer XXX';
922 it('covers success active', async
function () {
923 const nowEpoch
= Math
.ceil(Date
.now() / 1000);
924 communication
.axios
.resolves({
925 data: JSON
.stringify({
927 me: 'https://profile.example.com/',
928 'client_id': 'https://app.example.com/',
929 scope: 'create profile email',
930 exp: nowEpoch
+ 86400,
934 const result
= await communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
);
935 assert
.strictEqual(result
.active
, true);
937 it('covers success inactive', async
function () {
938 communication
.axios
.resolves({
939 data: JSON
.stringify({
943 const result
= await communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
);
944 assert
.strictEqual(result
.active
, false);
946 it('covers failure', async
function () {
947 communication
.axios
.resolves('what kind of response is this?');
948 await assert
.rejects(() => communication
.introspectToken(introspectionUrlObj
, authenticationHeader
, token
));
950 }); // introspectToken
952 describe('deliverTicket', function () {
953 let ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
;
954 beforeEach(function () {
955 ticketEndpointUrlObj
= new URL('https://ticket.example.com/');
956 resourceUrlObj
= new URL('https://resource.example.com/');
957 subjectUrlObj
= new URL('https://subject.example.com/');
958 ticket
= 'XXXThisIsATicketXXX';
960 it('covers success', async
function () {
961 const expected
= { data: 'blah', statusCode: 200 };
962 communication
.axios
.resolves(expected
);
963 const result
= await communication
.deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
);
964 assert
.deepStrictEqual(result
, expected
);
966 it('covers failure', async
function () {
967 const expectedException
= new Error('oh no');
968 communication
.axios
.rejects(expectedException
);
969 await assert
.rejects(() => communication
.deliverTicket(ticketEndpointUrlObj
, resourceUrlObj
, subjectUrlObj
, ticket
), expectedException
);