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