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