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