Skip to content

Commit d18d2e2

Browse files
committed
test(vite): open a real wss handshake on the https hub server
1 parent c4b6aef commit d18d2e2

1 file changed

Lines changed: 93 additions & 24 deletions

File tree

‎packages/vite/test/hub.test.ts‎

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,42 @@
1-
import type { IncomingMessage, Server as NodeHttpServer, ServerResponse } from 'node:http'
1+
import type { IncomingMessage, ServerResponse } from 'node:http'
22
import type { Http2SecureServer } from 'node:http2'
3+
import type { Socket } from 'node:net'
34
import type { ViteDevServer } from 'vite'
4-
import { mkdtempSync } from 'node:fs'
5-
import { createServer } from 'node:http'
5+
import { execFileSync } from 'node:child_process'
6+
import { mkdtempSync, readFileSync } from 'node:fs'
67
import { createSecureServer } from 'node:http2'
8+
import { request } from 'node:https'
79
import { tmpdir } from 'node:os'
810
import { join } from 'node:path'
9-
import { getPort } from 'get-port-please'
10-
import { afterEach, describe, expect, it } from 'vitest'
11+
import { afterEach, beforeAll, describe, expect, it } from 'vitest'
12+
import { WebSocket } from 'ws'
1113
import { viteDevframeHub } from '../src/hub'
1214

1315
type ConnectMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void
1416

17+
function hasOpenssl(): boolean {
18+
try {
19+
execFileSync('openssl', ['version'], { stdio: 'ignore' })
20+
return true
21+
}
22+
catch {
23+
return false
24+
}
25+
}
26+
27+
const opensslAvailable = hasOpenssl()
28+
if (!opensslAvailable)
29+
console.warn('[vite hub test] openssl not found, skipping the https dev server test')
30+
1531
/**
16-
* Vite on `server.https` hands plugins an `Http2SecureServer` (with
17-
* `allowHTTP1`), which is not a `node:http` `Server`. Requests are served
18-
* here over a plain HTTP server running the same middleware stack, so the
19-
* test needs no certificate.
32+
* Vite on `server.https` hands plugins an `Http2SecureServer` with
33+
* `allowHTTP1`, which is not a `node:http` `Server`. This one runs the
34+
* plugin's connect middlewares like Vite does.
2035
*/
21-
function fakeHttpsViteServer() {
36+
function fakeHttpsViteServer(tls: { key: string, cert: string }) {
2237
const stack: ConnectMiddleware[] = []
23-
const requestServer: NodeHttpServer = createServer((req, res) => {
38+
const httpServer: Http2SecureServer = createSecureServer({ ...tls, allowHTTP1: true })
39+
httpServer.on('request', (req: IncomingMessage, res: ServerResponse) => {
2440
let i = 0
2541
const next = (): void => {
2642
const handler = stack[i++]
@@ -33,46 +49,99 @@ function fakeHttpsViteServer() {
3349
}
3450
next()
3551
})
36-
const httpServer: Http2SecureServer = createSecureServer({ allowHTTP1: true })
52+
const sockets = new Set<Socket>()
53+
httpServer.on('secureConnection', (socket: Socket) => {
54+
sockets.add(socket)
55+
socket.once('close', () => sockets.delete(socket))
56+
})
3757
const server = {
3858
httpServer,
3959
resolvedUrls: null,
4060
middlewares: { use: (handler: ConnectMiddleware) => stack.push(handler) },
4161
}
42-
return { server, httpServer, requestServer }
62+
const close = async (): Promise<void> => {
63+
for (const socket of sockets)
64+
socket.destroy()
65+
await new Promise<void>(resolve => httpServer.close(() => resolve()))
66+
}
67+
return { server, httpServer, close }
68+
}
69+
70+
function getInsecure(url: string): Promise<{ status: number, body: string }> {
71+
return new Promise((resolve, reject) => {
72+
request(url, { rejectUnauthorized: false }, (res) => {
73+
let body = ''
74+
res.setEncoding('utf8')
75+
res.on('data', (chunk: string) => body += chunk)
76+
res.on('end', () => resolve({ status: res.statusCode ?? 0, body }))
77+
}).on('error', reject).end()
78+
})
79+
}
80+
81+
function openWs(url: string): Promise<WebSocket> {
82+
return new Promise((resolve, reject) => {
83+
const ws = new WebSocket(url, { rejectUnauthorized: false })
84+
ws.once('open', () => resolve(ws))
85+
ws.once('error', reject)
86+
})
4387
}
4488

45-
describe('viteDevframeHub', () => {
89+
describe.skipIf(!opensslAvailable)('viteDevframeHub', () => {
90+
let tls: { key: string, cert: string }
4691
let cleanup: (() => Promise<void>) | undefined
4792

93+
beforeAll(() => {
94+
const dir = mkdtempSync(join(tmpdir(), 'devframe-vite-hub-tls-'))
95+
const keyPath = join(dir, 'key.pem')
96+
const certPath = join(dir, 'cert.pem')
97+
execFileSync('openssl', [
98+
'req',
99+
'-x509',
100+
'-newkey',
101+
'rsa:2048',
102+
'-nodes',
103+
'-keyout',
104+
keyPath,
105+
'-out',
106+
certPath,
107+
'-days',
108+
'1',
109+
'-subj',
110+
'/CN=localhost',
111+
], { stdio: 'ignore' })
112+
tls = { key: readFileSync(keyPath, 'utf8'), cert: readFileSync(certPath, 'utf8') }
113+
})
114+
48115
afterEach(async () => {
49116
await cleanup?.()
50117
cleanup = undefined
51118
})
52119

53120
it('shares an https (http2) dev server for the WebSocket upgrade', async () => {
54121
const host = '127.0.0.1'
55-
const port = await getPort({ port: 19800, host })
56-
const { server, httpServer, requestServer } = fakeHttpsViteServer()
57-
await new Promise<void>(resolve => requestServer.listen(port, host, resolve))
122+
const { server, httpServer, close } = fakeHttpsViteServer(tls)
123+
await new Promise<void>(resolve => httpServer.listen(0, host, resolve))
124+
const { port } = httpServer.address() as { port: number }
58125

59126
const plugin = viteDevframeHub({
60127
ui: false,
61128
auth: false,
62129
quiet: true,
63130
cwd: mkdtempSync(join(tmpdir(), 'devframe-vite-hub-')),
64131
})
132+
let ws: WebSocket | undefined
65133
cleanup = async () => {
66-
httpServer.emit('close')
134+
ws?.terminate()
67135
await (plugin.closeBundle as () => Promise<void>)()
68-
requestServer.close()
69-
requestServer.closeAllConnections()
136+
await close()
70137
}
71138
await (plugin.configureServer as (s: ViteDevServer) => Promise<void>)(server as any)
72139

73-
const res = await fetch(`http://${host}:${port}/__devframes/__connection.json`)
74-
const meta = await res.json() as { websocket?: unknown }
75-
expect(meta.websocket).toEqual({ path: '/__devframes/__ws' })
76-
expect(httpServer.listenerCount('upgrade')).toBe(1)
140+
const res = await getInsecure(`https://${host}:${port}/__devframes/__connection.json`)
141+
expect(res.status).toBe(200)
142+
expect((JSON.parse(res.body) as { websocket?: unknown }).websocket).toEqual({ path: '/__devframes/__ws' })
143+
144+
ws = await openWs(`wss://${host}:${port}/__devframes/__ws`)
145+
expect(ws.readyState).toBe(WebSocket.OPEN)
77146
})
78147
})

0 commit comments

Comments
 (0)