From ad3db678501469f61e86a5e1d0f03b862b41f8b9 Mon Sep 17 00:00:00 2001 From: Anshuman Verma Date: Tue, 6 Apr 2021 21:13:22 +0530 Subject: [PATCH] test: refactor (#3150) --- test/Validation.test.js | 211 --------- .../Validation.test.js.snap.webpack4 | 47 -- .../Validation.test.js.snap.webpack5 | 47 -- .../validate-options.test.js.snap.webpack4 | 341 ++++++++++++++ .../validate-options.test.js.snap.webpack5 | 341 ++++++++++++++ test/options.test.js | 420 ----------------- test/server/Server.test.js | 146 ++++++ test/validate-options.test.js | 426 ++++++++++++++++++ 8 files changed, 1254 insertions(+), 725 deletions(-) delete mode 100644 test/Validation.test.js delete mode 100644 test/__snapshots__/Validation.test.js.snap.webpack4 delete mode 100644 test/__snapshots__/Validation.test.js.snap.webpack5 create mode 100644 test/__snapshots__/validate-options.test.js.snap.webpack4 create mode 100644 test/__snapshots__/validate-options.test.js.snap.webpack5 delete mode 100644 test/options.test.js create mode 100644 test/validate-options.test.js diff --git a/test/Validation.test.js b/test/Validation.test.js deleted file mode 100644 index 9f47662e95..0000000000 --- a/test/Validation.test.js +++ /dev/null @@ -1,211 +0,0 @@ -'use strict'; - -const webpack = require('webpack'); -const Server = require('../lib/Server'); -const config = require('./fixtures/simple-config/webpack.config'); - -describe('Validation', () => { - let compiler; - let server; - - beforeAll(() => { - compiler = webpack(config); - }); - - describe('validation', () => { - afterEach((done) => { - // `server` is undefined if a test is good - if (server) { - server.close(() => { - done(); - }); - } else { - done(); - } - }); - - const tests = [ - { - name: 'invalid `hot` configuration', - config: { hot: 'false' }, - }, - { - name: 'invalid `needClientEntry` configuration', - config: { client: { needClientEntry: 1 } }, - }, - { - name: 'invalid `needHotEntry` configuration', - config: { client: { needHotEntry: 1 } }, - }, - { - name: 'invalid `static` configuration', - config: { static: [0] }, - }, - { - name: 'no additional properties', - config: { additional: true }, - }, - ]; - - tests.forEach((test) => { - it(`should fail validation for ${test.name}`, () => { - try { - if (!test.config.static) { - test.config.static = false; - } - server = new Server(compiler, test.config); - } catch (err) { - if (err.name !== 'ValidationError') { - throw err; - } - - const [title] = err.message.split('\n\n'); - - expect(title).toMatchSnapshot(); - return; - } - - throw new Error("Validation didn't fail"); - }); - }); - }); - - describe('checkHost', () => { - afterEach((done) => { - server.close(() => { - done(); - }); - }); - - it('should always allow any host if options.firewall is disabled', () => { - const options = { - public: 'test.host:80', - firewall: false, - }; - - const headers = { - host: 'bad.host', - }; - - server = new Server(compiler, options); - - if (!server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - - it('should allow any valid options.public when host is localhost', () => { - const options = { - public: 'test.host:80', - }; - const headers = { - host: 'localhost', - }; - server = new Server(compiler, options); - if (!server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - - it('should allow any valid options.public when host is 127.0.0.1', () => { - const options = { - public: 'test.host:80', - }; - - const headers = { - host: '127.0.0.1', - }; - - server = new Server(compiler, options); - - if (!server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - - it('should allow access for every requests using an IP', () => { - const options = {}; - - const tests = [ - '192.168.1.123', - '192.168.1.2:8080', - '[::1]', - '[::1]:8080', - '[ad42::1de2:54c2:c2fa:1234]', - '[ad42::1de2:54c2:c2fa:1234]:8080', - ]; - - server = new Server(compiler, options); - - tests.forEach((test) => { - const headers = { host: test }; - - if (!server.checkHost(headers)) { - throw new Error("Validation didn't pass"); - } - }); - }); - - it("should not allow hostnames that don't match options.public", () => { - const options = { - public: 'test.host:80', - }; - - const headers = { - host: 'test.hostname:80', - }; - - server = new Server(compiler, options); - - if (server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - - it('should allow urls with scheme for checking origin', () => { - const options = { - public: 'test.host:80', - }; - const headers = { - origin: 'https://test.host', - }; - server = new Server(compiler, options); - if (!server.checkOrigin(headers)) { - throw new Error("Validation didn't fail"); - } - }); - - describe('firewall', () => { - it('should allow hosts in firewall', () => { - const tests = ['test.host', 'test2.host', 'test3.host']; - const options = { firewall: tests }; - server = new Server(compiler, options); - tests.forEach((test) => { - const headers = { host: test }; - if (!server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - }); - - it('should allow hosts that pass a wildcard in firewall', () => { - const options = { firewall: ['.example.com'] }; - server = new Server(compiler, options); - const tests = [ - 'www.example.com', - 'subdomain.example.com', - 'example.com', - 'subsubcomain.subdomain.example.com', - 'example.com:80', - 'subdomain.example.com:80', - ]; - tests.forEach((test) => { - const headers = { host: test }; - if (!server.checkHost(headers)) { - throw new Error("Validation didn't fail"); - } - }); - }); - }); - }); -}); diff --git a/test/__snapshots__/Validation.test.js.snap.webpack4 b/test/__snapshots__/Validation.test.js.snap.webpack4 deleted file mode 100644 index 55e88d26ff..0000000000 --- a/test/__snapshots__/Validation.test.js.snap.webpack4 +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Validation validation should fail validation for invalid \`hot\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.hot should be one of these: - boolean | \\"only\\" - Details: - * configuration.hot should be a boolean. - * configuration.hot should be \\"only\\"." -`; - -exports[`Validation validation should fail validation for invalid \`needClientEntry\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.client.needClientEntry should be one of these: - boolean | function - Details: - * configuration.client.needClientEntry should be a boolean. - * configuration.client.needClientEntry should be an instance of function." -`; - -exports[`Validation validation should fail validation for invalid \`needHotEntry\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.client.needHotEntry should be one of these: - boolean | function - Details: - * configuration.client.needHotEntry should be a boolean. - * configuration.client.needHotEntry should be an instance of function." -`; - -exports[`Validation validation should fail validation for invalid \`static\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.static should be one of these: - boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) - Details: - * configuration.static[0] should be one of these: - non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } - Details: - * configuration.static[0] should be a non-empty string. - * configuration.static[0] should be an object: - object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }" -`; - -exports[`Validation validation should fail validation for no additional properties 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration has an unknown property 'additional'. These properties are valid: - object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, public?, setupExitSignals?, static?, transportMode?, watchFiles? }" -`; diff --git a/test/__snapshots__/Validation.test.js.snap.webpack5 b/test/__snapshots__/Validation.test.js.snap.webpack5 deleted file mode 100644 index 55e88d26ff..0000000000 --- a/test/__snapshots__/Validation.test.js.snap.webpack5 +++ /dev/null @@ -1,47 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Validation validation should fail validation for invalid \`hot\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.hot should be one of these: - boolean | \\"only\\" - Details: - * configuration.hot should be a boolean. - * configuration.hot should be \\"only\\"." -`; - -exports[`Validation validation should fail validation for invalid \`needClientEntry\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.client.needClientEntry should be one of these: - boolean | function - Details: - * configuration.client.needClientEntry should be a boolean. - * configuration.client.needClientEntry should be an instance of function." -`; - -exports[`Validation validation should fail validation for invalid \`needHotEntry\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.client.needHotEntry should be one of these: - boolean | function - Details: - * configuration.client.needHotEntry should be a boolean. - * configuration.client.needHotEntry should be an instance of function." -`; - -exports[`Validation validation should fail validation for invalid \`static\` configuration 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration.static should be one of these: - boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) - Details: - * configuration.static[0] should be one of these: - non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } - Details: - * configuration.static[0] should be a non-empty string. - * configuration.static[0] should be an object: - object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }" -`; - -exports[`Validation validation should fail validation for no additional properties 1`] = ` -"Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. - - configuration has an unknown property 'additional'. These properties are valid: - object { bonjour?, client?, compress?, dev?, firewall?, headers?, historyApiFallback?, host?, hot?, http2?, https?, liveReload?, onAfterSetupMiddleware?, onBeforeSetupMiddleware?, onListening?, open?, port?, proxy?, public?, setupExitSignals?, static?, transportMode?, watchFiles? }" -`; diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack4 b/test/__snapshots__/validate-options.test.js.snap.webpack4 new file mode 100644 index 0000000000..d9d8353315 --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap.webpack4 @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`options validate should throw an error on the "bonjour" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.bonjour should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"host":true,"path":"","port":8080}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.host should be a string." +`; + +exports[`options validate should throw an error on the "client" option with '{"logging":"silent"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.logging should be one of these: + \\"none\\" | \\"error\\" | \\"warn\\" | \\"info\\" | \\"log\\" | \\"verbose\\"" +`; + +exports[`options validate should throw an error on the "client" option with '{"logging":"whoops!"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.logging should be one of these: + \\"none\\" | \\"error\\" | \\"warn\\" | \\"info\\" | \\"log\\" | \\"verbose\\"" +`; + +exports[`options validate should throw an error on the "client" option with '{"needClientEntry":[""]}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.needClientEntry should be one of these: + boolean | function + Details: + * configuration.client.needClientEntry should be a boolean. + * configuration.client.needClientEntry should be an instance of function." +`; + +exports[`options validate should throw an error on the "client" option with '{"needHotEntry":[""]}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.needHotEntry should be one of these: + boolean | function + Details: + * configuration.client.needHotEntry should be a boolean. + * configuration.client.needHotEntry should be an instance of function." +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":""}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay should be one of these: + boolean | object { errors?, warnings?, … } + Details: + * configuration.client.overlay should be a boolean. + * configuration.client.overlay should be an object: + object { errors?, warnings?, … }" +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":{"errors":""}}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay.errors should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":{"warnings":""}}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay.warnings should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.progress should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"unknownOption":true}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client has an unknown property 'unknownOption'. These properties are valid: + object { host?, path?, port?, logging?, progress?, overlay?, needClientEntry?, needHotEntry? }" +`; + +exports[`options validate should throw an error on the "client" option with 'whoops!' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client should be an object: + object { host?, path?, port?, logging?, progress?, overlay?, needClientEntry?, needHotEntry? }" +`; + +exports[`options validate should throw an error on the "compress" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.compress should be a boolean." +`; + +exports[`options validate should throw an error on the "dev" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.dev should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "firewall" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.firewall should be one of these: + boolean | [string, ...] (should not have fewer than 1 item) + Details: + * configuration.firewall should be a boolean. + * configuration.firewall should be an array: + [string, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "firewall" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.firewall should be an non-empty array." +`; + +exports[`options validate should throw an error on the "headers" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.headers should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "historyApiFallback" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.historyApiFallback should be one of these: + boolean | object { … } + Details: + * configuration.historyApiFallback should be a boolean. + * configuration.historyApiFallback should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "host" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.host should be one of these: + string | null + Details: + * configuration.host should be a string. + * configuration.host should be a null." +`; + +exports[`options validate should throw an error on the "hot" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.hot should be one of these: + boolean | \\"only\\" + Details: + * configuration.hot should be a boolean. + * configuration.hot should be \\"only\\"." +`; + +exports[`options validate should throw an error on the "hot" option with 'foo' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.hot should be one of these: + boolean | \\"only\\" + Details: + * configuration.hot should be a boolean. + * configuration.hot should be \\"only\\"." +`; + +exports[`options validate should throw an error on the "http2" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.http2 should be a boolean." +`; + +exports[`options validate should throw an error on the "https" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.https should be one of these: + boolean | object { passphrase?, requestCert?, ca?, key?, pfx?, cert? } + Details: + * configuration.https should be a boolean. + * configuration.https should be an object: + object { passphrase?, requestCert?, ca?, key?, pfx?, cert? }" +`; + +exports[`options validate should throw an error on the "https" option with '{"foo":"bar"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.https has an unknown property 'foo'. These properties are valid: + object { passphrase?, requestCert?, ca?, key?, pfx?, cert? }" +`; + +exports[`options validate should throw an error on the "onAfterSetupMiddleware" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onAfterSetupMiddleware should be an instance of function." +`; + +exports[`options validate should throw an error on the "onBeforeSetupMiddleware" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onBeforeSetupMiddleware should be an instance of function." +`; + +exports[`options validate should throw an error on the "onListening" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onListening should be an instance of function." +`; + +exports[`options validate should throw an error on the "open" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open should be an non-empty string." +`; + +exports[`options validate should throw an error on the "open" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open should be an non-empty array." +`; + +exports[`options validate should throw an error on the "open" option with '{"foo":"bar"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open has an unknown property 'foo'. These properties are valid: + object { target?, app? }" +`; + +exports[`options validate should throw an error on the "port" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.port should be one of these: + number | string | null + Details: + * configuration.port should be a number. + * configuration.port should be a string. + * configuration.port should be a null." +`; + +exports[`options validate should throw an error on the "proxy" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be an non-empty array." +`; + +exports[`options validate should throw an error on the "proxy" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be one of these: + object { … } | [object { … } | function, ...] (should not have fewer than 1 item) + Details: + * configuration.proxy should be an object: + object { … } + * configuration.proxy should be an array: + [object { … } | function, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "proxy" option with 'function () {}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be one of these: + object { … } | [object { … } | function, ...] (should not have fewer than 1 item) + Details: + * configuration.proxy should be an object: + object { … } + * configuration.proxy should be an array: + [object { … } | function, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "public" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.public should be a string." +`; + +exports[`options validate should throw an error on the "static" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be an non-empty string." +`; + +exports[`options validate should throw an error on the "static" option with '0' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be one of these: + boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) + Details: + * configuration.static should be a boolean. + * configuration.static should be a non-empty string. + * configuration.static should be an object: + object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } + * configuration.static should be an array: + [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "static" option with 'null' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be one of these: + boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) + Details: + * configuration.static should be a boolean. + * configuration.static should be a non-empty string. + * configuration.static should be an object: + object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } + * configuration.static should be an array: + [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "transportMode" option with '{"notAnOption":true}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode has an unknown property 'notAnOption'. These properties are valid: + object { client?, server? }" +`; + +exports[`options validate should throw an error on the "transportMode" option with '{"server":false}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode.server should be one of these: + string | function + Details: + * configuration.transportMode.server should be a string. + * configuration.transportMode.server should be an instance of function." +`; + +exports[`options validate should throw an error on the "transportMode" option with '{}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode.client should be a string." +`; + +exports[`options validate should throw an error on the "transportMode" option with 'nonexistent-implementation' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode should be an object: + object { client?, server? } + * configuration.transportMode should be one of these: + \\"sockjs\\" | \\"ws\\"" +`; + +exports[`options validate should throw an error on the "transportMode" option with 'null' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode should be an object: + object { client?, server? } + * configuration.transportMode should be one of these: + \\"sockjs\\" | \\"ws\\"" +`; + +exports[`options validate should throw an error on the "watchFiles" option with '123' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.watchFiles should be one of these: + non-empty string | object { paths?, options? } | [non-empty string | object { paths?, options? }, ...] + Details: + * configuration.watchFiles should be a non-empty string. + * configuration.watchFiles should be an object: + object { paths?, options? } + * configuration.watchFiles should be an array: + [non-empty string | object { paths?, options? }, ...]" +`; + +exports[`options validate should throw an error on the "watchFiles" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.watchFiles should be one of these: + non-empty string | object { paths?, options? } | [non-empty string | object { paths?, options? }, ...] + Details: + * configuration.watchFiles should be a non-empty string. + * configuration.watchFiles should be an object: + object { paths?, options? } + * configuration.watchFiles should be an array: + [non-empty string | object { paths?, options? }, ...]" +`; diff --git a/test/__snapshots__/validate-options.test.js.snap.webpack5 b/test/__snapshots__/validate-options.test.js.snap.webpack5 new file mode 100644 index 0000000000..d9d8353315 --- /dev/null +++ b/test/__snapshots__/validate-options.test.js.snap.webpack5 @@ -0,0 +1,341 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`options validate should throw an error on the "bonjour" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.bonjour should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"host":true,"path":"","port":8080}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.host should be a string." +`; + +exports[`options validate should throw an error on the "client" option with '{"logging":"silent"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.logging should be one of these: + \\"none\\" | \\"error\\" | \\"warn\\" | \\"info\\" | \\"log\\" | \\"verbose\\"" +`; + +exports[`options validate should throw an error on the "client" option with '{"logging":"whoops!"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.logging should be one of these: + \\"none\\" | \\"error\\" | \\"warn\\" | \\"info\\" | \\"log\\" | \\"verbose\\"" +`; + +exports[`options validate should throw an error on the "client" option with '{"needClientEntry":[""]}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.needClientEntry should be one of these: + boolean | function + Details: + * configuration.client.needClientEntry should be a boolean. + * configuration.client.needClientEntry should be an instance of function." +`; + +exports[`options validate should throw an error on the "client" option with '{"needHotEntry":[""]}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.needHotEntry should be one of these: + boolean | function + Details: + * configuration.client.needHotEntry should be a boolean. + * configuration.client.needHotEntry should be an instance of function." +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":""}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay should be one of these: + boolean | object { errors?, warnings?, … } + Details: + * configuration.client.overlay should be a boolean. + * configuration.client.overlay should be an object: + object { errors?, warnings?, … }" +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":{"errors":""}}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay.errors should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"overlay":{"warnings":""}}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.overlay.warnings should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"progress":""}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client.progress should be a boolean." +`; + +exports[`options validate should throw an error on the "client" option with '{"unknownOption":true}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client has an unknown property 'unknownOption'. These properties are valid: + object { host?, path?, port?, logging?, progress?, overlay?, needClientEntry?, needHotEntry? }" +`; + +exports[`options validate should throw an error on the "client" option with 'whoops!' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.client should be an object: + object { host?, path?, port?, logging?, progress?, overlay?, needClientEntry?, needHotEntry? }" +`; + +exports[`options validate should throw an error on the "compress" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.compress should be a boolean." +`; + +exports[`options validate should throw an error on the "dev" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.dev should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "firewall" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.firewall should be one of these: + boolean | [string, ...] (should not have fewer than 1 item) + Details: + * configuration.firewall should be a boolean. + * configuration.firewall should be an array: + [string, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "firewall" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.firewall should be an non-empty array." +`; + +exports[`options validate should throw an error on the "headers" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.headers should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "historyApiFallback" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.historyApiFallback should be one of these: + boolean | object { … } + Details: + * configuration.historyApiFallback should be a boolean. + * configuration.historyApiFallback should be an object: + object { … }" +`; + +exports[`options validate should throw an error on the "host" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.host should be one of these: + string | null + Details: + * configuration.host should be a string. + * configuration.host should be a null." +`; + +exports[`options validate should throw an error on the "hot" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.hot should be one of these: + boolean | \\"only\\" + Details: + * configuration.hot should be a boolean. + * configuration.hot should be \\"only\\"." +`; + +exports[`options validate should throw an error on the "hot" option with 'foo' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.hot should be one of these: + boolean | \\"only\\" + Details: + * configuration.hot should be a boolean. + * configuration.hot should be \\"only\\"." +`; + +exports[`options validate should throw an error on the "http2" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.http2 should be a boolean." +`; + +exports[`options validate should throw an error on the "https" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.https should be one of these: + boolean | object { passphrase?, requestCert?, ca?, key?, pfx?, cert? } + Details: + * configuration.https should be a boolean. + * configuration.https should be an object: + object { passphrase?, requestCert?, ca?, key?, pfx?, cert? }" +`; + +exports[`options validate should throw an error on the "https" option with '{"foo":"bar"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.https has an unknown property 'foo'. These properties are valid: + object { passphrase?, requestCert?, ca?, key?, pfx?, cert? }" +`; + +exports[`options validate should throw an error on the "onAfterSetupMiddleware" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onAfterSetupMiddleware should be an instance of function." +`; + +exports[`options validate should throw an error on the "onBeforeSetupMiddleware" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onBeforeSetupMiddleware should be an instance of function." +`; + +exports[`options validate should throw an error on the "onListening" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.onListening should be an instance of function." +`; + +exports[`options validate should throw an error on the "open" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open should be an non-empty string." +`; + +exports[`options validate should throw an error on the "open" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open should be an non-empty array." +`; + +exports[`options validate should throw an error on the "open" option with '{"foo":"bar"}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.open has an unknown property 'foo'. These properties are valid: + object { target?, app? }" +`; + +exports[`options validate should throw an error on the "port" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.port should be one of these: + number | string | null + Details: + * configuration.port should be a number. + * configuration.port should be a string. + * configuration.port should be a null." +`; + +exports[`options validate should throw an error on the "proxy" option with '[]' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be an non-empty array." +`; + +exports[`options validate should throw an error on the "proxy" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be one of these: + object { … } | [object { … } | function, ...] (should not have fewer than 1 item) + Details: + * configuration.proxy should be an object: + object { … } + * configuration.proxy should be an array: + [object { … } | function, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "proxy" option with 'function () {}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.proxy should be one of these: + object { … } | [object { … } | function, ...] (should not have fewer than 1 item) + Details: + * configuration.proxy should be an object: + object { … } + * configuration.proxy should be an array: + [object { … } | function, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "public" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.public should be a string." +`; + +exports[`options validate should throw an error on the "static" option with '' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be an non-empty string." +`; + +exports[`options validate should throw an error on the "static" option with '0' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be one of these: + boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) + Details: + * configuration.static should be a boolean. + * configuration.static should be a non-empty string. + * configuration.static should be an object: + object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } + * configuration.static should be an array: + [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "static" option with 'null' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.static should be one of these: + boolean | non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } | [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item) + Details: + * configuration.static should be a boolean. + * configuration.static should be a non-empty string. + * configuration.static should be an object: + object { directory?, staticOptions?, publicPath?, serveIndex?, watch? } + * configuration.static should be an array: + [non-empty string | object { directory?, staticOptions?, publicPath?, serveIndex?, watch? }, ...] (should not have fewer than 1 item)" +`; + +exports[`options validate should throw an error on the "transportMode" option with '{"notAnOption":true}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode has an unknown property 'notAnOption'. These properties are valid: + object { client?, server? }" +`; + +exports[`options validate should throw an error on the "transportMode" option with '{"server":false}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode.server should be one of these: + string | function + Details: + * configuration.transportMode.server should be a string. + * configuration.transportMode.server should be an instance of function." +`; + +exports[`options validate should throw an error on the "transportMode" option with '{}' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode.client should be a string." +`; + +exports[`options validate should throw an error on the "transportMode" option with 'nonexistent-implementation' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode should be an object: + object { client?, server? } + * configuration.transportMode should be one of these: + \\"sockjs\\" | \\"ws\\"" +`; + +exports[`options validate should throw an error on the "transportMode" option with 'null' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.transportMode should be one of these: + object { client?, server? } | \\"sockjs\\" | \\"ws\\" + Details: + * configuration.transportMode should be an object: + object { client?, server? } + * configuration.transportMode should be one of these: + \\"sockjs\\" | \\"ws\\"" +`; + +exports[`options validate should throw an error on the "watchFiles" option with '123' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.watchFiles should be one of these: + non-empty string | object { paths?, options? } | [non-empty string | object { paths?, options? }, ...] + Details: + * configuration.watchFiles should be a non-empty string. + * configuration.watchFiles should be an object: + object { paths?, options? } + * configuration.watchFiles should be an array: + [non-empty string | object { paths?, options? }, ...]" +`; + +exports[`options validate should throw an error on the "watchFiles" option with 'false' value 1`] = ` +"ValidationError: Invalid configuration object. Object has been initialized using a configuration object that does not match the API schema. + - configuration.watchFiles should be one of these: + non-empty string | object { paths?, options? } | [non-empty string | object { paths?, options? }, ...] + Details: + * configuration.watchFiles should be a non-empty string. + * configuration.watchFiles should be an object: + object { paths?, options? } + * configuration.watchFiles should be an array: + [non-empty string | object { paths?, options? }, ...]" +`; diff --git a/test/options.test.js b/test/options.test.js deleted file mode 100644 index ce1c87d4f3..0000000000 --- a/test/options.test.js +++ /dev/null @@ -1,420 +0,0 @@ -'use strict'; - -const { readFileSync } = require('fs'); -const { join } = require('path'); -const { ValidationError } = require('schema-utils'); -const webpack = require('webpack'); -const { createFsFromVolume, Volume } = require('memfs'); -const Server = require('../lib/Server'); -const options = require('../lib/options.json'); -const SockJSServer = require('../lib/servers/SockJSServer'); -const config = require('./fixtures/simple-config/webpack.config'); - -const httpsCertificateDirectory = join( - __dirname, - './fixtures/https-certificate' -); - -describe('options', () => { - jest.setTimeout(20000); - - let consoleMock; - - beforeAll(() => { - consoleMock = jest.spyOn(console, 'warn').mockImplementation(); - }); - - afterAll(() => { - consoleMock.mockRestore(); - }); - - it('should match properties and errorMessage', () => { - const properties = Object.keys(options.properties); - const messages = Object.keys(options.errorMessage.properties); - - expect(properties.length).toEqual(messages.length); - - const res = properties.every((name) => messages.includes(name)); - - expect(res).toEqual(true); - }); - - describe('validation', () => { - let server; - - afterAll((done) => { - if (server) { - server.close(done); - } else { - done(); - } - }); - - function validateOption(propertyName, cases) { - const successCount = cases.success.length; - const testCases = []; - - for (const key of Object.keys(cases)) { - testCases.push(...cases[key]); - } - - let current = 0; - - return testCases.reduce((p, value) => { - let compiler = webpack(config); - - return p - .then(() => { - server = new Server(compiler, { [propertyName]: value }); - }) - .then(() => { - if (current < successCount) { - expect(true).toBeTruthy(); - } else { - expect(false).toBeTruthy(); - } - }) - .catch((err) => { - if (current >= successCount) { - expect(err).toBeInstanceOf(ValidationError); - } else { - expect(false).toBeTruthy(); - } - }) - .then( - () => - new Promise((resolve) => { - if (server) { - server.close(() => { - compiler = null; - server = null; - - resolve(); - }); - } else { - resolve(); - } - }) - ) - .then(() => { - current += 1; - }); - }, Promise.resolve()); - } - - const memfs = createFsFromVolume(new Volume()); - // We need to patch memfs - // https://github.com/webpack/webpack-dev-middleware#fs - memfs.join = join; - - const cases = { - onAfterSetupMiddleware: { - success: [() => {}], - failure: [false], - }, - onBeforeSetupMiddleware: { - success: [() => {}], - failure: [false], - }, - bonjour: { - success: [false, true], - failure: [''], - }, - client: { - success: [ - {}, - { - host: '', - }, - { - path: '', - }, - { - port: '', - }, - { - logging: 'none', - }, - { - logging: 'error', - }, - { - logging: 'warn', - }, - { - logging: 'info', - }, - { - logging: 'log', - }, - { - logging: 'verbose', - }, - { - host: '', - path: '', - port: 8080, - logging: 'none', - }, - { - host: '', - path: '', - port: '', - }, - { - host: '', - path: '', - port: null, - }, - { - progress: false, - }, - { - overlay: true, - }, - { - overlay: {}, - }, - { - overlay: { - error: true, - }, - }, - { - overlay: { - warnings: true, - }, - }, - { - overlay: { - arbitrary: '', - }, - }, - { - needClientEntry: true, - }, - { - needHotEntry: true, - }, - ], - failure: [ - 'whoops!', - { - unknownOption: true, - }, - { - host: true, - path: '', - port: 8080, - }, - { - logging: 'whoops!', - }, - { - logging: 'silent', - }, - { - progress: '', - }, - { - overlay: '', - }, - { - overlay: { - errors: '', - }, - }, - { - overlay: { - warnings: '', - }, - }, - { - needClientEntry: [''], - }, - { - needHotEntry: [''], - }, - ], - }, - compress: { - success: [false, true], - failure: [''], - }, - dev: { - success: [{}], - failure: [''], - }, - firewall: { - success: [true, false, ['']], - failure: ['', []], - }, - headers: { - success: [{}, { foo: 'bar' }], - failure: [false], - }, - historyApiFallback: { - success: [{}, true], - failure: [''], - }, - host: { - success: ['', 'localhost', null], - failure: [false], - }, - hot: { - success: [true, 'only'], - failure: ['', 'foo'], - }, - http2: { - success: [false, true], - failure: [''], - }, - https: { - success: [ - false, - true, - { - ca: join(httpsCertificateDirectory, 'ca.pem'), - key: join(httpsCertificateDirectory, 'server.key'), - pfx: join(httpsCertificateDirectory, 'server.pfx'), - cert: join(httpsCertificateDirectory, 'server.crt'), - requestCert: true, - passphrase: 'webpack-dev-server', - }, - { - ca: readFileSync(join(httpsCertificateDirectory, 'ca.pem')), - pfx: readFileSync(join(httpsCertificateDirectory, 'server.pfx')), - key: readFileSync(join(httpsCertificateDirectory, 'server.key')), - cert: readFileSync(join(httpsCertificateDirectory, 'server.crt')), - passphrase: 'webpack-dev-server', - }, - ], - failure: [ - '', - { - foo: 'bar', - }, - ], - }, - onListening: { - success: [() => {}], - failure: [''], - }, - open: { - success: [ - true, - 'foo', - ['foo', 'bar'], - { target: true }, - { target: 'foo' }, - { target: ['foo', 'bar'] }, - { app: 'google-chrome' }, - { app: ['google-chrome', '--incognito'] }, - { target: 'foo', app: 'google-chrome' }, - { target: ['foo', 'bar'], app: ['google-chrome', '--incognito'] }, - {}, - ], - failure: ['', [], { foo: 'bar' }], - }, - port: { - success: ['', 0, null], - failure: [false], - }, - proxy: { - success: [ - { - '/api': 'http://localhost:3000', - }, - ], - failure: [[], () => {}, false], - }, - public: { - success: ['', 'foo', 'auto'], - failure: [false], - }, - static: { - success: [ - 'path', - false, - { - directory: 'path', - staticOptions: {}, - publicPath: '/', - serveIndex: true, - watch: true, - }, - { - directory: 'path', - staticOptions: {}, - publicPath: ['/public1/', '/public2/'], - serveIndex: {}, - watch: {}, - }, - [ - 'path1', - { - directory: 'path2', - staticOptions: {}, - publicPath: '/', - serveIndex: true, - watch: true, - }, - ], - ], - failure: [0, null, ''], - }, - transportMode: { - success: [ - 'ws', - 'sockjs', - { - server: 'sockjs', - }, - { - server: require.resolve('../lib/servers/SockJSServer'), - }, - { - server: SockJSServer, - }, - { - client: 'sockjs', - }, - { - client: require.resolve('../client/clients/SockJSClient'), - }, - { - server: SockJSServer, - client: require.resolve('../client/clients/SockJSClient'), - }, - ], - failure: [ - 'nonexistent-implementation', - null, - { - notAnOption: true, - }, - { - server: false, - }, - { - client: () => {}, - }, - ], - }, - watchFiles: { - success: [ - 'dir', - ['one-dir', 'two-dir'], - { paths: ['dir'] }, - { paths: ['dir'], options: { usePolling: true } }, - [{ paths: ['one-dir'] }, 'two-dir'], - ], - failure: [false, 123], - }, - }; - - Object.keys(cases).forEach((key) => { - it(key, () => validateOption(key, cases[key])); - }); - }); -}); diff --git a/test/server/Server.test.js b/test/server/Server.test.js index e7122afddf..a57c1e740a 100644 --- a/test/server/Server.test.js +++ b/test/server/Server.test.js @@ -128,6 +128,152 @@ describe('Server', () => { }); }); + describe('checkHost', () => { + let compiler; + let server; + + beforeAll(() => { + compiler = webpack(config); + }); + + afterEach((done) => { + server.close(() => { + done(); + }); + }); + + it('should always allow any host if options.firewall is disabled', () => { + const options = { + public: 'test.host:80', + firewall: false, + }; + + const headers = { + host: 'bad.host', + }; + + server = new Server(compiler, options); + + if (!server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + + it('should allow any valid options.public when host is localhost', () => { + const options = { + public: 'test.host:80', + }; + const headers = { + host: 'localhost', + }; + server = new Server(compiler, options); + if (!server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + + it('should allow any valid options.public when host is 127.0.0.1', () => { + const options = { + public: 'test.host:80', + }; + + const headers = { + host: '127.0.0.1', + }; + + server = new Server(compiler, options); + + if (!server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + + it('should allow access for every requests using an IP', () => { + const options = {}; + + const tests = [ + '192.168.1.123', + '192.168.1.2:8080', + '[::1]', + '[::1]:8080', + '[ad42::1de2:54c2:c2fa:1234]', + '[ad42::1de2:54c2:c2fa:1234]:8080', + ]; + + server = new Server(compiler, options); + + tests.forEach((test) => { + const headers = { host: test }; + + if (!server.checkHost(headers)) { + throw new Error("Validation didn't pass"); + } + }); + }); + + it("should not allow hostnames that don't match options.public", () => { + const options = { + public: 'test.host:80', + }; + + const headers = { + host: 'test.hostname:80', + }; + + server = new Server(compiler, options); + + if (server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + + it('should allow urls with scheme for checking origin', () => { + const options = { + public: 'test.host:80', + }; + const headers = { + origin: 'https://test.host', + }; + server = new Server(compiler, options); + if (!server.checkOrigin(headers)) { + throw new Error("Validation didn't fail"); + } + }); + + describe('firewall', () => { + it('should allow hosts in firewall', () => { + const tests = ['test.host', 'test2.host', 'test3.host']; + const options = { firewall: tests }; + server = new Server(compiler, options); + tests.forEach((test) => { + const headers = { host: test }; + if (!server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + }); + + it('should allow hosts that pass a wildcard in firewall', () => { + const options = { firewall: ['.example.com'] }; + server = new Server(compiler, options); + const tests = [ + 'www.example.com', + 'subdomain.example.com', + 'example.com', + 'subsubcomain.subdomain.example.com', + 'example.com:80', + 'subdomain.example.com:80', + ]; + tests.forEach((test) => { + const headers = { host: test }; + if (!server.checkHost(headers)) { + throw new Error("Validation didn't fail"); + } + }); + }); + }); + }); + describe('Invalidate Callback', () => { describe('Testing callback functions on calling invalidate without callback', () => { it('should use default `noop` callback', (done) => { diff --git a/test/validate-options.test.js b/test/validate-options.test.js new file mode 100644 index 0000000000..bdb24b948c --- /dev/null +++ b/test/validate-options.test.js @@ -0,0 +1,426 @@ +'use strict'; + +const { readFileSync } = require('fs'); +const { join } = require('path'); +const webpack = require('webpack'); +const { createFsFromVolume, Volume } = require('memfs'); +const Server = require('../lib/Server'); +const options = require('../lib/options.json'); +const SockJSServer = require('../lib/servers/SockJSServer'); +const config = require('./fixtures/simple-config/webpack.config'); + +const httpsCertificateDirectory = join( + __dirname, + './fixtures/https-certificate' +); + +const tests = { + onAfterSetupMiddleware: { + success: [() => {}], + failure: [false], + }, + onBeforeSetupMiddleware: { + success: [() => {}], + failure: [false], + }, + bonjour: { + success: [false, true], + failure: [''], + }, + client: { + success: [ + {}, + { + host: '', + }, + { + path: '', + }, + { + port: '', + }, + { + logging: 'none', + }, + { + logging: 'error', + }, + { + logging: 'warn', + }, + { + logging: 'info', + }, + { + logging: 'log', + }, + { + logging: 'verbose', + }, + { + host: '', + path: '', + port: 8080, + logging: 'none', + }, + { + host: '', + path: '', + port: '', + }, + { + host: '', + path: '', + port: null, + }, + { + progress: false, + }, + { + overlay: true, + }, + { + overlay: {}, + }, + { + overlay: { + error: true, + }, + }, + { + overlay: { + warnings: true, + }, + }, + { + overlay: { + arbitrary: '', + }, + }, + { + needClientEntry: true, + }, + { + needHotEntry: true, + }, + ], + failure: [ + 'whoops!', + { + unknownOption: true, + }, + { + host: true, + path: '', + port: 8080, + }, + { + logging: 'whoops!', + }, + { + logging: 'silent', + }, + { + progress: '', + }, + { + overlay: '', + }, + { + overlay: { + errors: '', + }, + }, + { + overlay: { + warnings: '', + }, + }, + { + needClientEntry: [''], + }, + { + needHotEntry: [''], + }, + ], + }, + compress: { + success: [false, true], + failure: [''], + }, + dev: { + success: [{}], + failure: [''], + }, + firewall: { + success: [true, false, ['']], + failure: ['', []], + }, + headers: { + success: [{}, { foo: 'bar' }], + failure: [false], + }, + historyApiFallback: { + success: [{}, true], + failure: [''], + }, + host: { + success: ['', 'localhost', null], + failure: [false], + }, + hot: { + success: [true, 'only'], + failure: ['', 'foo'], + }, + http2: { + success: [false, true], + failure: [''], + }, + https: { + success: [ + false, + true, + { + ca: join(httpsCertificateDirectory, 'ca.pem'), + key: join(httpsCertificateDirectory, 'server.key'), + pfx: join(httpsCertificateDirectory, 'server.pfx'), + cert: join(httpsCertificateDirectory, 'server.crt'), + requestCert: true, + passphrase: 'webpack-dev-server', + }, + { + ca: readFileSync(join(httpsCertificateDirectory, 'ca.pem')), + pfx: readFileSync(join(httpsCertificateDirectory, 'server.pfx')), + key: readFileSync(join(httpsCertificateDirectory, 'server.key')), + cert: readFileSync(join(httpsCertificateDirectory, 'server.crt')), + passphrase: 'webpack-dev-server', + }, + ], + failure: [ + '', + { + foo: 'bar', + }, + ], + }, + onListening: { + success: [() => {}], + failure: [''], + }, + open: { + success: [ + true, + 'foo', + ['foo', 'bar'], + { target: true }, + { target: 'foo' }, + { target: ['foo', 'bar'] }, + { app: 'google-chrome' }, + { app: ['google-chrome', '--incognito'] }, + { target: 'foo', app: 'google-chrome' }, + { target: ['foo', 'bar'], app: ['google-chrome', '--incognito'] }, + {}, + ], + failure: ['', [], { foo: 'bar' }], + }, + port: { + success: ['', 0, null], + failure: [false], + }, + proxy: { + success: [ + { + '/api': 'http://localhost:3000', + }, + ], + failure: [[], () => {}, false], + }, + public: { + success: ['', 'foo', 'auto'], + failure: [false], + }, + static: { + success: [ + 'path', + false, + { + directory: 'path', + staticOptions: {}, + publicPath: '/', + serveIndex: true, + watch: true, + }, + { + directory: 'path', + staticOptions: {}, + publicPath: ['/public1/', '/public2/'], + serveIndex: {}, + watch: {}, + }, + [ + 'path1', + { + directory: 'path2', + staticOptions: {}, + publicPath: '/', + serveIndex: true, + watch: true, + }, + ], + ], + failure: [0, null, ''], + }, + transportMode: { + success: [ + 'ws', + 'sockjs', + { + server: 'sockjs', + }, + { + server: require.resolve('../lib/servers/SockJSServer'), + }, + { + server: SockJSServer, + }, + { + client: 'sockjs', + }, + { + client: require.resolve('../client/clients/SockJSClient'), + }, + { + server: SockJSServer, + client: require.resolve('../client/clients/SockJSClient'), + }, + ], + failure: [ + 'nonexistent-implementation', + null, + { + notAnOption: true, + }, + { + server: false, + }, + { + client: () => {}, + }, + ], + }, + watchFiles: { + success: [ + 'dir', + ['one-dir', 'two-dir'], + { paths: ['dir'] }, + { paths: ['dir'], options: { usePolling: true } }, + [{ paths: ['one-dir'] }, 'two-dir'], + ], + failure: [false, 123], + }, +}; + +describe('options', () => { + jest.setTimeout(20000); + + let consoleMock; + + beforeAll(() => { + consoleMock = jest.spyOn(console, 'warn').mockImplementation(); + }); + + afterAll(() => { + consoleMock.mockRestore(); + }); + + it('should match properties and errorMessage', () => { + const properties = Object.keys(options.properties); + const messages = Object.keys(options.errorMessage.properties); + + expect(properties.length).toEqual(messages.length); + + const res = properties.every((name) => messages.includes(name)); + + expect(res).toEqual(true); + }); + + describe('validate', () => { + function stringifyValue(value) { + if ( + Array.isArray(value) || + (value && typeof value === 'object' && value.constructor === Object) + ) { + return JSON.stringify(value, (_key, replacedValue) => { + if ( + replacedValue && + replacedValue.type && + replacedValue.type === 'Buffer' + ) { + return ''; + } + + if (typeof replacedValue === 'string') { + replacedValue = replacedValue + .replace(/\\/g, '/') + .replace( + new RegExp(process.cwd().replace(/\\/g, '/'), 'g'), + '' + ); + } + + return replacedValue; + }); + } + + return value; + } + + function createTestCase(type, key, value) { + it(`should ${ + type === 'success' ? 'successfully validate' : 'throw an error on' + } the "${key}" option with '${stringifyValue(value)}' value`, (done) => { + let compiler = webpack(config); + let server; + let thrownError; + + try { + server = new Server(compiler, { [key]: value }); + } catch (error) { + thrownError = error; + } + + if (type === 'success') { + expect(thrownError).toBeUndefined(); + } else { + expect(thrownError).not.toBeUndefined(); + expect(thrownError.toString()).toMatchSnapshot(); + } + + if (server) { + server.close(() => { + compiler = null; + server = null; + + done(); + }); + } else { + done(); + } + }); + } + + const memfs = createFsFromVolume(new Volume()); + + // We need to patch memfs + // https://github.com/webpack/webpack-dev-middleware#fs + memfs.join = join; + + for (const [key, values] of Object.entries(tests)) { + for (const type of Object.keys(values)) { + for (const value of values[type]) { + createTestCase(type, key, value); + } + } + } + }); +});