describe('Communication', function () {
let communication, options;
- beforeEach(function () {
+ beforeEach(async function () {
options = {};
communication = new Communication(stubLogger, options);
+ await communication._init();
stubLogger._reset();
- sinon.stub(communication, 'axios');
+ sinon.stub(communication, 'got');
});
afterEach(function () {
sinon.restore();
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);
+ describe('_init', function () {
+ it('covers first use', async function () {
+ await communication._init({});
+ assert(communication.got.called);
});
- it('tags response', function () {
- communication.axios.interceptors.response.handlers[0].fulfilled(response);
- assert(response.elapsedTimeMs);
+ }); // _init
+
+ describe('_onRetry', function () {
+ it('covers', function () {
+ communication._onRetry(new Error('oh no'), 1);
+ assert(communication.logger.debug.called);
});
- }); // Axios timing coverage
+ }); // _onRetry
describe('_challengeFromVerifier', function () {
it('covers', function () {
});
}); // 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',
- validateStatus: Communication._validateStatus,
- };
- 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',
- validateStatus: Communication._validateStatus,
- };
- 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',
- 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 () {
it('covers no path', function () {
const urlObj = new URL('https://example.com');
headers: {
link: '<https://example.com/>; rel="self", <https://hub.example.com/>;rel="hub"',
},
- data: {},
+ body: {},
}
});
it('covers', function () {
urlObj = new URL('https://thuza.ratfeathers.com/');
response = {
headers: Object.assign({}, testData.linkHeaders),
- data: testData.hCardHtml,
+ body: Buffer.from(testData.hCardHtml),
};
});
it('covers', async function () {
- response.data = testData.hCardHtml;
- communication.axios.resolves(response);
+ response.body = testData.hCardHtml;
+ communication.got.resolves(response);
expected = {
rels: {
'authorization_endpoint': ['https://ia.squeep.com/auth'],
text: '',
},
'https://thuza.ratfeathers.com/': {
- rels: ['self', 'canonical', 'author', 'me'],
+ rels: ['self', 'author', 'canonical', 'me'],
text: 'Thuza',
},
'https://thuza.ratfeathers.com/image.png': {
result = await communication.fetchMicroformat(urlObj);
assert.deepStrictEqual(result, expected);
});
- it('covers axios error', async function () {
- communication.axios.rejects(new Error('blah'));
+ it('covers got error', async function () {
+ communication.got.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.body = 'some bare text';
response.headers = {};
- communication.axios.resolves(response);
+ communication.got.resolves(response);
expected = {
items: [],
rels: {},
});
it('covers non-utf8 content', async function () {
response.headers['content-type'] = 'text/html; charset=ASCII';
- communication.axios.resolves(response);
+ communication.got.resolves(response);
expected = {
rels: {
'authorization_endpoint': ['https://ia.squeep.com/auth'],
text: '',
},
'https://thuza.ratfeathers.com/': {
- rels: ['self', 'canonical', 'author', 'me'],
+ rels: ['self', 'author', 'canonical', 'me'],
text: 'Thuza',
},
'https://thuza.ratfeathers.com/image.png': {
urlObj = new URL('https://thuza.ratfeathers.com/');
response = {
headers: Object.assign({}, testData.linkHeaders),
- data: testData.hCardHtml,
+ body: testData.hCardHtml,
};
});
it('covers', async function () {
- communication.axios.resolves(response);
+ communication.got.resolves(response);
expected = { foo: 'bar', baz: 123 };
- response.data = JSON.stringify(expected);
+ response.body = expected;
result = await communication.fetchJSON(urlObj);
assert.deepStrictEqual(result, expected);
});
- it('covers axios error', async function () {
- communication.axios.rejects(new Error('blah'));
+ it('covers got error', async function () {
+ communication.got.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.body = 'some bare text';
response.headers = {};
- communication.axios.resolves(response);
+ const error = new Error('oh no');
+ response.request = { options: { url: new URL('https://example.com/') } };
+ communication.got.rejects(new communication.Got.ParseError(error, response));
expected = undefined;
result = await communication.fetchJSON(urlObj);
beforeEach(function () {
url = 'https://example.com/';
validationOptions = {};
- sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.14' }]);
+ sinon.stub(dns.promises, 'lookup').resolves([{ family: 4, address: '10.11.12.14' }]);
});
it('rejects invalid url', async function () {
url = 'bad url';
beforeEach(function () {
url = 'https://example.com/';
validationOptions = {};
- sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
+ sinon.stub(dns.promises, 'lookup').resolves([{ family: 4, address: '10.11.12.13' }]);
});
it('rejects invalid url', async function () {
await assert.rejects(() => communication.validateClientIdentifier('bad url'), ValidationError);
assert.strictEqual(result.isLoopback, true);
});
it('accepts resolved ipv4 loopback', async function () {
- dns.lookupAsync.resolves([{ family: 4, address: '127.0.0.1' }]);
+ dns.promises.lookup.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' }]);
+ dns.promises.lookup.resolves([{ family: 6, address: '::1' }]);
const result = await communication.validateClientIdentifier(url, validationOptions);
assert.strictEqual(result.isLoopback, true);
});
assert.strictEqual(result.isLoopback, false);
});
it('rejects resolution failure', async function () {
- dns.lookupAsync.rejects(new Error('oh no'));
+ dns.promises.lookup.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' }]);
+ dns.promises.lookup.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' }]);
+ dns.promises.lookup.resolves([{ family: 5, address: '10.9.8.7' }]);
const result = await communication.validateClientIdentifier(url, validationOptions);
assert.strictEqual(result.isLoopback, false);
});
assert.strictEqual(result.isLoopback, false);
});
it('covers unresolved', async function () {
- dns.lookupAsync.resolves();
+ dns.promises.lookup.resolves();
const result = await communication.validateClientIdentifier(url, validationOptions);
assert.strictEqual(result.isLoopback, false);
});
urlObj = new URL('https://thuza.ratfeathers.com/');
response = {
headers: {},
- data: testData.multiMF2Html,
+ body: testData.multiMF2Html,
};
});
it('covers', async function () {
- communication.axios.resolves(response);
+ communication.got.resolves(response);
expected = {
items: [{
properties: {
assert.deepStrictEqual(result, expected);
});
it('covers failed fetch', async function () {
- communication.axios.rejects();
+ communication.got.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);
+ response.body = testData.noneMF2Html;
+ communication.got.resolves(response);
expected = {
items: [],
rels: {},
urlObj = new URL('https://thuza.ratfeathers.com/');
response = {
headers: {},
- data: testData.hCardHtml,
+ body: testData.hCardHtml,
};
sinon.stub(communication, 'fetchJSON');
});
describe('legacy without indieauth-metadata', function () {
it('covers', async function () {
- communication.axios.resolves(response);
+ communication.got.resolves(response);
expected = {
name: 'Thuza',
photo: 'https://thuza.ratfeathers.com/image.png',
assert.deepStrictEqual(result, expected);
});
it('covers multiple hCards', async function () {
- response.data = testData.multiMF2Html;
- communication.axios.resolves(response);
+ response.body = testData.multiMF2Html;
+ communication.got.resolves(response);
expected = {
email: undefined,
name: 'Thuza',
assert.deepStrictEqual(result, expected);
});
it('covers failed fetch', async function () {
- communication.axios.rejects();
+ communication.got.rejects();
expected = {
email: undefined,
name: undefined,
});
});
it('covers', async function () {
- response.data = testData.hCardMetadataHtml;
- communication.axios.resolves(response);
+ response.body = testData.hCardMetadataHtml;
+ communication.got.resolves(response);
communication.fetchJSON.resolves({
'issuer': 'https://ia.squeep.com/',
'authorization_endpoint': 'https://ia.squeep.com/auth',
assert.deepStrictEqual(result, expected);
});
it('covers metadata missing fields', async function () {
- response.data = testData.hCardMetadataHtml;
- communication.axios.resolves(response);
+ response.body = testData.hCardMetadataHtml;
+ communication.got.resolves(response);
communication.fetchJSON.resolves({
'issuer': 'https://ia.squeep.com/',
});
});
it('covers metadata response failure', async function () {
const jsonError = new Error('oh no');
- response.data = testData.hCardMetadataHtml;
- communication.axios
+ response.body = testData.hCardMetadataHtml;
+ communication.got
.onCall(0).resolves(response)
.onCall(1).rejects(jsonError);
communication.fetchJSON.restore();
});
}); // fetchProfile
- describe('redeemProfileCode', function () {
+ describe('fetchMetadata', function () {
+ let metadataUrl;
+ beforeEach(function () {
+ metadataUrl = new URL('https://thuza.ratfeathers.com/');
+ sinon.stub(communication, 'fetchJSON');
+ });
+ it('covers success', async function () {
+ 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',
+ });
+ const expected = {
+ 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',
+ };
+ const result = await communication.fetchMetadata(metadataUrl);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers failure', async function () {
+ communication.fetchJSON.resolves(undefined);
+ const expected = {};
+ const result = await communication.fetchMetadata(metadataUrl);
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // fetchMetadata
+
+ describe('redeemCode', function () {
let expected, urlObj, code, codeVerifier, clientId, redirectURI;
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('+', '-');
+ code = Buffer.allocUnsafe(42).toString('base64url');
+ codeVerifier = Buffer.allocUnsafe(42).toString('base64url');
clientId = 'https://example.com/';
redirectURI = 'https://example.com/_ia';
});
it('covers', async function () {
- communication.axios.resolves({
- data: '{"me":"https://profile.example.com/"}',
+ communication.got.resolves({
+ body: {
+ me: 'https://profile.example.com/',
+ },
+ });
+ expected = {
+ me: 'https://profile.example.com/',
+ };
+
+ const result = await communication.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI);
+
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers deprecated method name', async function () {
+ communication.got.resolves({
+ body: {
+ me: 'https://profile.example.com/',
+ },
});
expected = {
me: 'https://profile.example.com/',
assert.deepStrictEqual(result, expected);
});
it('covers failure', async function () {
- communication.axios.resolves('Not a JSON payload.');
+ const error = new Error('oh no');
+ const response = {
+ request: {
+ options: {
+ url: new URL('https://example.com'),
+ },
+ },
+ };
+ const parseError = new communication.Got.ParseError(error, response);
+ communication.got.rejects(parseError);
- const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI);
+ const result = await communication.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI);
assert.strictEqual(result, undefined);
});
- }); // redeemProfileCode
+ }); // redeemCode
describe('introspectToken', function () {
let introspectionUrlObj, authenticationHeader, token;
});
it('covers success active', async function () {
const nowEpoch = Math.ceil(Date.now() / 1000);
- communication.axios.resolves({
- data: JSON.stringify({
+ communication.got.resolves({
+ body: {
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({
+ communication.got.resolves({
+ body: {
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?');
+ communication.got.resolves({ body: 'what kind of response is this?' });
await assert.rejects(() => communication.introspectToken(introspectionUrlObj, authenticationHeader, token));
});
}); // introspectToken
describe('deliverTicket', function () {
- let ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket;
+ let ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket, issuerUrlObj;
beforeEach(function () {
ticketEndpointUrlObj = new URL('https://ticket.example.com/');
resourceUrlObj = new URL('https://resource.example.com/');
subjectUrlObj = new URL('https://subject.example.com/');
+ issuerUrlObj = new URL('https://idp.example.com/');
ticket = 'XXXThisIsATicketXXX';
});
it('covers success', async function () {
- const expected = { data: 'blah', statusCode: 200 };
- communication.axios.resolves(expected);
+ const expected = { body: 'blah', statusCode: 200 };
+ communication.got.resolves(expected);
+ const result = await communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket, issuerUrlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers success, no issuer', async function () {
+ const expected = { body: 'blah', statusCode: 200 };
+ communication.got.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);
+ communication.got.rejects(expectedException);
await assert.rejects(() => communication.deliverTicket(ticketEndpointUrlObj, resourceUrlObj, subjectUrlObj, ticket), expectedException);
});
}); // deliverTicket
-}); // Communication
\ No newline at end of file
+ describe('_fetchMetadataOrTokenEndpoint', function () {
+ let urlObj, metadataUrl, tokenUrl;
+ beforeEach(function () {
+ urlObj = new URL('https://idp.example.com/');
+ metadataUrl = new URL('https://idp.example.com/meta');
+ tokenUrl = new URL('https://idp.example.com/token');
+ sinon.stub(communication, 'fetchMicroformat').resolves({
+ rels: {
+ 'indieauth-metadata': [ metadataUrl.href ],
+ 'token_endpoint': [ tokenUrl.href ],
+ },
+ });
+ });
+ it('covers success', async function () {
+ const expected = {
+ metadataUrl,
+ tokenUrl: undefined,
+ };
+ const result = await communication._fetchMetadataOrTokenEndpoint(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers bad metadata url', async function () {
+ communication.fetchMicroformat.resolves({
+ rels: {
+ 'indieauth-metadata': [ 'not a url' ],
+ 'token_endpoint': [ tokenUrl.href ],
+ },
+ });
+ const expected = {
+ metadataUrl: undefined,
+ tokenUrl,
+ };
+ const result = await communication._fetchMetadataOrTokenEndpoint(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers bad token url', async function () {
+ communication.fetchMicroformat.resolves({
+ rels: {
+ 'indieauth-metadata': [],
+ 'token_endpoint': [ 'not a url' ],
+ },
+ });
+ const expected = {
+ metadataUrl: undefined,
+ tokenUrl: undefined,
+ };
+ const result = await communication._fetchMetadataOrTokenEndpoint(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers no rels', async function () {
+ communication.fetchMicroformat.resolves({
+ rels: {
+ 'indieauth-metadata': [],
+ 'token_endpoint': [],
+ },
+ });
+ const expected = {
+ metadataUrl: undefined,
+ tokenUrl: undefined,
+ };
+ const result = await communication._fetchMetadataOrTokenEndpoint(urlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers no url', async function () {
+ const expected = {
+ metadataUrl: undefined,
+ tokenUrl: undefined,
+ };
+ const result = await communication._fetchMetadataOrTokenEndpoint();
+ assert.deepStrictEqual(result, expected);
+ });
+ }); // _fetchMetadataOrTokenEndpoint
+
+ describe('redeemTicket', function () {
+ let ticket, resourceUrlObj, issuerUrlObj;
+ beforeEach(function () {
+ resourceUrlObj = new URL('https://resource.example.com/');
+ issuerUrlObj = new URL('https://idp.example.com/');
+ ticket = 'XXXThisIsATicketXXX';
+ sinon.stub(communication, '_fetchMetadataOrTokenEndpoint').resolves({
+ metadataUrl: new URL('https://example.com'),
+ tokenUrl: undefined,
+ });
+ sinon.stub(communication, 'fetchMetadata').resolves({ tokenEndpoint: 'https://idp.example.com/' });
+ });
+ it('covers success', async function () {
+ const expected = { 'access_token': 'XXXThisIsAnAccessTokenXXX' };
+ const response = { body: expected, headers: {}, statusCode: 200 };
+ communication.got.resolves(response);
+ const result = await communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers success without issuer', async function () {
+ const expected = { 'access_token': 'XXXThisIsAnAccessTokenXXX' };
+ const response = { body: expected, headers: {}, statusCode: 200 };
+ communication.got.resolves(response);
+ const result = await communication.redeemTicket(ticket, resourceUrlObj);
+ assert.deepStrictEqual(result, expected);
+ });
+ it('covers got failure', async function () {
+ const expectedException = new Error('oh no');
+ communication.got.rejects(expectedException);
+ await assert.rejects(() => communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj), expectedException);
+ });
+ it('covers no metadata url', async function () {
+ communication._fetchMetadataOrTokenEndpoint.resolves({ metadataUrl: undefined, tokenUrl: undefined });
+ await assert.rejects(() => communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj), ValidationError);
+ });
+ it('covers bad token url', async function () {
+ communication.fetchMetadata.resolves({ tokenEndpoint: 'not a url' });
+ await assert.rejects(() => communication.redeemTicket(ticket, resourceUrlObj, issuerUrlObj), ValidationError);
+ });
+ }); // redeemTicket
+
+}); // Communication