X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=test%2Flib%2Fcommunication.js;h=a94bf90f6dfed98063916f5500af4430e6d64a34;hb=e486f80a4e7a1f65498335e7408f3301c9e7cb44;hp=9a6056db723093696e4e4cc7f72561c3c3b9b54a;hpb=e648aedc1c912cd07da0b1dad7be3910248b25c9;p=squeep-indieauth-helper diff --git a/test/lib/communication.js b/test/lib/communication.js index 9a6056d..a94bf90 100644 --- a/test/lib/communication.js +++ b/test/lib/communication.js @@ -7,12 +7,12 @@ const assert = require('assert'); 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; @@ -67,12 +67,7 @@ describe('Communication', function () { 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 @@ -102,12 +97,7 @@ describe('Communication', 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')); - } + assert.throws(() => Communication.verifyChallenge(challenge, verifier, method)); }); }); // verifyChallenge @@ -160,6 +150,7 @@ describe('Communication', function () { }, params: expectedUrlObj.searchParams, responseType: 'text', + validateStatus: Communication._validateStatus, }; const result = Communication._axiosConfig(method, urlObj, body, params, { 'Content-Type': contentType, @@ -177,6 +168,7 @@ describe('Communication', function () { headers: {}, params: expectedUrlObj.searchParams, responseType: 'text', + validateStatus: Communication._validateStatus, }; const result = Communication._axiosConfig(method, urlObj); delete result.transformResponse; @@ -194,17 +186,32 @@ describe('Communication', function () { 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 () { @@ -477,6 +484,154 @@ describe('Communication', 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 () { @@ -553,6 +708,17 @@ describe('Communication', 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 () { @@ -565,50 +731,148 @@ describe('Communication', 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 () { + describe('redeemCode', 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('+', '-'); @@ -617,7 +881,19 @@ describe('Communication', function () { }); 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/', + }; + + const result = await communication.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI); + + assert.deepStrictEqual(result, expected); + }); + it('covers deprecated method name', async function () { + communication.axios.resolves({ + data: '{"me":"https://profile.example.com/"}', }); expected = { me: 'https://profile.example.com/', @@ -630,9 +906,68 @@ describe('Communication', function () { it('covers failure', async function () { communication.axios.resolves('Not a JSON payload.'); - const result = await communication.redeemProfileCode(urlObj, code, codeVerifier, clientId, redirectURI); + const result = await communication.redeemCode(urlObj, code, codeVerifier, clientId, redirectURI); assert.strictEqual(result, undefined); }); - }); -}); // Communication \ No newline at end of file + }); // redeemCode + + 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