diff --git a/README.md b/README.md index f418b3b..b570987 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,21 @@ import { isPortAvailable } from '@rstackjs/test-utils'; const available = await isPortAvailable(30000); ``` +### occupyPort + +Occupies an available TCP port until `close()` is called. The host defaults to `0.0.0.0`. + +```ts +import { occupyPort } from '@rstackjs/test-utils'; + +const { port, close } = await occupyPort(); +try { + // Test behavior when this port is occupied. +} finally { + await close(); +} +``` + ### proxyConsole Captures console output and removes ANSI control characters. By default, it captures `log`, `warn`, `info`, and `error`. diff --git a/src/index.ts b/src/index.ts index b9bddf9..9d43b6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,6 +10,7 @@ export { getDistFiles } from './getDistFiles.js'; export { getFileContent } from './getFileContent.js'; export { getRandomPort, isPortAvailable } from './getRandomPort.js'; export { normalizeEol } from './normalizeEol.js'; +export { occupyPort } from './occupyPort.js'; export { prepareDist } from './prepareDist.js'; export { type ConsoleType, diff --git a/src/occupyPort.ts b/src/occupyPort.ts new file mode 100644 index 0000000..59e4b1b --- /dev/null +++ b/src/occupyPort.ts @@ -0,0 +1,24 @@ +import { once } from 'node:events'; +import net from 'node:net'; + +/** Occupy an available TCP port until close() is called. */ +export const occupyPort = async (host = '0.0.0.0') => { + const server = net.createServer().listen({ port: 0, host }); + await once(server, 'listening'); + + const { port } = server.address() as net.AddressInfo; + + return { + port, + close: () => + new Promise((resolve, reject) => { + server.close((error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }), + }; +}; diff --git a/tests/occupyPort.test.ts b/tests/occupyPort.test.ts new file mode 100644 index 0000000..7590efd --- /dev/null +++ b/tests/occupyPort.test.ts @@ -0,0 +1,26 @@ +import { once } from 'node:events'; +import net from 'node:net'; +import { expect, test } from 'rstack/test'; +import { occupyPort } from '../src/index'; + +test('should occupy a port and release it when closed', async () => { + const { port, close } = await occupyPort(); + const server = net.createServer(); + const options = { port, host: '0.0.0.0' }; + + try { + try { + server.listen(options); + await expect(once(server, 'listening')).rejects.toMatchObject({ + code: 'EADDRINUSE', + }); + } finally { + await close(); + } + + server.listen(options); + await once(server, 'listening'); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } +});