const sinon = require('sinon'); // eslint-disable-line node/no-unpublished-require
const Communication = require('../../lib/communication');
+const { ValidationError } = require('../../lib/errors');
+const dns = require('dns');
const stubLogger = require('../stub-logger');
const testData = require('../test-data/communication');
-const noExpectedException = 'did not get expected exception';
-
describe('Communication', function () {
let communication, options;
assert.strictEqual(result.codeChallengeMethod, 'S256');
});
it('covers error', async function () {
- try {
- await Communication.generatePKCE(1);
- assert.fail(noExpectedException);
- } catch (e) {
- assert(e instanceof RangeError);
- }
+ await assert.rejects(() => Communication.generatePKCE(1));
});
}); // generatePKCE
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'));
- }
+ assert.throws(() => Communication.verifyChallenge(challenge, verifier, method));
});
}); // verifyChallenge
},
params: expectedUrlObj.searchParams,
responseType: 'text',
+ validateStatus: Communication._validateStatus,
};
const result = Communication._axiosConfig(method, urlObj, body, params, {
'Content-Type': contentType,
headers: {},
params: expectedUrlObj.searchParams,
responseType: 'text',
+ validateStatus: Communication._validateStatus,
};
const result = Communication._axiosConfig(method, urlObj);
delete result.transformResponse;
headers: {},
params: urlObj.searchParams,
responseType: 'text',
+ validateStatus: Communication._validateStatus,
};
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]();
});
+
+ describe('_validateStatus', function () {
+ it('allows normal valid', function () {
+ const result = Communication._validateStatus(200);
+ assert.strictEqual(result, true);
+ });
+ it('allows unauthorized', function () {
+ const result = Communication._validateStatus(401);
+ assert.strictEqual(result, true);
+ });
+ it('rejects invalid', function () {
+ const result = Communication._validateStatus(400);
+ assert.strictEqual(result, false);
+ });
+ }); // _validateStatus
}); // Axios Configurations
describe('_baseUrlString', function () {
});
}); // fetchMicroformat
+ describe('fetchJSON', 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 () {
+ communication.axios.resolves(response);
+ expected = { foo: 'bar', baz: 123 };
+ response.data = JSON.stringify(expected);
+
+ result = await communication.fetchJSON(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers axios error', async function () {
+ communication.axios.rejects(new Error('blah'));
+ expected = undefined;
+
+ result = await communication.fetchJSON(urlObj);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers non-parsable content', async function () {
+ response.data = 'some bare text';
+ response.headers = {};
+ communication.axios.resolves(response);
+ expected = undefined;
+
+ result = await communication.fetchJSON(urlObj);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // fetchJSON
+
+ describe('validateProfile', function () {
+ let url, validationOptions;
+ beforeEach(function () {
+ url = 'https://example.com/';
+ validationOptions = {};
+ sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]);
+ });
+ it('rejects invalid url', async function () {
+ url = 'bad url';
+ await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError);
+ });
+ it('covers success', async function () {
+ const result = await communication.validateProfile(url, validationOptions);
+ assert.strictEqual(result.isLoopback, false);
+ });
+ it('rejects invalid', async function () {
+ url = 'ftp://example.com/';
+ await assert.rejects(() => communication.validateProfile(url, validationOptions), ValidationError);
+ });
+
+ }); // validateProfile
+
+ describe('validateClientIdentifier', function () {
+ let url, validationOptions;
+ beforeEach(function () {
+ url = 'https://example.com/';
+ validationOptions = {};
+ sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
+ });
+ it('rejects invalid url', async function () {
+ await assert.rejects(() => communication.validateClientIdentifier('bad url'), ValidationError);
+ });
+ it('rejects invalid scheme', async function () {
+ url = 'ftp://example.com/';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects fragment', async function () {
+ url = 'https://example.com/#foo';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects username', async function () {
+ url = 'https://user@example.com/';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects password', async function () {
+ url = 'https://:foo@example.com/';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects relative path', async function () {
+ url = 'https://example.com/client/../sneaky';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects ipv4', async function () {
+ url = 'https://10.11.12.13/';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects ipv6', async function () {
+ url = 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('accepts ipv4 loopback', async function () {
+ url = 'https://127.0.0.1/';
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, true);
+ });
+ it('accepts ipv6 loopback', async function () {
+ url = 'https://[::1]/';
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, true);
+ });
+ it('accepts resolved ipv4 loopback', async function () {
+ dns.lookupAsync.resolves([{ family: 4, address: '127.0.0.1' }]);
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, true);
+ });
+ it('accepts resolved ipv6 loopback', async function () {
+ dns.lookupAsync.resolves([{ family: 6, address: '::1' }]);
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, true);
+ });
+ it('covers success', async function () {
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, false);
+ });
+ it('rejects resolution failure', async function () {
+ dns.lookupAsync.rejects(new Error('oh no'));
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('rejects mismatched resolutions', async function () {
+ dns.lookupAsync.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
+ await assert.rejects(() => communication.validateClientIdentifier(url, validationOptions), ValidationError);
+ });
+ it('ignores unknown dns family', async function () {
+ dns.lookupAsync.resolves([{ family: 5, address: '10.9.8.7' }]);
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, false);
+ });
+ it('covers rooted hostname', async function() {
+ url = 'https://example.com./';
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, false);
+ });
+ it('covers unresolved', async function () {
+ dns.lookupAsync.resolves();
+ const result = await communication.validateClientIdentifier(url, validationOptions);
+ assert.strictEqual(result.isLoopback, false);
+ });
+ }); // validateClientIdentifier
+
describe('fetchClientIdentifier', function () {
let expected, response, result, urlObj;
beforeEach(function () {
result = await communication.fetchClientIdentifier(urlObj);
assert.deepStrictEqual(result, expected);
});
+ it('covers loopback', async function () {
+ sinon.spy(communication, 'fetchMicroformat');
+ urlObj.isLoopback = true;
+ expected = {
+ rels: {},
+ items: [],
+ };
+ result = await communication.fetchClientIdentifier(urlObj);
+ assert.deepStrictEqual(result, expected);
+ assert(communication.fetchMicroformat.notCalled);
+ });
}); // fetchClientIdentifier
describe('fetchProfile', function () {
headers: {},
data: testData.hCardHtml,
};
+ sinon.stub(communication, 'fetchJSON');
+ });
+ describe('legacy without indieauth-metadata', function () {
+ 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',
+ metadata: {
+ 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',
+ metadata: {
+ 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,
+ metadata: {},
+ };
+ result = await communication.fetchProfile(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
});
it('covers', async function () {
+ response.data = testData.hCardMetadataHtml;
communication.axios.resolves(response);
+ communication.fetchJSON.resolves({
+ 'issuer': 'https://ia.squeep.com/',
+ 'authorization_endpoint': 'https://ia.squeep.com/auth',
+ 'token_endpoint': 'https://ia.squeep.com/token',
+ 'introspection_endpoint': 'https://ia.squeep.com/introspect',
+ 'introspection_endpoint_auth_methods_supported': [ '' ],
+ 'revocation_endpoint': 'https://ia.squeep.com/revoke',
+ 'revocation_endpoint_auth_methods_supported': [ 'none' ],
+ 'scopes_supported': [ 'profile', 'email' ],
+ 'service_documentation': 'https://indieauth.spec.indieweb.org/',
+ 'code_challenge_methods_supported': [ 'S256', 'SHA256' ],
+ 'authorization_response_iss_parameter_supported': true,
+ 'userinfo_endpoint': 'https://ia.squeep.com/userinfo',
+ });
expected = {
name: 'Thuza',
photo: 'https://thuza.ratfeathers.com/image.png',
url: 'https://thuza.ratfeathers.com/',
email: undefined,
+ metadata: {
+ authorizationEndpoint: 'https://ia.squeep.com/auth',
+ tokenEndpoint: 'https://ia.squeep.com/token',
+ issuer: 'https://ia.squeep.com/',
+ introspectionEndpoint: 'https://ia.squeep.com/introspect',
+ introspectionEndpointAuthMethodsSupported: [ '' ],
+ revocationEndpoint: 'https://ia.squeep.com/revoke',
+ revocationEndpointAuthMethodsSupported: [ 'none' ],
+ scopesSupported: [ 'profile', 'email' ],
+ serviceDocumentation: 'https://indieauth.spec.indieweb.org/',
+ codeChallengeMethodsSupported: [ 'S256', 'SHA256' ],
+ authorizationResponseIssParameterSupported: true,
+ userinfoEndpoint: 'https://ia.squeep.com/userinfo',
+ },
authorizationEndpoint: 'https://ia.squeep.com/auth',
tokenEndpoint: 'https://ia.squeep.com/token',
+ indieauthMetadata: 'https://ia.squeep.com/meta',
};
+
result = await communication.fetchProfile(urlObj);
+
assert.deepStrictEqual(result, expected);
});
- it('covers multiple hCards', async function () {
- response.data = testData.multiMF2Html;
+ it('covers metadata missing fields', async function () {
+ response.data = testData.hCardMetadataHtml;
communication.axios.resolves(response);
+ communication.fetchJSON.resolves({
+ 'issuer': 'https://ia.squeep.com/',
+ });
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',
+ email: undefined,
+ metadata: {
+ issuer: 'https://ia.squeep.com/',
+ },
+ indieauthMetadata: 'https://ia.squeep.com/meta',
};
+
result = await communication.fetchProfile(urlObj);
+
assert.deepStrictEqual(result, expected);
});
- it('covers failed fetch', async function () {
- communication.axios.rejects();
+ it('covers metadata response failure', async function () {
+ const jsonError = new Error('oh no');
+ response.data = testData.hCardMetadataHtml;
+ communication.axios
+ .onCall(0).resolves(response)
+ .onCall(1).rejects(jsonError);
+ communication.fetchJSON.restore();
expected = {
+ name: 'Thuza',
+ photo: 'https://thuza.ratfeathers.com/image.png',
+ url: 'https://thuza.ratfeathers.com/',
email: undefined,
- name: undefined,
- photo: undefined,
- url: undefined,
+ metadata: {},
+ indieauthMetadata: 'https://ia.squeep.com/meta',
};
+
result = await communication.fetchProfile(urlObj);
+
assert.deepStrictEqual(result, expected);
});
}); // fetchProfile
describe('redeemProfileCode', function () {
let expected, urlObj, code, codeVerifier, clientId, redirectURI;
- this.beforeEach(function () {
+ 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('+', '-');
});
it('covers', async function () {
communication.axios.resolves({
- data: '{"me":"https://profile.example.com/"}'
+ data: '{"me":"https://profile.example.com/"}',
});
expected = {
me: 'https://profile.example.com/',
assert.strictEqual(result, undefined);
});
- });
+ }); // redeemProfileCode
+
+ describe('introspectToken', function () {
+ let introspectionUrlObj, authenticationHeader, token;
+ beforeEach(function () {
+ introspectionUrlObj = new URL('https://ia.example.com/introspect');
+ authenticationHeader = 'Bearer XXX';
+ token = 'xxx';
+ });
+ it('covers success active', async function () {
+ const nowEpoch = Math.ceil(Date.now() / 1000);
+ communication.axios.resolves({
+ data: JSON.stringify({
+ active: true,
+ me: 'https://profile.example.com/',
+ 'client_id': 'https://app.example.com/',
+ scope: 'create profile email',
+ exp: nowEpoch + 86400,
+ iat: nowEpoch,
+ }),
+ });
+ const result = await communication.introspectToken(introspectionUrlObj, authenticationHeader, token);
+ assert.strictEqual(result.active, true);
+ });
+ it('covers success inactive', async function () {
+ communication.axios.resolves({
+ data: JSON.stringify({
+ active: false,
+ }),
+ });
+ const result = await communication.introspectToken(introspectionUrlObj, authenticationHeader, token);
+ assert.strictEqual(result.active, false);
+ });
+ it('covers failure', async function () {
+ communication.axios.resolves('what kind of response is this?');
+ await assert.rejects(() => communication.introspectToken(introspectionUrlObj, authenticationHeader, token));
+ });
+ }); // introspectToken
+
+ describe('deliverTicket', function () {
+ let ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket;
+ beforeEach(function () {
+ ticketEndpointUrlObj = new URL('https://ticket.example.com/');
+ resourceUrlObj = new URL('https://resource.example.com/');
+ subjectUrlObj = new URL('https://subject.example.com/');
+ ticket = 'XXXThisIsATicketXXX';
+ });
+ it('covers success', async function () {
+ const expected = { data: 'blah', statusCode: 200 };
+ communication.axios.resolves(expected);
+ const result = await communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers failure', async function () {
+ const expectedException = new Error('oh no');
+ communication.axios.rejects(expectedException);
+ await assert.rejects(() => communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket), expectedException);
+ });
+ }); // deliverTicket
+
}); // Communication
\ No newline at end of file