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');
11 const stubLogger
= require('../stub-logger');
12 const testData
= require('../test-data/communication');
14 const noExpectedException
= 'did not get expected exception';
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 () {
71 await Communication
.generatePKCE(1);
72 assert
.fail(noExpectedException
);
74 assert(e
instanceof RangeError
);
79 describe('verifyChallenge', function () {
80 it('covers success', function () {
81 const method
= 'S256';
82 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
83 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
84 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
85 assert
.strictEqual(result
, true);
87 it('also covers success', function () {
88 const method
= 'SHA256';
89 const challenge
= 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
90 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
91 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
92 assert
.strictEqual(result
, true);
94 it('covers failure', function () {
95 const method
= 'S256';
96 const challenge
= 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
97 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
98 const result
= Communication
.verifyChallenge(challenge
, verifier
, method
);
99 assert
.strictEqual(result
, false);
101 it('covers unhandled method', function () {
102 const method
= 'MD5';
103 const challenge
= 'xkfP7DUYDsnu07Kg6ogc8A';
104 const verifier
= 'VGhpcyBpcyBhIHNlY3JldC4u';
106 Communication
.verifyChallenge(challenge
, verifier
, method
);
107 assert
.fail(noExpectedException
);
109 assert(e
.message
.includes('unsupported'));
112 }); // verifyChallenge
114 describe('_userAgentString', function () {
115 it('has default behavior', function () {
116 const result
= Communication
._userAgentString();
118 assert(result
.length
> 30);
120 it('is settable', function () {
121 const result
= Communication
._userAgentString({
124 implementation: 'custom',
127 assert
.strictEqual(result
, 'myClient/9.9.9 (custom)');
129 it('covers branches', function () {
130 const result
= Communication
._userAgentString({
136 assert
.strictEqual(result
, 'myClient/9.9.9');
138 }); // userAgentString
140 describe('Axios Configurations', function () {
141 let requestUrl
, expectedUrl
;
142 beforeEach(function () {
143 requestUrl
= 'https://example.com/client_id';
144 expectedUrl
= 'https://example.com/client_id';
146 it('_axiosConfig', function () {
147 const method
= 'GET';
148 const contentType
= 'text/plain';
149 const body
= undefined;
151 'extra_parameter': 'foobar',
153 const urlObj
= new URL(requestUrl
);
154 const expectedUrlObj
= new URL(`${requestUrl}?extra_parameter=foobar`);
157 url: 'https://example.com/client_id',
159 'Content-Type': 'text/plain',
161 params: expectedUrlObj
.searchParams
,
162 responseType: 'text',
164 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {
165 'Content-Type': contentType
,
167 delete result
.transformResponse
;
168 assert
.deepStrictEqual(result
, expected
);
170 it('_axiosConfig covers defaults', function () {
171 const method
= 'OPTIONS';
172 const urlObj
= new URL(requestUrl
);
173 const expectedUrlObj
= new URL(requestUrl
);
178 params: expectedUrlObj
.searchParams
,
179 responseType: 'text',
181 const result
= Communication
._axiosConfig(method
, urlObj
);
182 delete result
.transformResponse
;
183 assert
.deepStrictEqual(result
, expected
);
185 it('covers data', function () {
186 const method
= 'POST';
187 const body
= Buffer
.from('some data');
189 const urlObj
= new URL(requestUrl
);
192 url: 'https://example.com/client_id',
195 params: urlObj
.searchParams
,
196 responseType: 'text',
198 const result
= Communication
._axiosConfig(method
, urlObj
, body
, params
, {});
199 delete result
.transformResponse
;
200 assert
.deepStrictEqual(result
, expected
);
203 it('covers null response transform', function () {
204 const urlObj
= new URL(requestUrl
);
205 const result
= Communication
._axiosConfig('GET', urlObj
, undefined, {}, {});
206 result
.transformResponse
[0]();
208 }); // Axios Configurations
210 describe('_baseUrlString', function () {
211 it('covers no path', function () {
212 const urlObj
= new URL('https://example.com');
213 const expected
= 'https://example.com/';
214 const result
= Communication
._baseUrlString(urlObj
);
215 assert
.strictEqual(result
, expected
);
217 it('covers paths', function () {
218 const urlObj
= new URL('https://example.com/path/blah');
219 const expected
= 'https://example.com/path/';
220 const result
= Communication
._baseUrlString(urlObj
);
221 assert
.strictEqual(result
, expected
);
223 }); // _baseUrlString
225 describe('_parseContentType', function () {
226 let contentTypeHeader
, expected
, result
;
227 it('covers undefined', function () {
228 contentTypeHeader
= undefined;
230 mediaType: 'application/octet-stream',
233 result
= Communication
._parseContentType(contentTypeHeader
);
234 assert
.deepStrictEqual(result
, expected
);
236 it('covers empty', function () {
237 contentTypeHeader
= '';
239 mediaType: 'application/octet-stream',
242 result
= Communication
._parseContentType(contentTypeHeader
);
243 assert
.deepStrictEqual(result
, expected
);
245 it('covers extra parameters', function () {
246 contentTypeHeader
= 'text/plain; CharSet="UTF-8"; WeirdParam';
248 mediaType: 'text/plain',
251 'weirdparam': undefined,
254 result
= Communication
._parseContentType(contentTypeHeader
);
255 assert
.deepStrictEqual(result
, expected
);
257 }); // parseContentType
259 describe('_mergeLinkHeader', function () {
260 let microformat
, response
, expected
;
261 beforeEach(function () {
265 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
270 it('covers', function () {
274 'hub': ['https://hub.example.com/'],
275 'self': ['https://example.com/'],
278 'https://example.com/': {
282 'https://hub.example.com/': {
288 communication
._mergeLinkHeader(microformat
, response
);
289 assert
.deepStrictEqual(microformat
, expected
);
291 it('covers existing', function () {
295 'preload': ['https://example.com/style'],
296 'hub': ['https://hub.example.com/'],
299 'https://hub.example.com/': {
303 'https://example.com/style': {
312 'preload': ['https://example.com/style'],
313 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
314 'self': ['https://example.com/'],
317 'https://example.com/': {
321 'https://hub.example.com/': {
322 rels: ['hub', 'hub'],
325 'https://example.com/style': {
331 communication
._mergeLinkHeader(microformat
, response
);
332 assert
.deepStrictEqual(microformat
, expected
);
334 it('ignores bad header', function () {
335 response
.headers
.link
= 'not really a link header';
341 communication
._mergeLinkHeader(microformat
, response
);
342 assert
.deepStrictEqual(microformat
, expected
);
344 }); // _mergeLinkHeader
346 describe('fetchMicroformat', function () {
347 let expected
, response
, result
, urlObj
;
348 beforeEach(function () {
349 expected
= undefined;
351 urlObj
= new URL('https://thuza.ratfeathers.com/');
353 headers: Object
.assign({}, testData
.linkHeaders
),
354 data: testData
.hCardHtml
,
357 it('covers', async
function () {
358 response
.data
= testData
.hCardHtml
;
359 communication
.axios
.resolves(response
);
362 'authorization_endpoint': ['https://ia.squeep.com/auth'],
363 'token_endpoint': ['https://ia.squeep.com/token'],
364 'canonical': ['https://thuza.ratfeathers.com/'],
365 'author': ['https://thuza.ratfeathers.com/'],
366 'me': ['https://thuza.ratfeathers.com/'],
367 'self': ['https://thuza.ratfeathers.com/'],
368 'hub': ['https://hub.squeep.com/'],
369 'preload': ['https://thuza.ratfeathers.com/image.png'],
372 'https://hub.squeep.com/': {
376 'https://ia.squeep.com/auth': {
377 rels: ['authorization_endpoint'],
380 'https://ia.squeep.com/token': {
381 rels: ['token_endpoint'],
384 'https://thuza.ratfeathers.com/': {
385 rels: ['self', 'canonical', 'author', 'me'],
388 'https://thuza.ratfeathers.com/image.png': {
396 photo: ['https://thuza.ratfeathers.com/image.png'],
397 url: ['https://thuza.ratfeathers.com/'],
403 result
= await communication
.fetchMicroformat(urlObj
);
404 assert
.deepStrictEqual(result
, expected
);
406 it('covers axios error', async
function () {
407 communication
.axios
.rejects(new Error('blah'));
408 expected
= undefined;
410 result
= await communication
.fetchMicroformat(urlObj
);
412 assert
.deepStrictEqual(result
, expected
);
414 it('covers non-parsable content', async
function () {
415 response
.data
= 'some bare text';
416 response
.headers
= {};
417 communication
.axios
.resolves(response
);
424 result
= await communication
.fetchMicroformat(urlObj
);
426 assert
.deepStrictEqual(result
, expected
);
428 it('covers non-utf8 content', async
function () {
429 response
.headers
['content-type'] = 'text/html; charset=ASCII';
430 communication
.axios
.resolves(response
);
433 'authorization_endpoint': ['https://ia.squeep.com/auth'],
434 'token_endpoint': ['https://ia.squeep.com/token'],
435 'canonical': ['https://thuza.ratfeathers.com/'],
436 'author': ['https://thuza.ratfeathers.com/'],
437 'me': ['https://thuza.ratfeathers.com/'],
438 'self': ['https://thuza.ratfeathers.com/'],
439 'hub': ['https://hub.squeep.com/'],
440 'preload': ['https://thuza.ratfeathers.com/image.png'],
443 'https://hub.squeep.com/': {
447 'https://ia.squeep.com/auth': {
448 rels: ['authorization_endpoint'],
451 'https://ia.squeep.com/token': {
452 rels: ['token_endpoint'],
455 'https://thuza.ratfeathers.com/': {
456 rels: ['self', 'canonical', 'author', 'me'],
459 'https://thuza.ratfeathers.com/image.png': {
467 photo: ['https://thuza.ratfeathers.com/image.png'],
468 url: ['https://thuza.ratfeathers.com/'],
474 result
= await communication
.fetchMicroformat(urlObj
);
476 assert
.deepStrictEqual(result
, expected
);
478 }); // fetchMicroformat
480 describe('fetchJSON', function () {
481 let expected
, response
, result
, urlObj
;
482 beforeEach(function () {
483 expected
= undefined;
485 urlObj
= new URL('https://thuza.ratfeathers.com/');
487 headers: Object
.assign({}, testData
.linkHeaders
),
488 data: testData
.hCardHtml
,
491 it('covers', async
function () {
492 communication
.axios
.resolves(response
);
493 expected
= { foo: 'bar', baz: 123 };
494 response
.data
= JSON
.stringify(expected
);
496 result
= await communication
.fetchJSON(urlObj
);
497 assert
.deepStrictEqual(result
, expected
);
499 it('covers axios error', async
function () {
500 communication
.axios
.rejects(new Error('blah'));
501 expected
= undefined;
503 result
= await communication
.fetchJSON(urlObj
);
505 assert
.deepStrictEqual(result
, expected
);
507 it('covers non-parsable content', async
function () {
508 response
.data
= 'some bare text';
509 response
.headers
= {};
510 communication
.axios
.resolves(response
);
511 expected
= undefined;
513 result
= await communication
.fetchJSON(urlObj
);
515 assert
.deepStrictEqual(result
, expected
);
519 describe('fetchClientIdentifier', function () {
520 let expected
, response
, result
, urlObj
;
521 beforeEach(function () {
522 expected
= undefined;
524 urlObj
= new URL('https://thuza.ratfeathers.com/');
527 data: testData
.multiMF2Html
,
530 it('covers', async
function () {
531 communication
.axios
.resolves(response
);
535 name: ['Also Some Client'],
536 url: ['https://thuza.ratfeathers.com/'],
541 'author': ['https://thuza.ratfeathers.com/'],
542 'authorization_endpoint': ['https://ia.squeep.com/auth'],
543 'canonical': ['https://thuza.ratfeathers.com/'],
544 'me': ['https://thuza.ratfeathers.com/'],
545 'token_endpoint': ['https://ia.squeep.com/token'],
548 result
= await communication
.fetchClientIdentifier(urlObj
);
549 assert
.deepStrictEqual(result
, expected
);
551 it('covers failed fetch', async
function () {
552 communication
.axios
.rejects();
553 expected
= undefined;
554 result
= await communication
.fetchClientIdentifier(urlObj
);
555 assert
.deepStrictEqual(result
, expected
);
557 it('covers no h-app data', async
function () {
558 response
.data
= testData
.noneMF2Html
;
559 communication
.axios
.resolves(response
);
564 result
= await communication
.fetchClientIdentifier(urlObj
);
565 assert
.deepStrictEqual(result
, expected
);
567 it('covers missing fields', async
function () {
568 sinon
.stub(communication
, 'fetchMicroformat').resolves({});
573 result
= await communication
.fetchClientIdentifier(urlObj
);
574 assert
.deepStrictEqual(result
, expected
);
576 it('covers other missing fields', async
function () {
577 sinon
.stub(communication
, 'fetchMicroformat').resolves({
583 url: ['https://example.com'],
592 result
= await communication
.fetchClientIdentifier(urlObj
);
593 assert
.deepStrictEqual(result
, expected
);
595 }); // fetchClientIdentifier
597 describe('fetchProfile', function () {
598 let expected
, response
, result
, urlObj
;
599 beforeEach(function () {
600 expected
= undefined;
602 urlObj
= new URL('https://thuza.ratfeathers.com/');
605 data: testData
.hCardHtml
,
607 sinon
.stub(communication
, 'fetchJSON');
609 describe('legacy without indieauth-metadata', function () {
610 it('covers', async
function () {
611 communication
.axios
.resolves(response
);
614 photo: 'https://thuza.ratfeathers.com/image.png',
615 url: 'https://thuza.ratfeathers.com/',
617 authorizationEndpoint: 'https://ia.squeep.com/auth',
618 tokenEndpoint: 'https://ia.squeep.com/token',
620 authorizationEndpoint: 'https://ia.squeep.com/auth',
621 tokenEndpoint: 'https://ia.squeep.com/token',
624 result
= await communication
.fetchProfile(urlObj
);
625 assert
.deepStrictEqual(result
, expected
);
627 it('covers multiple hCards', async
function () {
628 response
.data
= testData
.multiMF2Html
;
629 communication
.axios
.resolves(response
);
633 photo: 'https://thuza.ratfeathers.com/image.png',
634 url: 'https://thuza.ratfeathers.com/',
635 authorizationEndpoint: 'https://ia.squeep.com/auth',
636 tokenEndpoint: 'https://ia.squeep.com/token',
638 authorizationEndpoint: 'https://ia.squeep.com/auth',
639 tokenEndpoint: 'https://ia.squeep.com/token',
642 result
= await communication
.fetchProfile(urlObj
);
643 assert
.deepStrictEqual(result
, expected
);
645 it('covers failed fetch', async
function () {
646 communication
.axios
.rejects();
654 result
= await communication
.fetchProfile(urlObj
);
655 assert
.deepStrictEqual(result
, expected
);
658 it('covers', async
function () {
659 response
.data
= testData
.hCardMetadataHtml
;
660 communication
.axios
.resolves(response
);
661 communication
.fetchJSON
.resolves({
662 'issuer': 'https://ia.squeep.com/',
663 'authorization_endpoint': 'https://ia.squeep.com/auth',
664 'token_endpoint': 'https://ia.squeep.com/token',
665 'introspection_endpoint': 'https://ia.squeep.com/introspect',
666 'introspection_endpoint_auth_methods_supported': [ '' ],
667 'revocation_endpoint': 'https://ia.squeep.com/revoke',
668 'revocation_endpoint_auth_methods_supported': [ 'none' ],
669 'scopes_supported': [ 'profile', 'email' ],
670 'service_documentation': 'https://indieauth.spec.indieweb.org/',
671 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
672 'authorization_response_iss_parameter_supported': true,
673 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
677 photo: 'https://thuza.ratfeathers.com/image.png',
678 url: 'https://thuza.ratfeathers.com/',
681 authorizationEndpoint: 'https://ia.squeep.com/auth',
682 tokenEndpoint: 'https://ia.squeep.com/token',
683 issuer: 'https://ia.squeep.com/',
684 introspectionEndpoint: 'https://ia.squeep.com/introspect',
685 introspectionEndpointAuthMethodsSupported: [ '' ],
686 revocationEndpoint: 'https://ia.squeep.com/revoke',
687 revocationEndpointAuthMethodsSupported: [ 'none' ],
688 scopesSupported: [ 'profile', 'email' ],
689 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
690 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
691 authorizationResponseIssParameterSupported: true,
692 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
694 authorizationEndpoint: 'https://ia.squeep.com/auth',
695 tokenEndpoint: 'https://ia.squeep.com/token',
696 indieauthMetadata: 'https://ia.squeep.com/meta',
699 result
= await communication
.fetchProfile(urlObj
);
701 assert
.deepStrictEqual(result
, expected
);
703 it('covers metadata missing fields', async
function () {
704 response
.data
= testData
.hCardMetadataHtml
;
705 communication
.axios
.resolves(response
);
706 communication
.fetchJSON
.resolves({
707 'issuer': 'https://ia.squeep.com/',
711 photo: 'https://thuza.ratfeathers.com/image.png',
712 url: 'https://thuza.ratfeathers.com/',
715 issuer: 'https://ia.squeep.com/',
717 indieauthMetadata: 'https://ia.squeep.com/meta',
720 result
= await communication
.fetchProfile(urlObj
);
722 assert
.deepStrictEqual(result
, expected
);
724 it('covers metadata response failure', async
function () {
725 const jsonError
= new Error('oh no');
726 response
.data
= testData
.hCardMetadataHtml
;
728 .onCall(0).resolves(response
)
729 .onCall(1).rejects(jsonError
);
730 communication
.fetchJSON
.restore();
733 photo: 'https://thuza.ratfeathers.com/image.png',
734 url: 'https://thuza.ratfeathers.com/',
737 indieauthMetadata: 'https://ia.squeep.com/meta',
740 result
= await communication
.fetchProfile(urlObj
);
742 assert
.deepStrictEqual(result
, expected
);
746 describe('redeemProfileCode', function () {
747 let expected
, urlObj
, code
, codeVerifier
, clientId
, redirectURI
;
748 this.beforeEach(function () {
749 urlObj
= new URL('https://example.com/auth');
750 code
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
751 codeVerifier
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
752 clientId
= 'https://example.com/';
753 redirectURI
= 'https://example.com/_ia';
755 it('covers', async
function () {
756 communication
.axios
.resolves({
757 data: '{"me":"https://profile.example.com/"}',
760 me: 'https://profile.example.com/',
763 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
765 assert
.deepStrictEqual(result
, expected
);
767 it('covers failure', async
function () {
768 communication
.axios
.resolves('Not a JSON payload.');
770 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
772 assert
.strictEqual(result
, undefined);