separate validateClientIdentifier from fetchClientIdentifier, add validateProfile
[squeep-indieauth-helper] / test / lib / communication.js
index 5c41469db57a763c3fc202cb769c77b5566e1e3b..f544d80f67304ce8e47468623368b635e487e1a7 100644 (file)
@@ -7,6 +7,8 @@ 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');
@@ -516,6 +518,165 @@ describe('Communication', function () {
     });
   }); // fetchJSON
 
+  describe('validateProfile', function () {
+    let url, validationOptions;
+    beforeEach(function () {
+      url = 'https://example.com/';
+      options = {};
+      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/';
+      options = {};
+      sinon.stub(dns, 'lookupAsync').resolves([{ family: 4, address: '10.11.12.13' }]);
+    });
+    it('rejects invalid url', async function () {
+      try {
+        await communication.validateClientIdentifier('bad url');
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects invalid scheme', async function () {
+      url = 'ftp://example.com/';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects fragment', async function () {
+      url = 'https://example.com/#foo';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects username', async function () {
+      url = 'https://user@example.com/';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects password', async function () {
+      url = 'https://:foo@example.com/';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects relative path', async function () {
+      url = 'https://example.com/client/../sneaky';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects ipv4', async function () {
+      url = 'https://10.11.12.13/';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects ipv6', async function () {
+      url = 'https://[fd64:defa:00e5:caf4:0dff::ad39]/';
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof 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'));
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof ValidationError);
+      }
+    });
+    it('rejects mismatched resolutions', async function () {
+      dns.lookupAsync.onCall(1).resolves([{ family: 4, address: '10.9.8.7' }]);
+      try {
+        await communication.validateClientIdentifier(url, validationOptions);
+        assert.fail(noExpectedException);
+      } catch (e) {
+        assert(e instanceof 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 () {
@@ -592,6 +753,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 () {