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('fetchClientIdentifier', function () {
481 let expected
, response
, result
, urlObj
;
482 beforeEach(function () {
483 expected
= undefined;
485 urlObj
= new URL('https://thuza.ratfeathers.com/');
488 data: testData
.multiMF2Html
,
491 it('covers', async
function () {
492 communication
.axios
.resolves(response
);
496 name: ['Also Some Client'],
497 url: ['https://thuza.ratfeathers.com/'],
502 'author': ['https://thuza.ratfeathers.com/'],
503 'authorization_endpoint': ['https://ia.squeep.com/auth'],
504 'canonical': ['https://thuza.ratfeathers.com/'],
505 'me': ['https://thuza.ratfeathers.com/'],
506 'token_endpoint': ['https://ia.squeep.com/token'],
509 result
= await communication
.fetchClientIdentifier(urlObj
);
510 assert
.deepStrictEqual(result
, expected
);
512 it('covers failed fetch', async
function () {
513 communication
.axios
.rejects();
514 expected
= undefined;
515 result
= await communication
.fetchClientIdentifier(urlObj
);
516 assert
.deepStrictEqual(result
, expected
);
518 it('covers no h-app data', async
function () {
519 response
.data
= testData
.noneMF2Html
;
520 communication
.axios
.resolves(response
);
525 result
= await communication
.fetchClientIdentifier(urlObj
);
526 assert
.deepStrictEqual(result
, expected
);
528 it('covers missing fields', async
function () {
529 sinon
.stub(communication
, 'fetchMicroformat').resolves({});
534 result
= await communication
.fetchClientIdentifier(urlObj
);
535 assert
.deepStrictEqual(result
, expected
);
537 it('covers other missing fields', async
function () {
538 sinon
.stub(communication
, 'fetchMicroformat').resolves({
544 url: ['https://example.com'],
553 result
= await communication
.fetchClientIdentifier(urlObj
);
554 assert
.deepStrictEqual(result
, expected
);
556 }); // fetchClientIdentifier
558 describe('fetchProfile', function () {
559 let expected
, response
, result
, urlObj
;
560 beforeEach(function () {
561 expected
= undefined;
563 urlObj
= new URL('https://thuza.ratfeathers.com/');
566 data: testData
.hCardHtml
,
569 it('covers', async
function () {
570 communication
.axios
.resolves(response
);
573 photo: 'https://thuza.ratfeathers.com/image.png',
574 url: 'https://thuza.ratfeathers.com/',
576 authorizationEndpoint: 'https://ia.squeep.com/auth',
577 tokenEndpoint: 'https://ia.squeep.com/token',
579 result
= await communication
.fetchProfile(urlObj
);
580 assert
.deepStrictEqual(result
, expected
);
582 it('covers multiple hCards', async
function () {
583 response
.data
= testData
.multiMF2Html
;
584 communication
.axios
.resolves(response
);
588 photo: 'https://thuza.ratfeathers.com/image.png',
589 url: 'https://thuza.ratfeathers.com/',
590 authorizationEndpoint: 'https://ia.squeep.com/auth',
591 tokenEndpoint: 'https://ia.squeep.com/token',
593 result
= await communication
.fetchProfile(urlObj
);
594 assert
.deepStrictEqual(result
, expected
);
596 it('covers failed fetch', async
function () {
597 communication
.axios
.rejects();
604 result
= await communication
.fetchProfile(urlObj
);
605 assert
.deepStrictEqual(result
, expected
);
609 describe('redeemProfileCode', function () {
610 let expected
, urlObj
, code
, codeVerifier
, clientId
, redirectURI
;
611 this.beforeEach(function () {
612 urlObj
= new URL('https://example.com/auth');
613 code
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
614 codeVerifier
= Buffer
.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
615 clientId
= 'https://example.com/';
616 redirectURI
= 'https://example.com/_ia';
618 it('covers', async
function () {
619 communication
.axios
.resolves({
620 data: '{"me":"https://profile.example.com/"}'
623 me: 'https://profile.example.com/',
626 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
628 assert
.deepStrictEqual(result
, expected
);
630 it('covers failure', async
function () {
631 communication
.axios
.resolves('Not a JSON payload.');
633 const result
= await communication
.redeemProfileCode(urlObj
, code
, codeVerifier
, clientId
, redirectURI
);
635 assert
.strictEqual(result
, undefined);