const { FastCGIHandler, constants, buildFastCGIPacket, buildNameValuePair, writeFastCGIPacket } = require("../../src/utils/fastCgi.js"); const net = require("net"); const EventEmitter = require("events"); jest.mock("net"); describe("FastCGI module", () => { let mockSocket; beforeEach(() => { mockSocket = { write: jest.fn(), on: jest.fn().mockImplementation((event, handler) => { if (event === "data") { mockSocket.dataHandler = handler; } return mockSocket; // Enable method chaining }), removeListener: jest.fn().mockImplementation((event, handler) => { if (event === "data" && mockSocket.dataHandler == handler) { mockSocket.dataHandler = () => {}; } return mockSocket; // Enable method chaining }), dataHandler: () => {} }; net.createConnection.mockReturnValue(mockSocket); }); afterEach(() => { jest.clearAllMocks(); }); test("should create a socket and connect", () => { const handler = new FastCGIHandler({ host: "localhost", port: 9000 }); expect(net.createConnection).toHaveBeenCalledWith( { host: "localhost", port: 9000 }, expect.any(Function) ); expect(mockSocket.on).toHaveBeenCalledWith("error", expect.any(Function)); expect(mockSocket.on).toHaveBeenCalledWith("data", expect.any(Function)); expect(handler).toBeInstanceOf(EventEmitter); }); test("should build a FastCGI packet", () => { const content = Buffer.from("test"); const packet = buildFastCGIPacket(1, 1, content); expect(packet.length).toBe(16); // Header + padded content expect(packet.readUInt8(0)).toBe(1); // version expect(packet.readUInt8(1)).toBe(1); // type expect(packet.readUInt16BE(2)).toBe(1); // requestID expect(packet.readUInt16BE(4)).toBe(4); // content length expect(packet.readUInt8(6)).toBe(4); // padding length }); test("should write FastCGI packets to the socket", () => { const handler = new FastCGIHandler({}); const content = Buffer.from("test"); writeFastCGIPacket(handler.socket, 1, handler.requestID, content); expect(mockSocket.write).toHaveBeenCalledTimes(1); expect(mockSocket.write).toHaveBeenCalledWith(expect.any(Buffer)); }); // eslint-disable-next-line jest/no-done-callback test("should handle socket data and emit events", (done) => { const handler = new FastCGIHandler({}); handler.stdout.on("data", (chunk) => { expect(chunk).toStrictEqual(Buffer.from("Hello World")); done(); }); // Simulate a FastCGI STDOUT packet const packet = buildFastCGIPacket( constants.STDOUT, handler.requestID, Buffer.from("Hello World") ); mockSocket.dataHandler(packet); }); // eslint-disable-next-line jest/no-done-callback test("should handle END_REQUEST and emit 'exit' event", (done) => { const handler = new FastCGIHandler({}); handler.on("exit", (code, signal) => { expect(code).toBe(0); expect(signal).toBeNull(); done(); }); // Simulate an END_REQUEST packet const content = Buffer.alloc(8); content.writeUInt32BE(0, 0); // appStatus content.writeUInt8(0, 4); // protocolStatus const packet = buildFastCGIPacket( constants.END_REQUEST, handler.requestID, content ); mockSocket.dataHandler(packet); }); test("should handle UNKNOWN_TYPE and discard it", () => { const handler = new FastCGIHandler({}); const dataListener = jest.fn(); handler.stdout.on("data", dataListener); // Simulate an UNKNOWN_TYPE packet const packet = buildFastCGIPacket( constants.UNKNOWN_TYPE, handler.requestID, Buffer.alloc(0) ); mockSocket.dataHandler(packet); expect(dataListener).not.toHaveBeenCalledWith(); }); test("should build name-value pairs correctly", () => { const pair = buildNameValuePair("key", "value"); expect(pair.length).toBe(10); // 1 byte for key length, 1 for value length, and key-value content expect(pair.toString("utf8", 2)).toBe("keyvalue"); }); test("should send environment variables on init", () => { const env = { FOO: "BAR", BAZ: "QUX" }; const handler = new FastCGIHandler({ env }); handler.init(); expect(mockSocket.write).toHaveBeenCalled(); const calls = mockSocket.write.mock.calls; const envPacket = calls.find(([arg]) => arg.includes(Buffer.from("FOO"))); expect(envPacket).toBeTruthy(); }); });