get-open-port
Version:
Utility to find and reserve an available network port.
159 lines (136 loc) • 4.65 kB
JavaScript
const net = require("net");
const {
getOpenPort,
getPortRange,
clearLockedPorts,
LockedPortError,
portManager,
} = require(".");
// Mocking necessary modules for testing
jest.mock("net");
jest.mock("os", () => ({
networkInterfaces: jest.fn(() => ({
eth0: [{ address: "192.168.1.1", internal: false }],
})),
}));
describe("Port Manager", () => {
let mockServer;
beforeEach(() => {
// Reset mock server for each test
mockServer = {
listen: jest.fn((options, callback) => {
setImmediate(() => callback?.());
return mockServer;
}),
close: jest.fn((callback) => {
setImmediate(() => callback?.());
return mockServer;
}),
unref: jest.fn(),
once: jest.fn(),
address: jest.fn(() => ({ port: 12345 })),
};
net.createServer.mockImplementation(() => mockServer);
});
afterEach(() => {
clearLockedPorts();
jest.clearAllMocks();
});
describe("getOpenPort", () => {
test("should return an available port within valid range", async () => {
const port = await getOpenPort();
expect(port).toBeGreaterThanOrEqual(1024);
expect(port).toBeLessThanOrEqual(65535);
expect(mockServer.listen).toHaveBeenCalled();
expect(mockServer.close).toHaveBeenCalled();
});
test("should honor specific port request when available", async () => {
const requestedPort = 12345;
const port = await getOpenPort({ port: requestedPort });
expect(port).toBe(requestedPort);
expect(mockServer.listen).toHaveBeenCalledWith(
expect.objectContaining({ port: requestedPort }),
expect.any(Function)
);
});
test("should throw LockedPortError if the port is locked", async () => {
const lockedPort = 8080;
// Mock the getOpenPort implementation for locked port scenario
mockServer.listen.mockImplementation((options, callback) => {
if (String(options.port) === String(lockedPort)) {
throw new LockedPortError(`Port ${lockedPort} is locked`);
}
callback?.();
return mockServer;
});
// Lock the port
portManager.locked.set(lockedPort, Date.now());
// Verify that attempting to get the locked port throws error
await expect(getOpenPort({ port: lockedPort })).rejects.toThrow(
LockedPortError
);
expect(mockServer.listen).not.toHaveBeenCalled();
});
test("should respect port exclusion list", async () => {
const excludedPorts = [8080, 8081];
const port = await getOpenPort({ exclude: excludedPorts });
expect(excludedPorts).not.toContain(port);
expect(mockServer.listen).toHaveBeenCalledWith(
expect.objectContaining({
port: expect.not.arrayContaining(excludedPorts),
}),
expect.any(Function)
);
});
test("should handle EADDRINUSE error", async () => {
mockServer.listen.mockImplementation((options, callback) => {
setImmediate(() => {
// Simulate the error event
mockServer.once.mock.calls.find((call) => call[0] === "error")?.[1](
Object.assign(new Error("EADDRINUSE"), { code: "EADDRINUSE" })
);
});
return mockServer;
});
await expect(getOpenPort()).rejects.toThrow("EADDRINUSE");
// Wait for the next tick to ensure the close method is called
await new Promise((resolve) => setImmediate(resolve));
// Verify that close was called after the error
expect(mockServer.close).toHaveBeenCalled();
});
test("should timeout if port check takes too long", async () => {
const timeout = 100;
mockServer.listen.mockImplementation(() => {
// Never call the callback to simulate hanging
return mockServer;
});
await expect(getOpenPort({ timeout })).rejects.toThrow(
`Port check timed out after ${timeout}ms`
);
});
});
describe("getPortRange", () => {
test("should return correct port range array", () => {
const range = getPortRange(1024, 1029);
expect(range).toEqual([1024, 1025, 1026, 1027, 1028, 1029]);
});
test.each([
[1029, 1024, "`to` must be greater than or equal to `from`"],
[70000, 80000, "`from` must be between 1024 and 65535"],
["a", 80000, "`from` and `to` must be integer numbers"],
[1024, "b", "`from` and `to` must be integer numbers"],
[1024.5, 1025, "`from` and `to` must be integer numbers"],
])("should validate range inputs (%s, %s)", (from, to, expectedError) => {
expect(() => getPortRange(from, to)).toThrow(expectedError);
});
});
describe("clearLockedPorts", () => {
test("should clear all locked ports", () => {
portManager.locked.set(8080, Date.now());
portManager.locked.set(8081, Date.now());
expect(portManager.locked.size).toBe(2);
clearLockedPorts();
expect(portManager.locked.size).toBe(0);
});
});
});