diff --git a/lib/Server.js b/lib/Server.js index 091b617a28..c736638b29 100644 --- a/lib/Server.js +++ b/lib/Server.js @@ -769,6 +769,7 @@ class Server { // eslint-disable-next-line no-shadow .then((port) => { this.port = port; + return this.server.listen(port, this.hostname, (error) => { if (this.options.hot || this.options.liveReload) { this.createSocketServer(); diff --git a/lib/utils/DevServerPlugin.js b/lib/utils/DevServerPlugin.js index 3da13aaab0..e96b4066f3 100644 --- a/lib/utils/DevServerPlugin.js +++ b/lib/utils/DevServerPlugin.js @@ -49,8 +49,8 @@ class DevServerPlugin { /** @type {string} */ const port = - options.client && options.client.port - ? `&port=${options.client.port}` + (options.client && options.client.port) || options.port + ? `&port=${options.client.port || options.port}` : ''; /** @type {string} */ diff --git a/test/e2e/ClientOptions.test.js b/test/e2e/ClientOptions.test.js index e0d5bb2248..403afda8c7 100644 --- a/test/e2e/ClientOptions.test.js +++ b/test/e2e/ClientOptions.test.js @@ -1,350 +1,420 @@ 'use strict'; const express = require('express'); +const internalIp = require('internal-ip'); const { createProxyMiddleware } = require('http-proxy-middleware'); -const request = require('supertest'); const testServer = require('../helpers/test-server'); const config = require('../fixtures/client-config/webpack.config'); const runBrowser = require('../helpers/run-browser'); const [port1, port2, port3] = require('../ports-map').ClientOptions; const { beforeBrowserCloseDelay } = require('../helpers/puppeteer-constants'); -describe('sockjs client proxy', () => { - function startProxy(port, cb) { - const proxy = express(); - proxy.use( - '/', - createProxyMiddleware({ - target: `http://localhost:${port1}`, - ws: true, - changeOrigin: true, - logLevel: 'warn', - }) - ); - return proxy.listen(port, cb); - } - - beforeAll((done) => { - const options = { - transportMode: 'sockjs', - compress: true, - port: port1, - host: '0.0.0.0', - firewall: false, - hot: true, - }; - testServer.startAwaitingCompilation(config, options, done); - }); +const transportModes = ['ws', 'sockjs']; + +for (const transportMode of transportModes) { + const websocketUrlProtocol = transportMode === 'ws' ? 'ws' : 'http'; + // Using Chrome DevTools protocol directly for now: + // TODO refactor, we should wait ws connection to specific location then wait it will be finished success and run callback + const waitForTest = (browser, page, filter, callback) => { + if (transportMode === 'sockjs') { + return page + .waitForRequest((requestObj) => requestObj.url().match(filter)) + .then((requestObj) => { + page.waitForTimeout(beforeBrowserCloseDelay).then(() => { + browser.close().then(() => { + callback(requestObj.url()); + }); + }); + }); + } - afterAll(testServer.close); + const client = page._client; - // [HPM] Proxy created: / -> http://localhost:{port1} - describe('behind a proxy', () => { - let proxy; + client.on('Network.webSocketCreated', (event) => { + page.waitForTimeout(beforeBrowserCloseDelay).then(() => { + browser.close().then(() => callback(event.url)); + }); + }); + }; + + describe(`should work behind proxy, when hostnames are same and ports are different (${transportMode})`, () => { + const devServerHost = '127.0.0.1'; + const devServerPort = port1; + const proxyHost = devServerHost; + const proxyPort = port2; + + function startProxy(cb) { + const proxy = express(); + + proxy.use( + '/', + createProxyMiddleware({ + target: `http://${devServerHost}:${devServerPort}`, + ws: true, + changeOrigin: true, + logLevel: 'warn', + }) + ); + + return proxy.listen(proxyPort, proxyHost, cb); + } beforeAll((done) => { - proxy = startProxy(port2, done); + const options = { + transportMode, + port: devServerPort, + host: devServerHost, + firewall: false, + hot: true, + }; + + testServer.startAwaitingCompilation(config, options, done); }); - afterAll((done) => { - proxy.close(() => { - done(); + afterAll(testServer.close); + + describe('behind a proxy', () => { + let proxy; + + beforeAll((done) => { + proxy = startProxy(() => { + done(); + }); + }); + + afterAll((done) => { + proxy.close(() => { + done(); + }); }); - }); - it('responds with a 200 on proxy port', (done) => { - const req = request(`http://localhost:${port2}`); - req.get('/ws').expect(200, 'Welcome to SockJS!\n', done); + it('requests websocket through the proxy', (done) => { + runBrowser().then(async ({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://${proxyHost}:${devServerPort}/ws` + ); + + done(); + }); + + page.goto(`http://${proxyHost}:${proxyPort}/main`); + }); + }); }); + }); + + describe(`should work behind proxy, when hostnames are different and ports are same (${transportMode})`, () => { + const devServerHost = '127.0.0.1'; + const devServerPort = port1; + const proxyHost = internalIp.v4.sync(); + const proxyPort = port1; + + function startProxy(cb) { + const proxy = express(); - it('responds with a 200 on non-proxy port', (done) => { - const req = request(`http://localhost:${port1}`); - req.get('/ws').expect(200, 'Welcome to SockJS!\n', done); + proxy.use( + '/', + createProxyMiddleware({ + target: `http://${devServerHost}:${devServerPort}`, + ws: true, + changeOrigin: true, + logLevel: 'warn', + }) + ); + + return proxy.listen(proxyPort, proxyHost, cb); + } + + beforeAll((done) => { + const options = { + transportMode, + port: devServerPort, + host: devServerHost, + firewall: false, + hot: true, + }; + + testServer.startAwaitingCompilation(config, options, done); }); - it('requests websocket through the proxy with proper port number', (done) => { - runBrowser().then(async ({ page, browser }) => { - page - .waitForRequest((requestObj) => requestObj.url().match(/ws/)) - .then((requestObj) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(requestObj.url()).toContain( - `http://localhost:${port2}/ws` - ); - done(); - }); - }); + afterAll(testServer.close); + + describe('behind a proxy', () => { + let proxy; + + beforeAll((done) => { + proxy = startProxy(() => { + done(); + }); + }); + + afterAll((done) => { + proxy.close(() => { + done(); + }); + }); + + it('requests websocket through the proxy', (done) => { + runBrowser().then(async ({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://${proxyHost}:${proxyPort}/ws` + ); + + done(); }); - page.goto(`http://localhost:${port2}/main`); + + page.goto(`http://${proxyHost}:${proxyPort}/main`); + }); }); }); }); -}); -describe('ws client proxy', () => { - function startProxy(port, cb) { - const proxy = express(); - proxy.use( - '/', - createProxyMiddleware({ - target: `http://localhost:${port1}`, - ws: true, - changeOrigin: true, - }) - ); - return proxy.listen(port, cb); - } - - beforeAll((done) => { - const options = { - transportMode: 'ws', - compress: true, - port: port1, - host: '0.0.0.0', - firewall: false, - hot: true, - public: 'myhost', - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe(`should work behind proxy, when hostnames are different and ports are different (${transportMode})`, () => { + const devServerHost = '127.0.0.1'; + const devServerPort = port1; + const proxyHost = internalIp.v4.sync(); + const proxyPort = port2; - afterAll(testServer.close); + function startProxy(cb) { + const proxy = express(); - // [HPM] Proxy created: / -> http://localhost:{port1} - describe('behind a proxy', () => { - let proxy; + proxy.use( + '/', + createProxyMiddleware({ + target: `http://${devServerHost}:${devServerPort}`, + ws: true, + changeOrigin: true, + logLevel: 'warn', + }) + ); + + return proxy.listen(proxyPort, proxyHost, cb); + } beforeAll((done) => { - proxy = startProxy(port2, done); + const options = { + client: { host: devServerHost }, + transportMode, + port: devServerPort, + host: devServerHost, + firewall: false, + hot: true, + static: true, + }; + + testServer.startAwaitingCompilation(config, options, done); }); - afterAll((done) => { - proxy.close(() => { - done(); + afterAll(testServer.close); + + describe('behind a proxy', () => { + let proxy; + + beforeAll((done) => { + proxy = startProxy(() => { + done(); + }); }); - }); - // Using Chrome DevTools protocol directly for now: - // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-webSocketCreated - // TODO: listen for websocket requestType via puppeteer when added - // to puppeteer: https://github.com/puppeteer/puppeteer/issues/2974 - it('requests websocket through the proxy with proper port number', (done) => { - runBrowser().then(({ page, browser }) => { - const client = page._client; - client.on('Network.webSocketCreated', (evt) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(evt.url).toContain(`ws://myhost:${port2}/ws`); - done(); - }); + afterAll((done) => { + proxy.close(() => { + done(); + }); + }); + + it('requests websocket through the proxy', (done) => { + runBrowser().then(async ({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://${devServerHost}:${devServerPort}/ws` + ); + + done(); }); + + page.goto(`http://${proxyHost}:${proxyPort}/main`); }); - page.goto(`http://localhost:${port2}/main`); }); }); }); -}); -describe('sockjs public and client path', () => { - beforeAll((done) => { - const options = { - transportMode: 'sockjs', - port: port2, - host: '0.0.0.0', - public: 'myhost.test', - client: { - path: '/foo/test/bar/', - }, - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe('should work with custom client port and path', () => { + beforeAll((done) => { + const options = { + transportMode, + port: port2, + host: '0.0.0.0', + client: { + path: '/foo/test/bar/', + port: port3, + }, + }; - afterAll(testServer.close); - - describe('browser client', () => { - it('uses the correct public hostname and path', (done) => { - runBrowser().then(({ page, browser }) => { - page - .waitForRequest((requestObj) => - requestObj.url().match(/foo\/test\/bar/) - ) - .then((requestObj) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(requestObj.url()).toContain( - `http://myhost.test:${port2}/foo/test/bar` - ); - done(); - }); - }); + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('uses correct port and path', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://localhost:${port3}/foo/test/bar` + ); + + done(); }); - page.goto(`http://localhost:${port2}/main`); + + page.goto(`http://localhost:${port2}/main`); + }); }); }); }); -}); -describe('sockjs client path and port', () => { - beforeAll((done) => { - const options = { - transportMode: 'sockjs', - port: port2, - host: '0.0.0.0', - client: { - path: '/foo/test/bar/', - port: port3, - }, - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe('should work with custom client port and without path', () => { + beforeAll((done) => { + const options = { + transportMode, + port: port2, + host: '0.0.0.0', + client: { + port: port3, + }, + }; - afterAll(testServer.close); - - describe('browser client', () => { - it('uses correct port and path', (done) => { - runBrowser().then(({ page, browser }) => { - page - .waitForRequest((requestObj) => - requestObj.url().match(/foo\/test\/bar/) - ) - .then((requestObj) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(requestObj.url()).toContain( - `http://localhost:${port3}/foo/test/bar` - ); - done(); - }); - }); + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('uses correct port and path', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://localhost:${port3}/ws` + ); + + done(); }); - page.goto(`http://localhost:${port2}/main`); + page.goto(`http://localhost:${port2}/main`); + }); }); }); }); -}); -// previously, using port without path had the ability -// to alter the path (based on a bug in client-src/default/index.js) -// so we need to make sure path is not altered in this case -describe('sockjs client port, no path', () => { - beforeAll((done) => { - const options = { - transportMode: 'sockjs', - port: port2, - host: '0.0.0.0', - client: { - port: port3, - }, - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe('should work with custom client', () => { + beforeAll((done) => { + const options = { + transportMode, + port: port2, + host: '0.0.0.0', + client: { + host: 'myhost.test', + }, + }; + testServer.startAwaitingCompilation(config, options, done); + }); - afterAll(testServer.close); - - describe('browser client', () => { - it('uses correct port and path', (done) => { - runBrowser().then(({ page, browser }) => { - page - .waitForRequest((requestObj) => requestObj.url().match(/ws/)) - .then((requestObj) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(requestObj.url()).toContain( - `http://localhost:${port3}/ws` - ); - done(); - }); - }); + afterAll(testServer.close); + + describe('browser client', () => { + it('uses correct host', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /ws/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://myhost.test:${port2}/ws` + ); + + done(); }); - page.goto(`http://localhost:${port2}/main`); + + page.goto(`http://localhost:${port2}/main`); + }); }); }); }); -}); -describe('sockjs client host', () => { - beforeAll((done) => { - const options = { - transportMode: 'sockjs', - port: port2, - host: '0.0.0.0', - client: { - host: 'myhost.test', - }, - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe('should work with custom client host, port, and path', () => { + beforeAll((done) => { + const options = { + transportMode, + port: port2, + host: '0.0.0.0', + client: { + host: 'myhost', + port: port3, + path: '/foo/test/bar/', + }, + }; - afterAll(testServer.close); - - describe('browser client', () => { - it('uses correct host', (done) => { - runBrowser().then(({ page, browser }) => { - page - .waitForRequest((requestObj) => requestObj.url().match(/ws/)) - .then((requestObj) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(requestObj.url()).toContain( - `http://myhost.test:${port2}/ws` - ); - done(); - }); - }); + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('uses correct host, port, and path', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://myhost:${port3}/foo/test/bar` + ); + + done(); }); - page.goto(`http://localhost:${port2}/main`); + + page.goto(`http://localhost:${port2}/main`); + }); }); }); }); -}); -describe('ws client host, port, and path', () => { - beforeAll((done) => { - const options = { - transportMode: 'ws', - port: port2, - host: '0.0.0.0', - client: { - host: 'myhost', - port: port3, - path: '/foo/test/bar/', - }, - }; - testServer.startAwaitingCompilation(config, options, done); - }); + describe('should work with the "public" option and custom client path', () => { + beforeAll((done) => { + const options = { + transportMode, + port: port2, + host: '0.0.0.0', + public: 'myhost.test', + client: { + path: '/foo/test/bar/', + }, + }; - afterAll(testServer.close); - - describe('browser client', () => { - // Using Chrome DevTools protocol directly for now: - // https://chromedevtools.github.io/devtools-protocol/tot/Network/#event-webSocketCreated - // TODO: listen for websocket requestType via puppeteer when added - // to puppeteer: https://github.com/puppeteer/puppeteer/issues/2974 - it('uses correct host, port, and path', (done) => { - runBrowser().then(({ page, browser }) => { - const client = page._client; - client.on('Network.webSocketCreated', (evt) => { - page.waitForTimeout(beforeBrowserCloseDelay).then(() => { - browser.close().then(() => { - expect(evt.url).toContain(`ws://myhost:${port3}/foo/test/bar`); - done(); - }); + testServer.startAwaitingCompilation(config, options, done); + }); + + afterAll(testServer.close); + + describe('browser client', () => { + it('uses the correct public hostname and path', (done) => { + runBrowser().then(({ page, browser }) => { + waitForTest(browser, page, /foo\/test\/bar/, (websocketUrl) => { + expect(websocketUrl).toContain( + `${websocketUrlProtocol}://myhost.test:${port2}/foo/test/bar` + ); + + done(); }); + + page.goto(`http://localhost:${port2}/main`); }); - page.goto(`http://localhost:${port2}/main`); }); }); }); -}); +} describe('Client console.log', () => { const baseOptions = { port: port2, host: '0.0.0.0', }; - const transportModes = [ + const transportModesForClientLog = [ {}, { transportMode: 'sockjs' }, { transportMode: 'ws' }, @@ -392,7 +462,7 @@ describe('Client console.log', () => { }, ]; - transportModes.forEach(async (mode) => { + transportModesForClientLog.forEach(async (mode) => { cases.forEach(async ({ title, options }) => { title += ` (${ Object.keys(mode).length ? mode.transportMode : 'default' diff --git a/test/server/__snapshots__/Server.test.js.snap.webpack4 b/test/server/__snapshots__/Server.test.js.snap.webpack4 index a8b9fcd67e..155cb1b415 100644 --- a/test/server/__snapshots__/Server.test.js.snap.webpack4 +++ b/test/server/__snapshots__/Server.test.js.snap.webpack4 @@ -5,7 +5,7 @@ Array [ Array [ "client", "index.js?http:", - "0.0.0.0", + "0.0.0.0&port=8100", ], Array [ "node_modules", @@ -24,7 +24,7 @@ Array [ Array [ "client", "index.js?http:", - "0.0.0.0", + "0.0.0.0&port=8100", ], Array [ "node_modules", diff --git a/test/server/__snapshots__/Server.test.js.snap.webpack5 b/test/server/__snapshots__/Server.test.js.snap.webpack5 index a8b9fcd67e..155cb1b415 100644 --- a/test/server/__snapshots__/Server.test.js.snap.webpack5 +++ b/test/server/__snapshots__/Server.test.js.snap.webpack5 @@ -5,7 +5,7 @@ Array [ Array [ "client", "index.js?http:", - "0.0.0.0", + "0.0.0.0&port=8100", ], Array [ "node_modules", @@ -24,7 +24,7 @@ Array [ Array [ "client", "index.js?http:", - "0.0.0.0", + "0.0.0.0&port=8100", ], Array [ "node_modules", diff --git a/test/server/utils/findPort.test.js b/test/server/utils/findPort.test.js index 1335d610e6..8f8eddd911 100644 --- a/test/server/utils/findPort.test.js +++ b/test/server/utils/findPort.test.js @@ -31,10 +31,18 @@ describe('findPort', () => { (p, _, i) => p.then( () => - new Promise((resolve) => { + new Promise((resolve, reject) => { const server = http.createServer(); + dummyServers.push(server); - server.listen(8080 + i, resolve); + + server.listen(8080 + i, () => { + resolve(); + }); + + server.on('error', (error) => { + reject(error); + }); }) ), Promise.resolve()