mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
test(bun/net): add TCP socket tests (#17520)
This commit is contained in:
232
test/js/bun/net/tcp.spec.ts
Normal file
232
test/js/bun/net/tcp.spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { Socket, SocketHandler, type TCPSocketListener } from "bun";
|
||||
import { jest, describe, beforeAll, afterAll, beforeEach, afterEach, it, expect } from "bun:test";
|
||||
|
||||
const createMockHandler = <Data = undefined>() =>
|
||||
({
|
||||
close: jest.fn(),
|
||||
connectError: jest.fn(),
|
||||
data: jest.fn(),
|
||||
drain: jest.fn(),
|
||||
end: jest.fn(),
|
||||
error: jest.fn(),
|
||||
handshake: jest.fn(),
|
||||
open: jest.fn(),
|
||||
timeout: jest.fn(),
|
||||
}) satisfies SocketHandler<Data>;
|
||||
|
||||
const clearMockHandler = <Data = undefined>(handler: SocketHandler<Data>) => {
|
||||
for (const key in handler) {
|
||||
if ("mockClear" in handler[key]) handler[key].mockClear();
|
||||
}
|
||||
};
|
||||
|
||||
const nextEventLoopTick = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
describe("Bun.listen(options)", () => {
|
||||
describe("socket options", () => {
|
||||
// @ts-expect-error
|
||||
it("must be provided", () => expect(() => Bun.listen()).toThrow(TypeError));
|
||||
it.each([undefined, null, 1, true, function foo() {}, "foo", Symbol.for("hi")])(
|
||||
"must be an object (%p)",
|
||||
// @ts-expect-error
|
||||
badOptions => expect(() => Bun.listen(badOptions)).toThrow(TypeError),
|
||||
);
|
||||
}); // </socket options>
|
||||
|
||||
describe("When called with valid options", () => {
|
||||
let listener: TCPSocketListener;
|
||||
|
||||
beforeAll(() => {
|
||||
listener = Bun.listen({
|
||||
port: 6543,
|
||||
hostname: "localhost",
|
||||
socket: {
|
||||
data(socket, data) {},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
expect(listener).toBeDefined();
|
||||
listener.stop();
|
||||
listener = undefined as any;
|
||||
});
|
||||
|
||||
it("returns an object", () => expect(listener).toBeTypeOf("object"));
|
||||
|
||||
// FIXME: this is overriding properties of `listener`
|
||||
it.skip("listener has the expected methods", () => {
|
||||
expect(listener).toMatchObject({
|
||||
stop: expect.any(Function),
|
||||
ref: expect.any(Function),
|
||||
unref: expect.any(Function),
|
||||
reload: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it("is listening on localhost:6543", () => {
|
||||
expect(listener.port).toBe(6543);
|
||||
expect(listener.hostname).toBe("localhost");
|
||||
});
|
||||
|
||||
it("does not have .data", () => {
|
||||
expect(listener).toHaveProperty("data");
|
||||
expect(listener.data).toBeUndefined();
|
||||
});
|
||||
}); // </When called with valid options>
|
||||
}); // </Bun.listen(options)>
|
||||
|
||||
describe("Given a TCP server listening on port 1234", () => {
|
||||
let listener: TCPSocketListener;
|
||||
const serverHandler = createMockHandler();
|
||||
|
||||
// FIXME: switching this to `beforeAll` then using `listener.reload() in
|
||||
// `beforeEach` causes a segfault.
|
||||
beforeEach(() => {
|
||||
listener = Bun.listen({
|
||||
hostname: "localhost",
|
||||
port: 0,
|
||||
socket: serverHandler,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
listener.stop(true);
|
||||
clearMockHandler(serverHandler);
|
||||
// listener.reload({ socket: serverHandler });
|
||||
});
|
||||
|
||||
describe("When a client connects and waits 1 event loop cycle", () => {
|
||||
let client: Socket;
|
||||
const events = {
|
||||
client: [] as string[],
|
||||
server: [] as string[],
|
||||
};
|
||||
const clientHandler = createMockHandler();
|
||||
const getClient = (port: number) =>
|
||||
Bun.connect({
|
||||
hostname: "localhost",
|
||||
port,
|
||||
socket: clientHandler,
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
client = await getClient(listener.port);
|
||||
|
||||
for (const event of Object.keys(clientHandler)) {
|
||||
if (typeof clientHandler[event] === "function") {
|
||||
clientHandler[event].mockImplementation(() => events.client.push(event));
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of Object.keys(serverHandler)) {
|
||||
if (typeof serverHandler[event] === "function") {
|
||||
serverHandler[event].mockImplementation(() => events.server.push(event));
|
||||
}
|
||||
}
|
||||
|
||||
await nextEventLoopTick();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
client.end();
|
||||
events.client.length = 0;
|
||||
events.server.length = 0;
|
||||
clearMockHandler(clientHandler);
|
||||
});
|
||||
|
||||
// FIXME: readyState is 1.
|
||||
it.skip("the client enters 'open' state", () => {
|
||||
expect(client.readyState).toBe("open");
|
||||
});
|
||||
|
||||
it("client.open() gets called", () => expect(clientHandler.open).toHaveBeenCalledTimes(1));
|
||||
it("server.open() gets called", () => expect(serverHandler.open).toHaveBeenCalledTimes(1));
|
||||
|
||||
it.each(["handshake", "close", "error", "end"])(
|
||||
"neither client nor server's %s handler is called",
|
||||
async handler => {
|
||||
expect(clientHandler[handler]).not.toHaveBeenCalled();
|
||||
expect(serverHandler[handler]).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("has sent no data", () => expect(client.bytesWritten).toBe(0));
|
||||
|
||||
it("when the client sends data, the server's data handler gets called after data are flushed", async () => {
|
||||
const bytesWritten = client.write("hello");
|
||||
expect(bytesWritten).toBe(5);
|
||||
expect(serverHandler.data).not.toHaveBeenCalled();
|
||||
client.flush();
|
||||
expect(serverHandler.data).not.toHaveBeenCalled();
|
||||
await nextEventLoopTick();
|
||||
expect(serverHandler.data).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
// FIXME: three bugs:
|
||||
// 1&2. client/server handshake callbacks are not called
|
||||
// 3. un-commenting this and moving Bun.listen into `beforeAll` causes the
|
||||
// `expect(server.end).toHaveBeenCalledTimes(1)` in the neighboring test
|
||||
// to fail (it gets called twice)
|
||||
//
|
||||
describe.skip("on the next event loop cycle", () => {
|
||||
beforeEach(nextEventLoopTick);
|
||||
it("server.handshake() gets called", async () => {
|
||||
expect(serverHandler.handshake).toHaveBeenCalled();
|
||||
});
|
||||
it("client.handshake() gets called", async () => {
|
||||
expect(clientHandler.handshake).toHaveBeenCalled();
|
||||
});
|
||||
}); // </on the next event loop cycle>
|
||||
|
||||
describe("When the client disconnects", () => {
|
||||
beforeEach(() => {
|
||||
client.end();
|
||||
});
|
||||
|
||||
// FIXME: readyState is -1.
|
||||
it.skip("client enters 'closing' state", () => {
|
||||
expect(client.readyState).toBe("closing");
|
||||
});
|
||||
|
||||
describe("On the next event loop cycle", () => {
|
||||
beforeEach(nextEventLoopTick);
|
||||
|
||||
it("the server's end handler fires", () => {
|
||||
expect(serverHandler.end).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("the server's close handler fires after end", () => {
|
||||
expect(serverHandler.close).toHaveBeenCalledTimes(1);
|
||||
const endIndex = events.server.indexOf("end");
|
||||
const closeIndex = events.server.indexOf("close");
|
||||
expect(closeIndex).toBeGreaterThan(endIndex);
|
||||
});
|
||||
|
||||
it("no client errors occur", () => {
|
||||
expect(clientHandler.error).not.toHaveBeenCalled();
|
||||
expect(clientHandler.connectError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("no server errors occur", () => {
|
||||
expect(serverHandler.error).not.toHaveBeenCalled();
|
||||
expect(serverHandler.connectError).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("can no longer send data", () => {
|
||||
expect(client.write("hello")).toBeLessThan(0);
|
||||
});
|
||||
|
||||
// FIXME: readyState is detached (-1)
|
||||
it.skip("client is closed", () => {
|
||||
expect(client.readyState).toBe("closed");
|
||||
});
|
||||
|
||||
// FIXME: readyState is -1.
|
||||
it.skip("calling client.end() twice does nothing", () => {
|
||||
client.end();
|
||||
expect(client.readyState).toBe("closed");
|
||||
});
|
||||
});
|
||||
});
|
||||
}); // </When a client connects>
|
||||
}); // </Given a TCP socket listening on port 1234>
|
||||
186
test/js/bun/net/tcp.test.ts
Normal file
186
test/js/bun/net/tcp.test.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { Socket, SocketHandler, TCPSocketConnectOptions, TCPSocketListener } from "bun";
|
||||
import type { Mock } from "bun:test";
|
||||
import { jest, afterEach, test, expect } from "bun:test";
|
||||
import { isLinux } from "harness";
|
||||
import jsc from "bun:jsc";
|
||||
|
||||
const handlerNames: Set<keyof SocketHandler<any>> = new Set([
|
||||
"open",
|
||||
"handshake",
|
||||
"close",
|
||||
"error",
|
||||
"end",
|
||||
"data",
|
||||
"drain",
|
||||
]);
|
||||
|
||||
class MockSocket<Data = void> implements SocketHandler<Data> {
|
||||
open = undefined as any;
|
||||
handshake = undefined as any;
|
||||
close = undefined as any;
|
||||
error = undefined as any;
|
||||
end = undefined as any;
|
||||
data = undefined as any;
|
||||
|
||||
events: string[];
|
||||
|
||||
constructor(impls: Partial<SocketHandler<Data>> = {}) {
|
||||
this.events = [];
|
||||
for (const method of handlerNames) {
|
||||
const impl = impls[method] ? impls[method].bind(this) : () => this.events.push(method);
|
||||
this[method] = jest.fn(impl as any);
|
||||
}
|
||||
}
|
||||
|
||||
public mockClear() {
|
||||
for (const method of Object.keys(this)) {
|
||||
if ("mockClear" in this[method]) {
|
||||
(this[method] as Mock<any>).mockClear();
|
||||
}
|
||||
}
|
||||
this.events.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const nextEventLoopCycle = () => new Promise(resolve => setTimeout(resolve, 0));
|
||||
const nextTick = () => new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
interface ClientState<Data> extends Disposable {
|
||||
socket: Socket<Data>;
|
||||
handler: MockSocket<Data>;
|
||||
}
|
||||
|
||||
const isServer = (value: unknown): value is TCPSocketListener<unknown> =>
|
||||
!!value && typeof value === "object" && "stop" in value && "ref" in value && "reload" in value;
|
||||
|
||||
function makeClient<Data = unknown>(
|
||||
handlerOverrides?: Partial<SocketHandler<Data>>,
|
||||
options?: TCPSocketConnectOptions<Data>,
|
||||
): Promise<ClientState<Data>>;
|
||||
function makeClient<Data = unknown>(server: TCPSocketListener<Data>): Promise<ClientState<Data>>;
|
||||
async function makeClient<Data = unknown>(
|
||||
serverOrHandlerOverrides?: Partial<SocketHandler<Data>>,
|
||||
options: Partial<TCPSocketConnectOptions<Data>> = {},
|
||||
) {
|
||||
let handlerOverrides: Partial<SocketHandler<Data>> | undefined;
|
||||
if (isServer(serverOrHandlerOverrides)) {
|
||||
options.port = serverOrHandlerOverrides.port;
|
||||
options.hostname = serverOrHandlerOverrides.hostname;
|
||||
} else {
|
||||
handlerOverrides = serverOrHandlerOverrides;
|
||||
if (options.port == null) throw new Error("port is required");
|
||||
}
|
||||
const handler = new MockSocket<Data>(handlerOverrides);
|
||||
const socket = await Bun.connect({
|
||||
hostname: "localhost",
|
||||
// port: 0,
|
||||
...options,
|
||||
socket: handler,
|
||||
} as any);
|
||||
|
||||
return {
|
||||
socket,
|
||||
handler,
|
||||
[Symbol.dispose]() {
|
||||
socket[Symbol.dispose]();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const makeServer = <Data = unknown>(handlerOverrides?: Partial<SocketHandler<Data>>, options?) => {
|
||||
const handler = new MockSocket<Data>(handlerOverrides);
|
||||
const socket: TCPSocketListener<Data> = Bun.listen({
|
||||
hostname: "localhost",
|
||||
port: 0,
|
||||
...options,
|
||||
socket: handler,
|
||||
});
|
||||
return {
|
||||
socket,
|
||||
handler,
|
||||
[Symbol.dispose]() {
|
||||
socket[Symbol.dispose]();
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await nextEventLoopCycle();
|
||||
jsc.drainMicrotasks();
|
||||
});
|
||||
|
||||
test("open() event timing", async () => {
|
||||
const socket = { server: new MockSocket(), client: new MockSocket() };
|
||||
|
||||
using server = Bun.listen({
|
||||
port: 0,
|
||||
hostname: "localhost",
|
||||
socket: socket.server,
|
||||
});
|
||||
|
||||
// just starting a server doesn't trigger any events
|
||||
await nextEventLoopCycle();
|
||||
expect(socket.server.events).toBeEmpty();
|
||||
expect(socket.client.events).toBeEmpty();
|
||||
|
||||
const clientPromise = Bun.connect({
|
||||
port: server.port,
|
||||
hostname: "localhost",
|
||||
socket: socket.client,
|
||||
});
|
||||
expect(socket.client.open).not.toHaveBeenCalled();
|
||||
await nextTick();
|
||||
expect(socket.client.open).not.toHaveBeenCalled();
|
||||
|
||||
// Promise resolves when client connects. Server's open only fires when
|
||||
// event loop polls for events again and finds a connection event on the
|
||||
// server socket
|
||||
using _client = await clientPromise;
|
||||
expect(socket.client.open).toHaveBeenCalled();
|
||||
// FIXME: server's open handler is called on linux, but not on macOS or windows.
|
||||
if (!isLinux) expect(socket.server.open).not.toHaveBeenCalled();
|
||||
|
||||
// next tick loop gets drained before event loop polls again. This check makes
|
||||
// sure that open(), indeed, only fires in the next event loop cycle
|
||||
await nextTick();
|
||||
// FIXME: server's open handler is called on linux, but not on macOS or windows.
|
||||
if (!isLinux) expect(socket.server.open).not.toHaveBeenCalled();
|
||||
|
||||
await nextEventLoopCycle();
|
||||
expect(socket.server.open).toHaveBeenCalled();
|
||||
expect(socket.client.open).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/**
|
||||
* Client sends FIN, so server emits `end` event. Client still reads data before
|
||||
* closing.
|
||||
*/
|
||||
test("client writes then closes the socket", async () => {
|
||||
using server = makeServer({
|
||||
data(_socket, data) {
|
||||
expect(data.toString("utf8")).toBe("hello");
|
||||
this.events.push("data");
|
||||
},
|
||||
});
|
||||
using client = await makeClient(server.socket);
|
||||
|
||||
server.socket;
|
||||
client.socket.end("hello");
|
||||
await nextEventLoopCycle();
|
||||
expect(server.handler.events).toEqual(["open", "data", "end", "close"]);
|
||||
expect(client.handler.events).toEqual(["open", "close"]);
|
||||
});
|
||||
|
||||
test.skip("client writes while server closes in the same tick", async () => {
|
||||
using server = makeServer({
|
||||
open(socket) {
|
||||
socket.write("hello");
|
||||
socket.end();
|
||||
this.events.push("open");
|
||||
},
|
||||
});
|
||||
using client = await makeClient(server.socket);
|
||||
await nextEventLoopCycle();
|
||||
expect(server.handler.events).toEqual(["open", "close"]);
|
||||
expect(client.handler.events).toEqual(["open", "data", "end", "close"]);
|
||||
});
|
||||
Reference in New Issue
Block a user