9781cd65b05088664a1d1dee04282d12ff31bf05
[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 const { ValidationError } = require('../../lib/errors');
11 const dns = require('dns');
12
13 const stubLogger = require('../stub-logger');
14 const testData = require('../test-data/communication');
15
16 const noExpectedException = 'did not get expected exception';
17
18 describe('Communication', function () {
19 let communication, options;
20
21 beforeEach(function () {
22 options = {};
23 communication = new Communication(stubLogger, options);
24 stubLogger._reset();
25 sinon.stub(communication, 'axios');
26 });
27 afterEach(function () {
28 sinon.restore();
29 });
30
31 it('instantiates', function () {
32 assert(communication);
33 });
34
35 it('covers no config', function () {
36 communication = new Communication(stubLogger);
37 });
38
39 describe('Axios timing coverage', function () {
40 const request = {};
41 const response = {
42 config: request,
43 };
44 it('tags request', function () {
45 communication.axios.interceptors.request.handlers[0].fulfilled(request);
46 assert(request.startTimestampMs);
47 });
48 it('tags response', function () {
49 communication.axios.interceptors.response.handlers[0].fulfilled(response);
50 assert(response.elapsedTimeMs);
51 });
52 }); // Axios timing coverage
53
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);
60 });
61 }); // _challengeFromVerifier
62
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');
70 });
71 it('covers error', async function () {
72 try {
73 await Communication.generatePKCE(1);
74 assert.fail(noExpectedException);
75 } catch (e) {
76 assert(e instanceof RangeError);
77 }
78 });
79 }); // generatePKCE
80
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);
88 });
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);
95 });
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);
102 });
103 it('covers unhandled method', function () {
104 const method = 'MD5';
105 const challenge = 'xkfP7DUYDsnu07Kg6ogc8A';
106 const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
107 try {
108 Communication.verifyChallenge(challenge, verifier, method);
109 assert.fail(noExpectedException);
110 } catch (e) {
111 assert(e.message.includes('unsupported'));
112 }
113 });
114 }); // verifyChallenge
115
116 describe('_userAgentString', function () {
117 it('has default behavior', function () {
118 const result = Communication._userAgentString();
119 assert(result);
120 assert(result.length > 30);
121 });
122 it('is settable', function () {
123 const result = Communication._userAgentString({
124 product: 'myClient',
125 version: '9.9.9',
126 implementation: 'custom',
127 });
128 assert(result);
129 assert.strictEqual(result, 'myClient/9.9.9 (custom)');
130 });
131 it('covers branches', function () {
132 const result = Communication._userAgentString({
133 product: 'myClient',
134 version: '9.9.9',
135 implementation: '',
136 });
137 assert(result);
138 assert.strictEqual(result, 'myClient/9.9.9');
139 });
140 }); // userAgentString
141
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';
147 });
148 it('_axiosConfig', function () {
149 const method = 'GET';
150 const contentType = 'text/plain';
151 const body = undefined;
152 const params = {
153 'extra_parameter': 'foobar',
154 };
155 const urlObj = new URL(requestUrl);
156 const expectedUrlObj = new URL(`${requestUrl}?extra_parameter=foobar`);
157 const expected = {
158 method,
159 url: 'https://example.com/client_id',
160 headers: {
161 'Content-Type': 'text/plain',
162 },
163 params: expectedUrlObj.searchParams,
164 responseType: 'text',
165 validateStatus: Communication._validateStatus,
166 };
167 const result = Communication._axiosConfig(method, urlObj, body, params, {
168 'Content-Type': contentType,
169 });
170 delete result.transformResponse;
171 assert.deepStrictEqual(result, expected);
172 });
173 it('_axiosConfig covers defaults', function () {
174 const method = 'OPTIONS';
175 const urlObj = new URL(requestUrl);
176 const expectedUrlObj = new URL(requestUrl);
177 const expected = {
178 method,
179 url: expectedUrl,
180 headers: {},
181 params: expectedUrlObj.searchParams,
182 responseType: 'text',
183 validateStatus: Communication._validateStatus,
184 };
185 const result = Communication._axiosConfig(method, urlObj);
186 delete result.transformResponse;
187 assert.deepStrictEqual(result, expected);
188 });
189 it('covers data', function () {
190 const method = 'POST';
191 const body = Buffer.from('some data');
192 const params = {};
193 const urlObj = new URL(requestUrl);
194 const expected = {
195 method,
196 url: 'https://example.com/client_id',
197 data: body,
198 headers: {},
199 params: urlObj.searchParams,
200 responseType: 'text',
201 validateStatus: Communication._validateStatus,
202 };
203 const result = Communication._axiosConfig(method, urlObj, body, params, {});
204 delete result.transformResponse;
205 assert.deepStrictEqual(result, expected);
206 });
207 it('covers null response transform', function () {
208 const urlObj = new URL(requestUrl);
209 const result = Communication._axiosConfig('GET', urlObj, undefined, {}, {});
210 result.transformResponse[0]();
211 });
212
213 describe('_validateStatus', function () {
214 it('allows normal valid', function () {
215 const result = Communication._validateStatus(200);
216 assert.strictEqual(result, true);
217 });
218 it('allows unauthorized', function () {
219 const result = Communication._validateStatus(401);
220 assert.strictEqual(result, true);
221 });
222 it('rejects invalid', function () {
223 const result = Communication._validateStatus(400);
224 assert.strictEqual(result, false);
225 });
226 }); // _validateStatus
227 }); // Axios Configurations
228
229 describe('_baseUrlString', function () {
230 it('covers no path', function () {
231 const urlObj = new URL('https://example.com');
232 const expected = 'https://example.com/';
233 const result = Communication._baseUrlString(urlObj);
234 assert.strictEqual(result, expected);
235 });
236 it('covers paths', function () {
237 const urlObj = new URL('https://example.com/path/blah');
238 const expected = 'https://example.com/path/';
239 const result = Communication._baseUrlString(urlObj);
240 assert.strictEqual(result, expected);
241 });
242 }); // _baseUrlString
243
244 describe('_parseContentType', function () {
245 let contentTypeHeader, expected, result;
246 it('covers undefined', function () {
247 contentTypeHeader = undefined;
248 expected = {
249 mediaType: 'application/octet-stream',
250 params: {},
251 };
252 result = Communication._parseContentType(contentTypeHeader);
253 assert.deepStrictEqual(result, expected);
254 });
255 it('covers empty', function () {
256 contentTypeHeader = '';
257 expected = {
258 mediaType: 'application/octet-stream',
259 params: {},
260 };
261 result = Communication._parseContentType(contentTypeHeader);
262 assert.deepStrictEqual(result, expected);
263 });
264 it('covers extra parameters', function () {
265 contentTypeHeader = 'text/plain; CharSet="UTF-8"; WeirdParam';
266 expected = {
267 mediaType: 'text/plain',
268 params: {
269 'charset': 'UTF-8',
270 'weirdparam': undefined,
271 },
272 };
273 result = Communication._parseContentType(contentTypeHeader);
274 assert.deepStrictEqual(result, expected);
275 });
276 }); // parseContentType
277
278 describe('_mergeLinkHeader', function () {
279 let microformat, response, expected;
280 beforeEach(function () {
281 microformat = {};
282 response = {
283 headers: {
284 link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
285 },
286 data: {},
287 }
288 });
289 it('covers', function () {
290 expected = {
291 items: [],
292 rels: {
293 'hub': ['https://hub.example.com/'],
294 'self': ['https://example.com/'],
295 },
296 'rel-urls': {
297 'https://example.com/': {
298 rels: ['self'],
299 text: '',
300 },
301 'https://hub.example.com/': {
302 rels: ['hub'],
303 text: '',
304 },
305 },
306 };
307 communication._mergeLinkHeader(microformat, response);
308 assert.deepStrictEqual(microformat, expected);
309 });
310 it('covers existing', function () {
311 microformat = {
312 items: [],
313 rels: {
314 'preload': ['https://example.com/style'],
315 'hub': ['https://hub.example.com/'],
316 },
317 'rel-urls': {
318 'https://hub.example.com/': {
319 rels: ['hub'],
320 text: '',
321 },
322 'https://example.com/style': {
323 rels: ['preload'],
324 text: '',
325 },
326 },
327 };
328 expected = {
329 items: [],
330 rels: {
331 'preload': ['https://example.com/style'],
332 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
333 'self': ['https://example.com/'],
334 },
335 'rel-urls': {
336 'https://example.com/': {
337 rels: ['self'],
338 text: '',
339 },
340 'https://hub.example.com/': {
341 rels: ['hub', 'hub'],
342 text: '',
343 },
344 'https://example.com/style': {
345 rels: ['preload'],
346 text: '',
347 },
348 },
349 };
350 communication._mergeLinkHeader(microformat, response);
351 assert.deepStrictEqual(microformat, expected);
352 });
353 it('ignores bad header', function () {
354 response.headers.link = 'not really a link header';
355 expected = {
356 items: [],
357 rels: {},
358 'rel-urls': {},
359 };
360 communication._mergeLinkHeader(microformat, response);
361 assert.deepStrictEqual(microformat, expected);
362 });
363 }); // _mergeLinkHeader
364
365 describe('fetchMicroformat', function () {
366 let expected, response, result, urlObj;
367 beforeEach(function () {
368 expected = undefined;
369 result = undefined;
370 urlObj = new URL('https://thuza.ratfeathers.com/');
371 response = {
372 headers: Object.assign({}, testData.linkHeaders),
373 data: testData.hCardHtml,
374 };
375 });
376 it('covers', async function () {
377 response.data = testData.hCardHtml;
378 communication.axios.resolves(response);
379 expected = {
380 rels: {
381 'authorization_endpoint': ['https://ia.squeep.com/auth'],
382 'token_endpoint': ['https://ia.squeep.com/token'],
383 'canonical': ['https://thuza.ratfeathers.com/'],
384 'author': ['https://thuza.ratfeathers.com/'],
385 'me': ['https://thuza.ratfeathers.com/'],
386 'self': ['https://thuza.ratfeathers.com/'],
387 'hub': ['https://hub.squeep.com/'],
388 'preload': ['https://thuza.ratfeathers.com/image.png'],
389 },
390 'rel-urls': {
391 'https://hub.squeep.com/': {
392 rels: ['hub'],
393 text: '',
394 },
395 'https://ia.squeep.com/auth': {
396 rels: ['authorization_endpoint'],
397 text: '',
398 },
399 'https://ia.squeep.com/token': {
400 rels: ['token_endpoint'],
401 text: '',
402 },
403 'https://thuza.ratfeathers.com/': {
404 rels: ['self', 'canonical', 'author', 'me'],
405 text: 'Thuza',
406 },
407 'https://thuza.ratfeathers.com/image.png': {
408 rels: ['preload'],
409 text: '',
410 },
411 },
412 items: [{
413 properties: {
414 name: ['Thuza'],
415 photo: ['https://thuza.ratfeathers.com/image.png'],
416 url: ['https://thuza.ratfeathers.com/'],
417 },
418 type: ['h-card'],
419 }],
420 };
421
422 result = await communication.fetchMicroformat(urlObj);
423 assert.deepStrictEqual(result, expected);
424 });
425 it('covers axios error', async function () {
426 communication.axios.rejects(new Error('blah'));
427 expected = undefined;
428
429 result = await communication.fetchMicroformat(urlObj);
430
431 assert.deepStrictEqual(result, expected);
432 });
433 it('covers non-parsable content', async function () {
434 response.data = 'some bare text';
435 response.headers = {};
436 communication.axios.resolves(response);
437 expected = {
438 items: [],
439 rels: {},
440 'rel-urls': {},
441 };
442
443 result = await communication.fetchMicroformat(urlObj);
444
445 assert.deepStrictEqual(result, expected);
446 });
447 it('covers non-utf8 content', async function () {
448 response.headers['content-type'] = 'text/html; charset=ASCII';
449 communication.axios.resolves(response);
450 expected = {
451 rels: {
452 'authorization_endpoint': ['https://ia.squeep.com/auth'],
453 'token_endpoint': ['https://ia.squeep.com/token'],
454 'canonical': ['https://thuza.ratfeathers.com/'],
455 'author': ['https://thuza.ratfeathers.com/'],
456 'me': ['https://thuza.ratfeathers.com/'],
457 'self': ['https://thuza.ratfeathers.com/'],
458 'hub': ['https://hub.squeep.com/'],
459 'preload': ['https://thuza.ratfeathers.com/image.png'],
460 },
461 'rel-urls': {
462 'https://hub.squeep.com/': {
463 rels: ['hub'],
464 text: '',
465 },
466 'https://ia.squeep.com/auth': {
467 rels: ['authorization_endpoint'],
468 text: '',
469 },
470 'https://ia.squeep.com/token': {
471 rels: ['token_endpoint'],
472 text: '',
473 },
474 'https://thuza.ratfeathers.com/': {
475 rels: ['self', 'canonical', 'author', 'me'],
476 text: 'Thuza',
477 },
478 'https://thuza.ratfeathers.com/image.png': {
479 rels: ['preload'],
480 text: '',
481 },
482 },
483 items: [{
484 properties: {
485 name: ['Thuza'],
486 photo: ['https://thuza.ratfeathers.com/image.png'],
487 url: ['https://thuza.ratfeathers.com/'],
488 },
489 type: ['h-card'],
490 }],
491 };
492
493 result = await communication.fetchMicroformat(urlObj);
494
495 assert.deepStrictEqual(result, expected);
496 });
497 }); // fetchMicroformat
498
499 describe('fetchJSON', function () {
500 let expected, response, result, urlObj;
501 beforeEach(function () {
502 expected = undefined;
503 result = undefined;
504 urlObj = new URL('https://thuza.ratfeathers.com/');
505 response = {
506 headers: Object.assign({}, testData.linkHeaders),
507 data: testData.hCardHtml,
508 };
509 });
510 it('covers', async function () {
511 communication.axios.resolves(response);
512 expected = { foo: 'bar', baz: 123 };
513 response.data = JSON.stringify(expected);
514
515 result = await communication.fetchJSON(urlObj);
516 assert.deepStrictEqual(result, expected);
517 });
518 it('covers axios error', async function () {
519 communication.axios.rejects(new Error('blah'));
520 expected = undefined;
521
522 result = await communication.fetchJSON(urlObj);
523
524 assert.deepStrictEqual(result, expected);
525 });
526 it('covers non-parsable content', async function () {
527 response.data = 'some bare text';
528 response.headers = {};
529 communication.axios.resolves(response);
530 expected = undefined;
531
532 result = await communication.fetchJSON(urlObj);
533
534 assert.deepStrictEqual(result, expected);
535 });
536 }); // fetchJSON
537
538 describe('validateProfile', function () {
539 let url, validationOptions;
540 beforeEach(function () {
541 url = 'https://example.com/';
542 options = {};
543 sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]);
544 });
545 it('rejects invalid url', async function () {
546 url = 'bad url';
547 await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError);
548 });
549 it('covers success', async function () {
550 const result = await communication.validateProfile(url, validationOptions);
551 assert.strictEqual(result.isLoopback, false);
552 });
553 it('rejects invalid', async function () {
554 url = 'ftp://example.com/';
555 await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError);
556 });
557
558 }); // validateProfile
559
560 describe('validateClientIdentifier', function () {
561 let url, validationOptions;
562 beforeEach(function () {
563 url = 'https://example.com/';
564 options = {};
565 sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
566 });
567 it('rejects invalid url', async function () {
568 try {
569 await communication.validateClientIdentifier('bad url');
570 assert.fail(noExpectedException);
571 } catch (e) {
572 assert(e instanceof ValidationError);
573 }
574 });
575 it('rejects invalid scheme', async function () {
576 url = 'ftp://example.com/';
577 try {
578 await communication.validateClientIdentifier(url, validationOptions);
579 assert.fail(noExpectedException);
580 } catch (e) {
581 assert(e instanceof ValidationError);
582 }
583 });
584 it('rejects fragment', async function () {
585 url = 'https://example.com/#foo';
586 try {
587 await communication.validateClientIdentifier(url, validationOptions);
588 assert.fail(noExpectedException);
589 } catch (e) {
590 assert(e instanceof ValidationError);
591 }
592 });
593 it('rejects username', async function () {
594 url = 'https://user@example.com/';
595 try {
596 await communication.validateClientIdentifier(url, validationOptions);
597 assert.fail(noExpectedException);
598 } catch (e) {
599 assert(e instanceof ValidationError);
600 }
601 });
602 it('rejects password', async function () {
603 url = 'https://:foo@example.com/';
604 try {
605 await communication.validateClientIdentifier(url, validationOptions);
606 assert.fail(noExpectedException);
607 } catch (e) {
608 assert(e instanceof ValidationError);
609 }
610 });
611 it('rejects relative path', async function () {
612 url = 'https://example.com/client/../sneaky';
613 try {
614 await communication.validateClientIdentifier(url, validationOptions);
615 assert.fail(noExpectedException);
616 } catch (e) {
617 assert(e instanceof ValidationError);
618 }
619 });
620 it('rejects ipv4', async function () {
621 url = 'https://10.11.12.13/';
622 try {
623 await communication.validateClientIdentifier(url, validationOptions);
624 assert.fail(noExpectedException);
625 } catch (e) {
626 assert(e instanceof ValidationError);
627 }
628 });
629 it('rejects ipv6', async function () {
630 url = 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
631 try {
632 await communication.validateClientIdentifier(url, validationOptions);
633 assert.fail(noExpectedException);
634 } catch (e) {
635 assert(e instanceof ValidationError);
636 }
637 });
638 it('accepts ipv4 loopback', async function () {
639 url = 'https://127.0.0.1/';
640 const result = await communication.validateClientIdentifier(url, validationOptions);
641 assert.strictEqual(result.isLoopback, true);
642 });
643 it('accepts ipv6 loopback', async function () {
644 url = 'https://[::1]/';
645 const result = await communication.validateClientIdentifier(url, validationOptions);
646 assert.strictEqual(result.isLoopback, true);
647 });
648 it('accepts resolved ipv4 loopback', async function () {
649 dns.lookupAsync.resolves([{ family: 4, address: '127.0.0.1' }]);
650 const result = await communication.validateClientIdentifier(url, validationOptions);
651 assert.strictEqual(result.isLoopback, true);
652 });
653 it('accepts resolved ipv6 loopback', async function () {
654 dns.lookupAsync.resolves([{ family: 6, address: '::1' }]);
655 const result = await communication.validateClientIdentifier(url, validationOptions);
656 assert.strictEqual(result.isLoopback, true);
657 });
658 it('covers success', async function () {
659 const result = await communication.validateClientIdentifier(url, validationOptions);
660 assert.strictEqual(result.isLoopback, false);
661 });
662 it('rejects resolution failure', async function () {
663 dns.lookupAsync.rejects(new Error('oh no'));
664 try {
665 await communication.validateClientIdentifier(url, validationOptions);
666 assert.fail(noExpectedException);
667 } catch (e) {
668 assert(e instanceof ValidationError);
669 }
670 });
671 it('rejects mismatched resolutions', async function () {
672 dns.lookupAsync.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
673 try {
674 await communication.validateClientIdentifier(url, validationOptions);
675 assert.fail(noExpectedException);
676 } catch (e) {
677 assert(e instanceof ValidationError);
678 }
679 });
680 it('ignores unknown dns family', async function () {
681 dns.lookupAsync.resolves([{ family: 5, address: '10.9.8.7' }]);
682 const result = await communication.validateClientIdentifier(url, validationOptions);
683 assert.strictEqual(result.isLoopback, false);
684 });
685 it('covers rooted hostname', async function() {
686 url = 'https://example.com./';
687 const result = await communication.validateClientIdentifier(url, validationOptions);
688 assert.strictEqual(result.isLoopback, false);
689 });
690 it('covers unresolved', async function () {
691 dns.lookupAsync.resolves();
692 const result = await communication.validateClientIdentifier(url, validationOptions);
693 assert.strictEqual(result.isLoopback, false);
694 });
695 }); // validateClientIdentifier
696
697 describe('fetchClientIdentifier', function () {
698 let expected, response, result, urlObj;
699 beforeEach(function () {
700 expected = undefined;
701 result = undefined;
702 urlObj = new URL('https://thuza.ratfeathers.com/');
703 response = {
704 headers: {},
705 data: testData.multiMF2Html,
706 };
707 });
708 it('covers', async function () {
709 communication.axios.resolves(response);
710 expected = {
711 items: [{
712 properties: {
713 name: ['Also Some Client'],
714 url: ['https://thuza.ratfeathers.com/'],
715 },
716 type: ['h-app'],
717 }],
718 rels: {
719 'author': ['https://thuza.ratfeathers.com/'],
720 'authorization_endpoint': ['https://ia.squeep.com/auth'],
721 'canonical': ['https://thuza.ratfeathers.com/'],
722 'me': ['https://thuza.ratfeathers.com/'],
723 'token_endpoint': ['https://ia.squeep.com/token'],
724 },
725 };
726 result = await communication.fetchClientIdentifier(urlObj);
727 assert.deepStrictEqual(result, expected);
728 });
729 it('covers failed fetch', async function () {
730 communication.axios.rejects();
731 expected = undefined;
732 result = await communication.fetchClientIdentifier(urlObj);
733 assert.deepStrictEqual(result, expected);
734 });
735 it('covers no h-app data', async function () {
736 response.data = testData.noneMF2Html;
737 communication.axios.resolves(response);
738 expected = {
739 items: [],
740 rels: {},
741 };
742 result = await communication.fetchClientIdentifier(urlObj);
743 assert.deepStrictEqual(result, expected);
744 });
745 it('covers missing fields', async function () {
746 sinon.stub(communication, 'fetchMicroformat').resolves({});
747 expected = {
748 rels: {},
749 items: [],
750 };
751 result = await communication.fetchClientIdentifier(urlObj);
752 assert.deepStrictEqual(result, expected);
753 });
754 it('covers other missing fields', async function () {
755 sinon.stub(communication, 'fetchMicroformat').resolves({
756 items: [
757 {},
758 {
759 type: ['h-app'],
760 properties: {
761 url: ['https://example.com'],
762 },
763 },
764 ],
765 });
766 expected = {
767 rels: {},
768 items: [],
769 };
770 result = await communication.fetchClientIdentifier(urlObj);
771 assert.deepStrictEqual(result, expected);
772 });
773 it('covers loopback', async function () {
774 sinon.spy(communication, 'fetchMicroformat');
775 urlObj.isLoopback = true;
776 expected = {
777 rels: {},
778 items: [],
779 };
780 result = await communication.fetchClientIdentifier(urlObj);
781 assert.deepStrictEqual(result, expected);
782 assert(communication.fetchMicroformat.notCalled);
783 });
784 }); // fetchClientIdentifier
785
786 describe('fetchProfile', function () {
787 let expected, response, result, urlObj;
788 beforeEach(function () {
789 expected = undefined;
790 result = undefined;
791 urlObj = new URL('https://thuza.ratfeathers.com/');
792 response = {
793 headers: {},
794 data: testData.hCardHtml,
795 };
796 sinon.stub(communication, 'fetchJSON');
797 });
798 describe('legacy without indieauth-metadata', function () {
799 it('covers', async function () {
800 communication.axios.resolves(response);
801 expected = {
802 name: 'Thuza',
803 photo: 'https://thuza.ratfeathers.com/image.png',
804 url: 'https://thuza.ratfeathers.com/',
805 email: undefined,
806 authorizationEndpoint: 'https://ia.squeep.com/auth',
807 tokenEndpoint: 'https://ia.squeep.com/token',
808 metadata: {
809 authorizationEndpoint: 'https://ia.squeep.com/auth',
810 tokenEndpoint: 'https://ia.squeep.com/token',
811 },
812 };
813 result = await communication.fetchProfile(urlObj);
814 assert.deepStrictEqual(result, expected);
815 });
816 it('covers multiple hCards', async function () {
817 response.data = testData.multiMF2Html;
818 communication.axios.resolves(response);
819 expected = {
820 email: undefined,
821 name: 'Thuza',
822 photo: 'https://thuza.ratfeathers.com/image.png',
823 url: 'https://thuza.ratfeathers.com/',
824 authorizationEndpoint: 'https://ia.squeep.com/auth',
825 tokenEndpoint: 'https://ia.squeep.com/token',
826 metadata: {
827 authorizationEndpoint: 'https://ia.squeep.com/auth',
828 tokenEndpoint: 'https://ia.squeep.com/token',
829 },
830 };
831 result = await communication.fetchProfile(urlObj);
832 assert.deepStrictEqual(result, expected);
833 });
834 it('covers failed fetch', async function () {
835 communication.axios.rejects();
836 expected = {
837 email: undefined,
838 name: undefined,
839 photo: undefined,
840 url: undefined,
841 metadata: {},
842 };
843 result = await communication.fetchProfile(urlObj);
844 assert.deepStrictEqual(result, expected);
845 });
846 });
847 it('covers', async function () {
848 response.data = testData.hCardMetadataHtml;
849 communication.axios.resolves(response);
850 communication.fetchJSON.resolves({
851 'issuer': 'https://ia.squeep.com/',
852 'authorization_endpoint': 'https://ia.squeep.com/auth',
853 'token_endpoint': 'https://ia.squeep.com/token',
854 'introspection_endpoint': 'https://ia.squeep.com/introspect',
855 'introspection_endpoint_auth_methods_supported': [ '' ],
856 'revocation_endpoint': 'https://ia.squeep.com/revoke',
857 'revocation_endpoint_auth_methods_supported': [ 'none' ],
858 'scopes_supported': [ 'profile', 'email' ],
859 'service_documentation': 'https://indieauth.spec.indieweb.org/',
860 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
861 'authorization_response_iss_parameter_supported': true,
862 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
863 });
864 expected = {
865 name: 'Thuza',
866 photo: 'https://thuza.ratfeathers.com/image.png',
867 url: 'https://thuza.ratfeathers.com/',
868 email: undefined,
869 metadata: {
870 authorizationEndpoint: 'https://ia.squeep.com/auth',
871 tokenEndpoint: 'https://ia.squeep.com/token',
872 issuer: 'https://ia.squeep.com/',
873 introspectionEndpoint: 'https://ia.squeep.com/introspect',
874 introspectionEndpointAuthMethodsSupported: [ '' ],
875 revocationEndpoint: 'https://ia.squeep.com/revoke',
876 revocationEndpointAuthMethodsSupported: [ 'none' ],
877 scopesSupported: [ 'profile', 'email' ],
878 serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
879 codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
880 authorizationResponseIssParameterSupported: true,
881 userinfoEndpoint: 'https://ia.squeep.com/userinfo',
882 },
883 authorizationEndpoint: 'https://ia.squeep.com/auth',
884 tokenEndpoint: 'https://ia.squeep.com/token',
885 indieauthMetadata: 'https://ia.squeep.com/meta',
886 };
887
888 result = await communication.fetchProfile(urlObj);
889
890 assert.deepStrictEqual(result, expected);
891 });
892 it('covers metadata missing fields', async function () {
893 response.data = testData.hCardMetadataHtml;
894 communication.axios.resolves(response);
895 communication.fetchJSON.resolves({
896 'issuer': 'https://ia.squeep.com/',
897 });
898 expected = {
899 name: 'Thuza',
900 photo: 'https://thuza.ratfeathers.com/image.png',
901 url: 'https://thuza.ratfeathers.com/',
902 email: undefined,
903 metadata: {
904 issuer: 'https://ia.squeep.com/',
905 },
906 indieauthMetadata: 'https://ia.squeep.com/meta',
907 };
908
909 result = await communication.fetchProfile(urlObj);
910
911 assert.deepStrictEqual(result, expected);
912 });
913 it('covers metadata response failure', async function () {
914 const jsonError = new Error('oh no');
915 response.data = testData.hCardMetadataHtml;
916 communication.axios
917 .onCall(0).resolves(response)
918 .onCall(1).rejects(jsonError);
919 communication.fetchJSON.restore();
920 expected = {
921 name: 'Thuza',
922 photo: 'https://thuza.ratfeathers.com/image.png',
923 url: 'https://thuza.ratfeathers.com/',
924 email: undefined,
925 metadata: {},
926 indieauthMetadata: 'https://ia.squeep.com/meta',
927 };
928
929 result = await communication.fetchProfile(urlObj);
930
931 assert.deepStrictEqual(result, expected);
932 });
933 }); // fetchProfile
934
935 describe('redeemProfileCode', function () {
936 let expected, urlObj, code, codeVerifier, clientId, redirectURI;
937 this.beforeEach(function () {
938 urlObj = new URL('https://example.com/auth');
939 code = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
940 codeVerifier = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
941 clientId = 'https://example.com/';
942 redirectURI = 'https://example.com/_ia';
943 });
944 it('covers', async function () {
945 communication.axios.resolves({
946 data: '{"me":"https://profile.example.com/"}',
947 });
948 expected = {
949 me: 'https://profile.example.com/',
950 };
951
952 const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
953
954 assert.deepStrictEqual(result, expected);
955 });
956 it('covers failure', async function () {
957 communication.axios.resolves('Not a JSON payload.');
958
959 const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
960
961 assert.strictEqual(result, undefined);
962 });
963 });
964 }); // Communication