From cadd4d1dd0a54e7ccf7d84dace3456fbc8b7b590 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Wed, 28 Sep 2022 16:01:13 -0800 Subject: [PATCH] chore: migrate http server to ts (#17677) --- package-lock.json | 19 ++ package.json | 1 + tests/page/page-event-network.spec.ts | 2 +- .../assets/simple-server-with-ready-route.js | 33 ++-- tests/playwright-test/assets/simple-server.js | 47 +++-- utils/testserver/index.d.ts | 44 ----- utils/testserver/{index.js => index.ts} | 177 ++++++------------ 7 files changed, 136 insertions(+), 187 deletions(-) delete mode 100644 utils/testserver/index.d.ts rename utils/testserver/{index.js => index.ts} (68%) diff --git a/package-lock.json b/package-lock.json index 131e3be854..cb34847e8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/resize-observer-browser": "^0.1.7", + "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", @@ -1294,6 +1295,15 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/xml2js": { "version": "0.4.9", "dev": true, @@ -9205,6 +9215,15 @@ "version": "0.16.1", "dev": true }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/xml2js": { "version": "0.4.9", "dev": true, diff --git a/package.json b/package.json index b41942675e..2373b9ddeb 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "@types/react": "^18.0.12", "@types/react-dom": "^18.0.5", "@types/resize-observer-browser": "^0.1.7", + "@types/ws": "^8.5.3", "@types/xml2js": "^0.4.9", "@typescript-eslint/eslint-plugin": "^5.10.2", "@typescript-eslint/parser": "^5.10.2", diff --git a/tests/page/page-event-network.spec.ts b/tests/page/page-event-network.spec.ts index 505ca3f673..490a0f8ce1 100644 --- a/tests/page/page-event-network.spec.ts +++ b/tests/page/page-event-network.spec.ts @@ -15,7 +15,7 @@ * limitations under the License. */ -import type { ServerResponse } from '../../utils/testserver'; +import type { ServerResponse } from 'http'; import { test as it, expect } from './pageTest'; it('Page.Events.Request @smoke', async ({ page, server }) => { diff --git a/tests/playwright-test/assets/simple-server-with-ready-route.js b/tests/playwright-test/assets/simple-server-with-ready-route.js index 5c5236bb9d..ced6dfe3bc 100644 --- a/tests/playwright-test/assets/simple-server-with-ready-route.js +++ b/tests/playwright-test/assets/simple-server-with-ready-route.js @@ -1,15 +1,26 @@ -const { TestServer } = require('../../../utils/testserver/'); -TestServer.create(__dirname, process.argv[2] || 3000).then(server => { - console.log('listening on port', server.PORT); - let ready = false; - setTimeout(() => ready = true, 750); - server.setRoute('/ready', (message, response) => { +const http = require('http'); + +const port = process.argv[2] || 3000; + +let ready = false; +setTimeout(() => ready = true, 750); + +const requestListener = function (req, res) { + if (req.url === '/ready') { if (ready) { - response.statusCode = 200; - response.end('hello'); + res.writeHead(200); + res.end('hello'); } else { - response.statusCode = 404; - response.end('not-ready'); + res.writeHead(404); + res.end('not-ready'); } - }); + } else { + res.writeHead(404); + res.end(); + } +}; + +const server = http.createServer(requestListener); +server.listen(port, () => { + console.log('listening on port', port); }); diff --git a/tests/playwright-test/assets/simple-server.js b/tests/playwright-test/assets/simple-server.js index b1d7b217af..0a78049cff 100644 --- a/tests/playwright-test/assets/simple-server.js +++ b/tests/playwright-test/assets/simple-server.js @@ -1,21 +1,36 @@ -const { TestServer } = require('../../../utils/testserver/'); +const http = require('http'); + console.error('error from server'); + +const port = process.argv[2] || 3000; + +const requestListener = function (req, res) { + if (req.url === '/hello') { + res.end('hello'); + return; + } + if (req.url === '/env-FOO') { + res.end(process.env.FOO); + return; + } + if (req.url === '/port') { + res.end('' + port); + return; + } + if (req.url === '/redirect') { + res.writeHead(301, 'Moved'); + res.end(); + return; + } + res.writeHead(404); + res.end(); +}; + +const server = http.createServer(requestListener); + // delay creating the server to test waiting for it setTimeout(() => { - TestServer.create(__dirname, process.argv[2] || 3000).then(server => { - console.log('listening on port', server.PORT); - server.setRoute('/hello', (message, response) => { - response.end('hello'); - }); - server.setRoute('/env-FOO', (message, response) => { - response.end(process.env.FOO); - }); - server.setRoute('/port', (_, response) => { - response.end('' + server.PORT); - }); - server.setRoute('/redirect', (_, response) => { - response.writeHead(301, 'Moved'); - response.end(); - }); + server.listen(port, () => { + console.log('listening on port', port); }); }, process.argv[3] ? +process.argv[3] : 0); diff --git a/utils/testserver/index.d.ts b/utils/testserver/index.d.ts deleted file mode 100644 index 46467c5af7..0000000000 --- a/utils/testserver/index.d.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright (c) Microsoft Corporation. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -type ServerResponse = import('http').ServerResponse; -type IncomingMessage = import('http').IncomingMessage; -import WebSocket from 'ws'; - -export class TestServer { - static create(dirPath: string, port: number, loopback?: string): Promise; - static createHTTPS(dirPath: string, port: number, loopback?: string): Promise; - enableHTTPCache(pathPrefix: string); - setAuth(path: string, username: string, password: string); - enableGzip(path: string); - setCSP(path: string, csp: string); - setExtraHeaders(path: string, headers: { [key: string]: string }); - stop(): Promise; - setRoute(path: string, handler: (message: IncomingMessage & { postBody: Promise }, response: ServerResponse) => void); - setRedirect(from: string, to: string); - waitForRequest(path: string): Promise }>; - waitForWebSocketConnectionRequest(): Promise; - onceWebSocketConnection(handler: (ws: WebSocket, request: IncomingMessage) => void); - sendOnWebSocketConnection(data: string); - reset(); - serveFile(request: IncomingMessage, response: ServerResponse); - serveFile(request: IncomingMessage, response: ServerResponse, filePath: string); - - PORT: number; - PREFIX: string; - CROSS_PROCESS_PREFIX: string; - EMPTY_PAGE: string; -} diff --git a/utils/testserver/index.js b/utils/testserver/index.ts similarity index 68% rename from utils/testserver/index.js rename to utils/testserver/index.ts index 7fd92731d5..1f0d5bfa7a 100644 --- a/utils/testserver/index.js +++ b/utils/testserver/index.ts @@ -1,5 +1,6 @@ /** * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,41 +15,48 @@ * limitations under the License. */ -const http = require('http'); -const https = require('https'); -const url = require('url'); -const fs = require('fs'); -const path = require('path'); -const zlib = require('zlib'); -const util = require('util'); -const mime = require('mime'); -const WebSocketServer = require('ws').Server; +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import mime from 'mime'; +import type net from 'net'; +import path from 'path'; +import url from 'url'; +import util from 'util'; +import ws from 'ws'; +import zlib, { gzip } from 'zlib'; const fulfillSymbol = Symbol('fullfil callback'); const rejectSymbol = Symbol('reject callback'); -const gzipAsync = util.promisify(zlib.gzip.bind(zlib)); +const gzipAsync = util.promisify(gzip.bind(zlib)); -class TestServer { - /** - * @param {string} dirPath - * @param {number} port - * @param {string=} loopback - * @return {!Promise} - */ - static async create(dirPath, port, loopback) { +export class TestServer { + private _server: http.Server; + private _wsServer: ws.WebSocketServer; + private _dirPath: string; + readonly debugServer: any; + private _startTime: Date; + private _cachedPathPrefix: string | null; + private _sockets = new Set(); + private _routes = new Map any>(); + private _auths = new Map(); + private _csp = new Map(); + private _extraHeaders = new Map(); + private _gzipRoutes = new Set(); + private _requestSubscribers = new Map>(); + readonly PORT: number; + readonly PREFIX: string; + readonly CROSS_PROCESS_PREFIX: string; + readonly EMPTY_PAGE: string; + + static async create(dirPath: string, port: number, loopback?: string): Promise { const server = new TestServer(dirPath, port, loopback); await new Promise(x => server._server.once('listening', x)); return server; } - /** - * @param {string} dirPath - * @param {number} port - * @param {string=} loopback - * @return {!Promise} - */ - static async createHTTPS(dirPath, port, loopback) { + static async createHTTPS(dirPath: string, port: number, loopback?: string): Promise { const server = new TestServer(dirPath, port, loopback, { key: await fs.promises.readFile(path.join(__dirname, 'key.pem')), cert: await fs.promises.readFile(path.join(__dirname, 'cert.pem')), @@ -58,21 +66,15 @@ class TestServer { return server; } - /** - * @param {string} dirPath - * @param {number} port - * @param {string=} loopback - * @param {!Object=} sslOptions - */ - constructor(dirPath, port, loopback, sslOptions) { + constructor(dirPath: string, port: number, loopback?: string, sslOptions?: object) { if (sslOptions) this._server = https.createServer(sslOptions, this._onRequest.bind(this)); else this._server = http.createServer(this._onRequest.bind(this)); this._server.on('connection', socket => this._onSocket(socket)); - this._wsServer = new WebSocketServer({ noServer: true }); + this._wsServer = new ws.WebSocketServer({ noServer: true }); this._server.on('upgrade', async (request, socket, head) => { - const pathname = url.parse(request.url).pathname; + const pathname = url.parse(request.url!).path; if (pathname === '/ws-slow') await new Promise(f => setTimeout(f, 2000)); if (!['/ws', '/ws-slow'].includes(pathname)) { @@ -92,21 +94,6 @@ class TestServer { this._startTime = new Date(); this._cachedPathPrefix = null; - /** @type {!Set} */ - this._sockets = new Set(); - /** @type {!Map} */ - this._routes = new Map(); - /** @type {!Map} */ - this._auths = new Map(); - /** @type {!Map} */ - this._csp = new Map(); - /** @type {!Map} */ - this._extraHeaders = new Map(); - /** @type {!Set} */ - this._gzipRoutes = new Set(); - /** @type {!Map} */ - this._requestSubscribers = new Map(); - const cross_origin = loopback || '127.0.0.1'; const same_origin = loopback || 'localhost'; const protocol = sslOptions ? 'https' : 'http'; @@ -116,51 +103,35 @@ class TestServer { this.EMPTY_PAGE = `${protocol}://${same_origin}:${port}/empty.html`; } - _onSocket(socket) { + _onSocket(socket: net.Socket) { this._sockets.add(socket); // ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given // that tab closing aborts outgoing connections to the server. socket.on('error', error => { - if (error.code !== 'ECONNRESET' && error.code !== 'HPE_INVALID_EOF_STATE') + if ((error as any).code !== 'ECONNRESET' && (error as any).code !== 'HPE_INVALID_EOF_STATE') throw error; }); socket.once('close', () => this._sockets.delete(socket)); } - /** - * @param {string} pathPrefix - */ - enableHTTPCache(pathPrefix) { + enableHTTPCache(pathPrefix: string) { this._cachedPathPrefix = pathPrefix; } - /** - * @param {string} path - * @param {string} username - * @param {string} password - */ - setAuth(path, username, password) { + setAuth(path: string, username: string, password: string) { this.debugServer(`set auth for ${path} to ${username}:${password}`); - this._auths.set(path, {username, password}); + this._auths.set(path, { username, password }); } - enableGzip(path) { + enableGzip(path: string) { this._gzipRoutes.add(path); } - /** - * @param {string} path - * @param {string} csp - */ - setCSP(path, csp) { + setCSP(path: string, csp: string) { this._csp.set(path, csp); } - /** - * @param {string} path - * @param {Object} object - */ - setExtraHeaders(path, object) { + setExtraHeaders(path: string, object: Record) { this._extraHeaders.set(path, object); } @@ -172,31 +143,19 @@ class TestServer { await new Promise(x => this._server.close(x)); } - /** - * @param {string} path - * @param {function(!http.IncomingMessage,http.ServerResponse)} handler - */ - setRoute(path, handler) { + setRoute(path: string, handler: (arg0: http.IncomingMessage & { postBody: Promise }, arg1: http.ServerResponse) => any) { this._routes.set(path, handler); } - /** - * @param {string} from - * @param {string} to - */ - setRedirect(from, to) { + setRedirect(from: string, to: string) { this.setRoute(from, (req, res) => { - let headers = this._extraHeaders.get(req.url) || {}; + const headers = this._extraHeaders.get(req.url!) || {}; res.writeHead(302, { ...headers, location: to }); res.end(); }); } - /** - * @param {string} path - * @return {!Promise} - */ - waitForRequest(path) { + waitForRequest(path: string): Promise }> { let promise = this._requestSubscribers.get(path); if (promise) return promise; @@ -223,28 +182,24 @@ class TestServer { this._requestSubscribers.clear(); } - /** - * @param {http.IncomingMessage} request - * @param {http.ServerResponse} response - */ - _onRequest(request, response) { + _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { request.on('error', error => { - if (error.code === 'ECONNRESET') + if ((error as any).code === 'ECONNRESET') response.end(); else throw error; }); - request.postBody = new Promise(resolve => { - const chunks = []; + (request as any).postBody = new Promise(resolve => { + const chunks: Buffer[] = []; request.on('data', chunk => { chunks.push(chunk); }); request.on('end', () => resolve(Buffer.concat(chunks))); }); - const path = url.parse(request.url).path; + const path = url.parse(request.url!).path; this.debugServer(`request ${request.method} ${path}`); if (this._auths.has(path)) { - const auth = this._auths.get(path); + const auth = this._auths.get(path)!; const credentials = Buffer.from((request.headers.authorization || '').split(' ')[1] || '', 'base64').toString(); this.debugServer(`request credentials ${credentials}`); this.debugServer(`actual credentials ${auth.username}:${auth.password}`); @@ -257,24 +212,18 @@ class TestServer { } // Notify request subscriber. if (this._requestSubscribers.has(path)) { - this._requestSubscribers.get(path)[fulfillSymbol].call(null, request); + this._requestSubscribers.get(path)![fulfillSymbol].call(null, request); this._requestSubscribers.delete(path); } const handler = this._routes.get(path); - if (handler) { + if (handler) handler.call(null, request, response); - } else { + else this.serveFile(request, response); - } } - /** - * @param {!http.IncomingMessage} request - * @param {!http.ServerResponse} response - * @param {string|undefined} filePath - */ - async serveFile(request, response, filePath) { - let pathName = url.parse(request.url).path; + async serveFile(request: http.IncomingMessage, response: http.ServerResponse, filePath?: string) { + let pathName = url.parse(request.url!).path; if (!filePath) { if (pathName === '/') pathName = '/index.html'; @@ -293,7 +242,7 @@ class TestServer { response.setHeader('Cache-Control', 'no-cache, no-store'); } if (this._csp.has(pathName)) - response.setHeader('Content-Security-Policy', this._csp.get(pathName)); + response.setHeader('Content-Security-Policy', this._csp.get(pathName)!); if (this._extraHeaders.has(pathName)) { const object = this._extraHeaders.get(pathName); @@ -301,7 +250,7 @@ class TestServer { response.setHeader(key, object[key]); } - const {err, data} = await fs.promises.readFile(filePath).then(data => ({data})).catch(err => ({err})); + const { err, data } = await fs.promises.readFile(filePath).then(data => ({ data, err: undefined })).catch(err => ({ data: undefined, err })); // The HTTP transaction might be already terminated after async hop here - do nothing in this case. if (response.writableEnded) return; @@ -332,7 +281,7 @@ class TestServer { } waitForWebSocketConnectionRequest() { - return new Promise(fullfil => { + return new Promise(fullfil => { this._wsServer.once('connection', (ws, req) => fullfil(req)); }); } @@ -341,5 +290,3 @@ class TestServer { this.onceWebSocketConnection(ws => ws.send(data)); } } - -module.exports = {TestServer};