--- /dev/null
+/* eslint-env mocha */
+/* eslint-disable capitalized-comments, sonarjs/no-duplicate-string */
+
+'use strict';
+
+const assert = require('assert');
+const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
+
+const Communication = require('../../lib/communication');
+
+const stubLogger = require('../stub-logger');
+const testData = require('../test-data/communication');
+
+const noExpectedException = 'did not get expected exception';
+
+describe('Communication', function () {
+ let communication, options;
+
+ beforeEach(function () {
+ options = {};
+ communication = new Communication(stubLogger, options);
+ stubLogger._reset();
+ sinon.stub(communication, 'axios');
+ });
+ afterEach(function () {
+ sinon.restore();
+ });
+
+ it('instantiates', function () {
+ assert(communication);
+ });
+
+ it('covers no config', function () {
+ communication = new Communication(stubLogger);
+ });
+
+ describe('Axios timing coverage', function () {
+ const request = {};
+ const response = {
+ config: request,
+ };
+ it('tags request', function () {
+ communication.axios.interceptors.request.handlers[0].fulfilled(request);
+ assert(request.startTimestampMs);
+ });
+ it('tags response', function () {
+ communication.axios.interceptors.response.handlers[0].fulfilled(response);
+ assert(response.elapsedTimeMs);
+ });
+ }); // Axios timing coverage
+
+ describe('_challengeFromVerifier', function () {
+ it('covers', function () {
+ const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
+ const expected = 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
+ const result = Communication._challengeFromVerifier(verifier);
+ assert.strictEqual(result, expected);
+ });
+ }); // _challengeFromVerifier
+
+ describe('generatePKCE', function () {
+ it('covers', async function () {
+ const result = await Communication.generatePKCE();
+ assert(result.codeVerifier);
+ assert(result.codeChallenge);
+ assert(result.codeChallengeMethod);
+ assert.strictEqual(result.codeChallengeMethod, 'S256');
+ });
+ it('covers error', async function () {
+ try {
+ await Communication.generatePKCE(1);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e instanceof RangeError);
+ }
+ });
+ }); // generatePKCE
+
+ describe('verifyChallenge', function () {
+ it('covers success', function () {
+ const method = 'S256';
+ const challenge = 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
+ const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
+ const result = Communication.verifyChallenge(challenge, verifier, method);
+ assert.strictEqual(result, true);
+ });
+ it('also covers success', function () {
+ const method = 'SHA256';
+ const challenge = 'O5W5A-1CAnrNGp2yHZtEql6rfHere4wJmzsyow7LLiY';
+ const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
+ const result = Communication.verifyChallenge(challenge, verifier, method);
+ assert.strictEqual(result, true);
+ });
+ it('covers failure', function () {
+ const method = 'S256';
+ const challenge = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
+ const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
+ const result = Communication.verifyChallenge(challenge, verifier, method);
+ assert.strictEqual(result, false);
+ });
+ it('covers unhandled method', function () {
+ const method = 'MD5';
+ const challenge = 'xkfP7DUYDsnu07Kg6ogc8A';
+ const verifier = 'VGhpcyBpcyBhIHNlY3JldC4u';
+ try {
+ Communication.verifyChallenge(challenge, verifier, method);
+ assert.fail(noExpectedException);
+ } catch (e) {
+ assert(e.message.includes('unsupported'));
+ }
+ });
+ }); // verifyChallenge
+
+ describe('_userAgentString', function () {
+ it('has default behavior', function () {
+ const result = Communication._userAgentString();
+ assert(result);
+ assert(result.length > 30);
+ });
+ it('is settable', function () {
+ const result = Communication._userAgentString({
+ product: 'myClient',
+ version: '9.9.9',
+ implementation: 'custom',
+ });
+ assert(result);
+ assert.strictEqual(result, 'myClient/9.9.9 (custom)');
+ });
+ it('covers branches', function () {
+ const result = Communication._userAgentString({
+ product: 'myClient',
+ version: '9.9.9',
+ implementation: '',
+ });
+ assert(result);
+ assert.strictEqual(result, 'myClient/9.9.9');
+ });
+ }); // userAgentString
+
+ describe('Axios Configurations', function () {
+ let requestUrl, expectedUrl;
+ beforeEach(function () {
+ requestUrl = 'https://example.com/client_id';
+ expectedUrl = 'https://example.com/client_id';
+ });
+ it('_axiosConfig', function () {
+ const method = 'GET';
+ const contentType = 'text/plain';
+ const body = undefined;
+ const params = {
+ 'extra_parameter': 'foobar',
+ };
+ const urlObj = new URL(requestUrl);
+ const expectedUrlObj = new URL(`${requestUrl}?extra_parameter=foobar`);
+ const expected = {
+ method,
+ url: 'https://example.com/client_id',
+ headers: {
+ 'Content-Type': 'text/plain',
+ },
+ params: expectedUrlObj.searchParams,
+ responseType: 'text',
+ };
+ const result = Communication._axiosConfig(method, urlObj, body, params, {
+ 'Content-Type': contentType,
+ });
+ delete result.transformResponse;
+ assert.deepStrictEqual(result, expected);
+ });
+ it('_axiosConfig covers defaults', function () {
+ const method = 'OPTIONS';
+ const urlObj = new URL(requestUrl);
+ const expectedUrlObj = new URL(requestUrl);
+ const expected = {
+ method,
+ url: expectedUrl,
+ headers: {},
+ params: expectedUrlObj.searchParams,
+ responseType: 'text',
+ };
+ const result = Communication._axiosConfig(method, urlObj);
+ delete result.transformResponse;
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers data', function () {
+ const method = 'POST';
+ const body = Buffer.from('some data');
+ const params = {};
+ const urlObj = new URL(requestUrl);
+ const expected = {
+ method,
+ url: 'https://example.com/client_id',
+ data: body,
+ headers: {},
+ params: urlObj.searchParams,
+ responseType: 'text',
+ };
+ const result = Communication._axiosConfig(method, urlObj, body, params, {});
+ delete result.transformResponse;
+ assert.deepStrictEqual(result, expected);
+
+ });
+ it('covers null response transform', function () {
+ const urlObj = new URL(requestUrl);
+ const result = Communication._axiosConfig('GET', urlObj, undefined, {}, {});
+ result.transformResponse[0]();
+ });
+ }); // Axios Configurations
+
+ describe('_baseUrlString', function () {
+ it('covers no path', function () {
+ const urlObj = new URL('https://example.com');
+ const expected = 'https://example.com/';
+ const result = Communication._baseUrlString(urlObj);
+ assert.strictEqual(result, expected);
+ });
+ it('covers paths', function () {
+ const urlObj = new URL('https://example.com/path/blah');
+ const expected = 'https://example.com/path/';
+ const result = Communication._baseUrlString(urlObj);
+ assert.strictEqual(result, expected);
+ });
+ }); // _baseUrlString
+
+ describe('_parseContentType', function () {
+ let contentTypeHeader, expected, result;
+ it('covers undefined', function () {
+ contentTypeHeader = undefined;
+ expected = {
+ mediaType: 'application/octet-stream',
+ params: {},
+ };
+ result = Communication._parseContentType(contentTypeHeader);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers empty', function () {
+ contentTypeHeader = '';
+ expected = {
+ mediaType: 'application/octet-stream',
+ params: {},
+ };
+ result = Communication._parseContentType(contentTypeHeader);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers extra parameters', function () {
+ contentTypeHeader = 'text/plain; CharSet="UTF-8"; WeirdParam';
+ expected = {
+ mediaType: 'text/plain',
+ params: {
+ 'charset': 'UTF-8',
+ 'weirdparam': undefined,
+ },
+ };
+ result = Communication._parseContentType(contentTypeHeader);
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // parseContentType
+
+ describe('_mergeLinkHeader', function () {
+ let microformat, response, expected;
+ beforeEach(function () {
+ microformat = {};
+ response = {
+ headers: {
+ link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
+ },
+ data: {},
+ }
+ });
+ it('covers', function () {
+ expected = {
+ items: [],
+ rels: {
+ 'hub': ['https://hub.example.com/'],
+ 'self': ['https://example.com/'],
+ },
+ 'rel-urls': {
+ 'https://example.com/': {
+ rels: ['self'],
+ text: '',
+ },
+ 'https://hub.example.com/': {
+ rels: ['hub'],
+ text: '',
+ },
+ },
+ };
+ communication._mergeLinkHeader(microformat, response);
+ assert.deepStrictEqual(microformat, expected);
+ });
+ it('covers existing', function () {
+ microformat = {
+ items: [],
+ rels: {
+ 'preload': ['https://example.com/style'],
+ 'hub': ['https://hub.example.com/'],
+ },
+ 'rel-urls': {
+ 'https://hub.example.com/': {
+ rels: ['hub'],
+ text: '',
+ },
+ 'https://example.com/style': {
+ rels: ['preload'],
+ text: '',
+ },
+ },
+ };
+ expected = {
+ items: [],
+ rels: {
+ 'preload': ['https://example.com/style'],
+ 'hub': ['https://hub.example.com/', 'https://hub.example.com/'],
+ 'self': ['https://example.com/'],
+ },
+ 'rel-urls': {
+ 'https://example.com/': {
+ rels: ['self'],
+ text: '',
+ },
+ 'https://hub.example.com/': {
+ rels: ['hub', 'hub'],
+ text: '',
+ },
+ 'https://example.com/style': {
+ rels: ['preload'],
+ text: '',
+ },
+ },
+ };
+ communication._mergeLinkHeader(microformat, response);
+ assert.deepStrictEqual(microformat, expected);
+ });
+ it('ignores bad header', function () {
+ response.headers.link = 'not really a link header';
+ expected = {
+ items: [],
+ rels: {},
+ 'rel-urls': {},
+ };
+ communication._mergeLinkHeader(microformat, response);
+ assert.deepStrictEqual(microformat, expected);
+ });
+ }); // _mergeLinkHeader
+
+ describe('fetchMicroformat', function () {
+ let expected, response, result, urlObj;
+ beforeEach(function () {
+ expected = undefined;
+ result = undefined;
+ urlObj = new URL('https://thuza.ratfeathers.com/');
+ response = {
+ headers: Object.assign({}, testData.linkHeaders),
+ data: testData.hCardHtml,
+ };
+ });
+ it('covers', async function () {
+ response.data = testData.hCardHtml;
+ communication.axios.resolves(response);
+ expected = {
+ rels: {
+ 'authorization_endpoint': ['https://ia.squeep.com/auth'],
+ 'token_endpoint': ['https://ia.squeep.com/token'],
+ 'canonical': ['https://thuza.ratfeathers.com/'],
+ 'author': ['https://thuza.ratfeathers.com/'],
+ 'me': ['https://thuza.ratfeathers.com/'],
+ 'self': ['https://thuza.ratfeathers.com/'],
+ 'hub': ['https://hub.squeep.com/'],
+ 'preload': ['https://thuza.ratfeathers.com/image.png'],
+ },
+ 'rel-urls': {
+ 'https://hub.squeep.com/': {
+ rels: ['hub'],
+ text: '',
+ },
+ 'https://ia.squeep.com/auth': {
+ rels: ['authorization_endpoint'],
+ text: '',
+ },
+ 'https://ia.squeep.com/token': {
+ rels: ['token_endpoint'],
+ text: '',
+ },
+ 'https://thuza.ratfeathers.com/': {
+ rels: ['self', 'canonical', 'author', 'me'],
+ text: 'Thuza',
+ },
+ 'https://thuza.ratfeathers.com/image.png': {
+ rels: ['preload'],
+ text: '',
+ },
+ },
+ items: [{
+ properties: {
+ name: ['Thuza'],
+ photo: ['https://thuza.ratfeathers.com/image.png'],
+ url: ['https://thuza.ratfeathers.com/'],
+ },
+ type: ['h-card'],
+ }],
+ };
+
+ result = await communication.fetchMicroformat(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers axios error', async function () {
+ communication.axios.rejects(new Error('blah'));
+ expected = undefined;
+
+ result = await communication.fetchMicroformat(urlObj);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers non-parsable content', async function () {
+ response.data = 'some bare text';
+ response.headers = {};
+ communication.axios.resolves(response);
+ expected = {
+ items: [],
+ rels: {},
+ 'rel-urls': {},
+ };
+
+ result = await communication.fetchMicroformat(urlObj);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers non-utf8 content', async function () {
+ response.headers['content-type'] = 'text/html; charset=ASCII';
+ communication.axios.resolves(response);
+ expected = {
+ rels: {
+ 'authorization_endpoint': ['https://ia.squeep.com/auth'],
+ 'token_endpoint': ['https://ia.squeep.com/token'],
+ 'canonical': ['https://thuza.ratfeathers.com/'],
+ 'author': ['https://thuza.ratfeathers.com/'],
+ 'me': ['https://thuza.ratfeathers.com/'],
+ 'self': ['https://thuza.ratfeathers.com/'],
+ 'hub': ['https://hub.squeep.com/'],
+ 'preload': ['https://thuza.ratfeathers.com/image.png'],
+ },
+ 'rel-urls': {
+ 'https://hub.squeep.com/': {
+ rels: ['hub'],
+ text: '',
+ },
+ 'https://ia.squeep.com/auth': {
+ rels: ['authorization_endpoint'],
+ text: '',
+ },
+ 'https://ia.squeep.com/token': {
+ rels: ['token_endpoint'],
+ text: '',
+ },
+ 'https://thuza.ratfeathers.com/': {
+ rels: ['self', 'canonical', 'author', 'me'],
+ text: 'Thuza',
+ },
+ 'https://thuza.ratfeathers.com/image.png': {
+ rels: ['preload'],
+ text: '',
+ },
+ },
+ items: [{
+ properties: {
+ name: ['Thuza'],
+ photo: ['https://thuza.ratfeathers.com/image.png'],
+ url: ['https://thuza.ratfeathers.com/'],
+ },
+ type: ['h-card'],
+ }],
+ };
+
+ result = await communication.fetchMicroformat(urlObj);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // fetchMicroformat
+
+ describe('fetchClientIdentifier', function () {
+ let expected, response, result, urlObj;
+ beforeEach(function () {
+ expected = undefined;
+ result = undefined;
+ urlObj = new URL('https://thuza.ratfeathers.com/');
+ response = {
+ headers: {},
+ data: testData.multiMF2Html,
+ };
+ });
+ it('covers', async function () {
+ communication.axios.resolves(response);
+ expected = {
+ items: [{
+ properties: {
+ name: ['Also Some Client'],
+ url: ['https://thuza.ratfeathers.com/'],
+ },
+ type: ['h-app'],
+ }],
+ rels: {
+ 'author': ['https://thuza.ratfeathers.com/'],
+ 'authorization_endpoint': ['https://ia.squeep.com/auth'],
+ 'canonical': ['https://thuza.ratfeathers.com/'],
+ 'me': ['https://thuza.ratfeathers.com/'],
+ 'token_endpoint': ['https://ia.squeep.com/token'],
+ },
+ };
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers failed fetch', async function () {
+ communication.axios.rejects();
+ expected = undefined;
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers no h-app data', async function () {
+ response.data = testData.noneMF2Html;
+ communication.axios.resolves(response);
+ expected = {
+ items: [],
+ rels: {},
+ };
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers missing fields', async function () {
+ sinon.stub(communication, 'fetchMicroformat').resolves({});
+ expected = {
+ rels: {},
+ items: [],
+ };
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers other missing fields', async function () {
+ sinon.stub(communication, 'fetchMicroformat').resolves({
+ items: [
+ {},
+ {
+ type: ['h-app'],
+ properties: {
+ url: ['https://example.com'],
+ },
+ },
+ ],
+ });
+ expected = {
+ rels: {},
+ items: [],
+ };
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // fetchClientIdentifier
+
+ describe('fetchProfile', function () {
+ let expected, response, result, urlObj;
+ beforeEach(function () {
+ expected = undefined;
+ result = undefined;
+ urlObj = new URL('https://thuza.ratfeathers.com/');
+ response = {
+ headers: {},
+ data: testData.hCardHtml,
+ };
+ });
+ it('covers', async function () {
+ communication.axios.resolves(response);
+ expected = {
+ name: 'Thuza',
+ photo: 'https://thuza.ratfeathers.com/image.png',
+ url: 'https://thuza.ratfeathers.com/',
+ email: undefined,
+ authorizationEndpoint: 'https://ia.squeep.com/auth',
+ tokenEndpoint: 'https://ia.squeep.com/token',
+ };
+ result = await communication.fetchProfile(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers multiple hCards', async function () {
+ response.data = testData.multiMF2Html;
+ communication.axios.resolves(response);
+ expected = {
+ email: undefined,
+ name: 'Thuza',
+ photo: 'https://thuza.ratfeathers.com/image.png',
+ url: 'https://thuza.ratfeathers.com/',
+ authorizationEndpoint: 'https://ia.squeep.com/auth',
+ tokenEndpoint: 'https://ia.squeep.com/token',
+ };
+ result = await communication.fetchProfile(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers failed fetch', async function () {
+ communication.axios.rejects();
+ expected = {
+ email: undefined,
+ name: undefined,
+ photo: undefined,
+ url: undefined,
+ };
+ result = await communication.fetchProfile(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // fetchProfile
+
+ describe('redeemProfileCode', function () {
+ let expected, urlObj, code, codeVerifier, clientId, redirectURI;
+ this.beforeEach(function () {
+ urlObj = new URL('https://example.com/auth');
+ code = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
+ codeVerifier = Buffer.allocUnsafe(42).toString('base64').replace('/', '_').replace('+', '-');
+ clientId = 'https://example.com/';
+ redirectURI = 'https://example.com/_ia';
+ });
+ it('covers', async function () {
+ communication.axios.resolves({
+ data: '{"me":"https://profile.example.com/"}'
+ });
+ expected = {
+ me: 'https://profile.example.com/',
+ };
+
+ const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers failure', async function () {
+ communication.axios.resolves('Not a JSON payload.');
+
+ const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
+
+ assert.strictEqual(result, undefined);
+ });
+ });
+}); // Communication
\ No newline at end of file