initial commit
[squeep-indieauth-helper] / test / lib / communication.js
1 /* eslint-env mocha */
2 /* eslint-disable capitalized-comments, sonarjs/no-duplicate-string */
3
4 'use strict';
5
6 const assert = require('assert');
7 const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
8
9 const Communication = require('../../lib/communication');
10
11 const stubLogger = require('../stub-logger');
12 const testData = require('../test-data/communication');
13
14 const noExpectedException = 'did not get expected exception';
15
16 describe('Communication', function () {
17 let communication, options;
18
19 beforeEach(function () {
20 options = {};
21 communication = new Communication(stubLogger, options);
22 stubLogger._reset();
23 sinon.stub(communication, 'axios');
24 });
25 afterEach(function () {
26 sinon.restore();
27 });
28
29 it('instantiates', function () {
30 assert(communication);
31 });
32
33 it('covers no config', function () {
34 communication = new Communication(stubLogger);
35 });
36
37 describe('Axios timing coverage', function () {
38 const request = {};
39 const response = {
40 config: request,
41 };
42 it('tags request', function () {
43 communication.axios.interceptors.request.handlers[0].fulfilled(request);
44 assert(request.startTimestampMs);
45 });
46 it('tags response', function () {
47 communication.axios.interceptors.response.handlers[0].fulfilled(response);
48 assert(response.elapsedTimeMs);
49 });
50 }); // Axios timing coverage
51
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);
58 });
59 }); // _challengeFromVerifier
60
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');
68 });
69 it('covers error', async function () {
70 try {
71 await Communication.generatePKCE(1);
72 assert.fail(noExpectedException);
73 } catch (e) {
74 assert(e instanceof RangeError);
75 }
76 });
77 }); // generatePKCE
78
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);
86 });
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);
93 });
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);
100 });
101 it('covers unhandled method', function () {
102 const method = 'MD5';
103 const challenge = 'xkfP7DUYDsnu07Kg6ogc8A';
104 const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
105 try {
106 Communication.verifyChallenge(challenge, verifier, method);
107 assert.fail(noExpectedException);
108 } catch (e) {
109 assert(e.message.includes('unsupported'));
110 }
111 });
112 }); // verifyChallenge
113
114 describe('_userAgentString', function () {
115 it('has default behavior', function () {
116 const result = Communication._userAgentString();
117 assert(result);
118 assert(result.length > 30);
119 });
120 it('is settable', function () {
121 const result = Communication._userAgentString({
122 product: 'myClient',
123 version: '9.9.9',
124 implementation: 'custom',
125 });
126 assert(result);
127 assert.strictEqual(result, 'myClient/9.9.9 (custom)');
128 });
129 it('covers branches', function () {
130 const result = Communication._userAgentString({
131 product: 'myClient',
132 version: '9.9.9',
133 implementation: '',
134 });
135 assert(result);
136 assert.strictEqual(result, 'myClient/9.9.9');
137 });
138 }); // userAgentString
139
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';
145 });
146 it('_axiosConfig', function () {
147 const method = 'GET';
148 const contentType = 'text/plain';
149 const body = undefined;
150 const params = {
151 'extra_parameter': 'foobar',
152 };
153 const urlObj = new URL(requestUrl);
154 const expectedUrlObj = new URL(`${requestUrl}?extra_parameter=foobar`);
155 const expected = {
156 method,
157 url: 'https://example.com/client_id',
158 headers: {
159 'Content-Type': 'text/plain',
160 },
161 params: expectedUrlObj.searchParams,
162 responseType: 'text',
163 };
164 const result = Communication._axiosConfig(method, urlObj, body, params, {
165 'Content-Type': contentType,
166 });
167 delete result.transformResponse;
168 assert.deepStrictEqual(result, expected);
169 });
170 it('_axiosConfig covers defaults', function () {
171 const method = 'OPTIONS';
172 const urlObj = new URL(requestUrl);
173 const expectedUrlObj = new URL(requestUrl);
174 const expected = {
175 method,
176 url: expectedUrl,
177 headers: {},
178 params: expectedUrlObj.searchParams,
179 responseType: 'text',
180 };
181 const result = Communication._axiosConfig(method, urlObj);
182 delete result.transformResponse;
183 assert.deepStrictEqual(result, expected);
184 });
185 it('covers data', function () {
186 const method = 'POST';
187 const body = Buffer.from('some data');
188 const params = {};
189 const urlObj = new URL(requestUrl);
190 const expected = {
191 method,
192 url: 'https://example.com/client_id',
193 data: body,
194 headers: {},
195 params: urlObj.searchParams,
196 responseType: 'text',
197 };
198 const result = Communication._axiosConfig(method, urlObj, body, params, {});
199 delete result.transformResponse;
200 assert.deepStrictEqual(result, expected);
201
202 });
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]();
207 });
208 }); // Axios Configurations
209
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);
216 });
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);
222 });
223 }); // _baseUrlString
224
225 describe('_parseContentType', function () {
226 let contentTypeHeader, expected, result;
227 it('covers undefined', function () {
228 contentTypeHeader = undefined;
229 expected = {
230 mediaType: 'application/octet-stream',
231 params: {},
232 };
233 result = Communication._parseContentType(contentTypeHeader);
234 assert.deepStrictEqual(result, expected);
235 });
236 it('covers empty', function () {
237 contentTypeHeader = '';
238 expected = {
239 mediaType: 'application/octet-stream',
240 params: {},
241 };
242 result = Communication._parseContentType(contentTypeHeader);
243 assert.deepStrictEqual(result, expected);
244 });
245 it('covers extra parameters', function () {
246 contentTypeHeader = 'text/plain; CharSet="UTF-8"; WeirdParam';
247 expected = {
248 mediaType: 'text/plain',
249 params: {
250 'charset': 'UTF-8',
251 'weirdparam': undefined,
252 },
253 };
254 result = Communication._parseContentType(contentTypeHeader);
255 assert.deepStrictEqual(result, expected);
256 });
257 }); // parseContentType
258
259 describe('_mergeLinkHeader', function () {
260 let microformat, response, expected;
261 beforeEach(function () {
262 microformat = {};
263 response = {
264 headers: {
265 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
266 },
267 data: {},
268 }
269 });
270 it('covers', function () {
271 expected = {
272 items: [],
273 rels: {
274 'hub': ['https://hub.example.com/'],
275 'self': ['https://example.com/'],
276 },
277 'rel-urls': {
278 'https://example.com/': {
279 rels: ['self'],
280 text: '',
281 },
282 'https://hub.example.com/': {
283 rels: ['hub'],
284 text: '',
285 },
286 },
287 };
288 communication._mergeLinkHeader(microformat, response);
289 assert.deepStrictEqual(microformat, expected);
290 });
291 it('covers existing', function () {
292 microformat = {
293 items: [],
294 rels: {
295 'preload': ['https://example.com/style'],
296 'hub': ['https://hub.example.com/'],
297 },
298 'rel-urls': {
299 'https://hub.example.com/': {
300 rels: ['hub'],
301 text: '',
302 },
303 'https://example.com/style': {
304 rels: ['preload'],
305 text: '',
306 },
307 },
308 };
309 expected = {
310 items: [],
311 rels: {
312 'preload': ['https://example.com/style'],
313 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
314 'self': ['https://example.com/'],
315 },
316 'rel-urls': {
317 'https://example.com/': {
318 rels: ['self'],
319 text: '',
320 },
321 'https://hub.example.com/': {
322 rels: ['hub', 'hub'],
323 text: '',
324 },
325 'https://example.com/style': {
326 rels: ['preload'],
327 text: '',
328 },
329 },
330 };
331 communication._mergeLinkHeader(microformat, response);
332 assert.deepStrictEqual(microformat, expected);
333 });
334 it('ignores bad header', function () {
335 response.headers.link = 'not really a link header';
336 expected = {
337 items: [],
338 rels: {},
339 'rel-urls': {},
340 };
341 communication._mergeLinkHeader(microformat, response);
342 assert.deepStrictEqual(microformat, expected);
343 });
344 }); // _mergeLinkHeader
345
346 describe('fetchMicroformat', function () {
347 let expected, response, result, urlObj;
348 beforeEach(function () {
349 expected = undefined;
350 result = undefined;
351 urlObj = new URL('https://thuza.ratfeathers.com/');
352 response = {
353 headers: Object.assign({}, testData.linkHeaders),
354 data: testData.hCardHtml,
355 };
356 });
357 it('covers', async function () {
358 response.data = testData.hCardHtml;
359 communication.axios.resolves(response);
360 expected = {
361 rels: {
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'],
370 },
371 'rel-urls': {
372 'https://hub.squeep.com/': {
373 rels: ['hub'],
374 text: '',
375 },
376 'https://ia.squeep.com/auth': {
377 rels: ['authorization_endpoint'],
378 text: '',
379 },
380 'https://ia.squeep.com/token': {
381 rels: ['token_endpoint'],
382 text: '',
383 },
384 'https://thuza.ratfeathers.com/': {
385 rels: ['self', 'canonical', 'author', 'me'],
386 text: 'Thuza',
387 },
388 'https://thuza.ratfeathers.com/image.png': {
389 rels: ['preload'],
390 text: '',
391 },
392 },
393 items: [{
394 properties: {
395 name: ['Thuza'],
396 photo: ['https://thuza.ratfeathers.com/image.png'],
397 url: ['https://thuza.ratfeathers.com/'],
398 },
399 type: ['h-card'],
400 }],
401 };
402
403 result = await communication.fetchMicroformat(urlObj);
404 assert.deepStrictEqual(result, expected);
405 });
406 it('covers axios error', async function () {
407 communication.axios.rejects(new Error('blah'));
408 expected = undefined;
409
410 result = await communication.fetchMicroformat(urlObj);
411
412 assert.deepStrictEqual(result, expected);
413 });
414 it('covers non-parsable content', async function () {
415 response.data = 'some bare text';
416 response.headers = {};
417 communication.axios.resolves(response);
418 expected = {
419 items: [],
420 rels: {},
421 'rel-urls': {},
422 };
423
424 result = await communication.fetchMicroformat(urlObj);
425
426 assert.deepStrictEqual(result, expected);
427 });
428 it('covers non-utf8 content', async function () {
429 response.headers['content-type'] = 'text/html; charset=ASCII';
430 communication.axios.resolves(response);
431 expected = {
432 rels: {
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'],
441 },
442 'rel-urls': {
443 'https://hub.squeep.com/': {
444 rels: ['hub'],
445 text: '',
446 },
447 'https://ia.squeep.com/auth': {
448 rels: ['authorization_endpoint'],
449 text: '',
450 },
451 'https://ia.squeep.com/token': {
452 rels: ['token_endpoint'],
453 text: '',
454 },
455 'https://thuza.ratfeathers.com/': {
456 rels: ['self', 'canonical', 'author', 'me'],
457 text: 'Thuza',
458 },
459 'https://thuza.ratfeathers.com/image.png': {
460 rels: ['preload'],
461 text: '',
462 },
463 },
464 items: [{
465 properties: {
466 name: ['Thuza'],
467 photo: ['https://thuza.ratfeathers.com/image.png'],
468 url: ['https://thuza.ratfeathers.com/'],
469 },
470 type: ['h-card'],
471 }],
472 };
473
474 result = await communication.fetchMicroformat(urlObj);
475
476 assert.deepStrictEqual(result, expected);
477 });
478 }); // fetchMicroformat
479
480 describe('fetchClientIdentifier', function () {
481 let expected, response, result, urlObj;
482 beforeEach(function () {
483 expected = undefined;
484 result = undefined;
485 urlObj = new URL('https://thuza.ratfeathers.com/');
486 response = {
487 headers: {},
488 data: testData.multiMF2Html,
489 };
490 });
491 it('covers', async function () {
492 communication.axios.resolves(response);
493 expected = {
494 items: [{
495 properties: {
496 name: ['Also Some Client'],
497 url: ['https://thuza.ratfeathers.com/'],
498 },
499 type: ['h-app'],
500 }],
501 rels: {
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'],
507 },
508 };
509 result = await communication.fetchClientIdentifier(urlObj);
510 assert.deepStrictEqual(result, expected);
511 });
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);
517 });
518 it('covers no h-app data', async function () {
519 response.data = testData.noneMF2Html;
520 communication.axios.resolves(response);
521 expected = {
522 items: [],
523 rels: {},
524 };
525 result = await communication.fetchClientIdentifier(urlObj);
526 assert.deepStrictEqual(result, expected);
527 });
528 it('covers missing fields', async function () {
529 sinon.stub(communication, 'fetchMicroformat').resolves({});
530 expected = {
531 rels: {},
532 items: [],
533 };
534 result = await communication.fetchClientIdentifier(urlObj);
535 assert.deepStrictEqual(result, expected);
536 });
537 it('covers other missing fields', async function () {
538 sinon.stub(communication, 'fetchMicroformat').resolves({
539 items: [
540 {},
541 {
542 type: ['h-app'],
543 properties: {
544 url: ['https://example.com'],
545 },
546 },
547 ],
548 });
549 expected = {
550 rels: {},
551 items: [],
552 };
553 result = await communication.fetchClientIdentifier(urlObj);
554 assert.deepStrictEqual(result, expected);
555 });
556 }); // fetchClientIdentifier
557
558 describe('fetchProfile', function () {
559 let expected, response, result, urlObj;
560 beforeEach(function () {
561 expected = undefined;
562 result = undefined;
563 urlObj = new URL('https://thuza.ratfeathers.com/');
564 response = {
565 headers: {},
566 data: testData.hCardHtml,
567 };
568 });
569 it('covers', async function () {
570 communication.axios.resolves(response);
571 expected = {
572 name: 'Thuza',
573 photo: 'https://thuza.ratfeathers.com/image.png',
574 url: 'https://thuza.ratfeathers.com/',
575 email: undefined,
576 authorizationEndpoint: 'https://ia.squeep.com/auth',
577 tokenEndpoint: 'https://ia.squeep.com/token',
578 };
579 result = await communication.fetchProfile(urlObj);
580 assert.deepStrictEqual(result, expected);
581 });
582 it('covers multiple hCards', async function () {
583 response.data = testData.multiMF2Html;
584 communication.axios.resolves(response);
585 expected = {
586 email: undefined,
587 name: 'Thuza',
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',
592 };
593 result = await communication.fetchProfile(urlObj);
594 assert.deepStrictEqual(result, expected);
595 });
596 it('covers failed fetch', async function () {
597 communication.axios.rejects();
598 expected = {
599 email: undefined,
600 name: undefined,
601 photo: undefined,
602 url: undefined,
603 };
604 result = await communication.fetchProfile(urlObj);
605 assert.deepStrictEqual(result, expected);
606 });
607 }); // fetchProfile
608
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';
617 });
618 it('covers', async function () {
619 communication.axios.resolves({
620 data: '{"me":"https://profile.example.com/"}'
621 });
622 expected = {
623 me: 'https://profile.example.com/',
624 };
625
626 const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
627
628 assert.deepStrictEqual(result, expected);
629 });
630 it('covers failure', async function () {
631 communication.axios.resolves('Not a JSON payload.');
632
633 const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
634
635 assert.strictEqual(result, undefined);
636 });
637 });
638 }); // Communication