@epic-web/test-server
Version:
Utility for creating HTTP and WebSocket servers for testing
206 lines (203 loc) • 6.18 kB
JavaScript
import fs from 'node:fs';
import https from 'node:https';
import EventEmitter from 'node:events';
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { serve } from '@hono/node-server';
import { DeferredPromise } from '@open-draft/deferred-promise';
// src/http.ts
var DEFAULT_PROTOCOLS = ["http"];
var BASE_URL = import.meta.url;
var SSL_CERT_PATH = new URL("../cert.pem", BASE_URL);
var SSL_KEY_PATH = new URL("../key.pem", BASE_URL);
var kApp = Symbol("kApp");
var kServer = Symbol("kServer");
var kServers = Symbol("kServers");
var kEmitter = Symbol("kEmitter");
async function createTestHttpServer(options) {
const protocols = Array.from(
new Set(
typeof options === "object" && Array.isArray(options.protocols) ? options.protocols : DEFAULT_PROTOCOLS
)
);
const sockets = /* @__PURE__ */ new Set();
const emitter = new EventEmitter();
const rootRouter = new Hono();
rootRouter.get("/", () => {
return new Response("Test server is listening");
});
if (typeof options === "object" && typeof options.defineRoutes === "function") {
options.defineRoutes(rootRouter);
}
rootRouter.use(cors());
const app = new Hono(rootRouter);
const serveOptions = {
fetch: app.fetch,
hostname: (options == null ? void 0 : options.hostname) || "127.0.0.1",
port: 0
};
const servers = /* @__PURE__ */ new Map();
const serverInits = new Map(
protocols.map((protocol) => {
switch (protocol) {
case "http": {
return ["http", serveOptions];
}
case "https": {
return [
"https",
{
...serveOptions,
createServer: https.createServer,
serverOptions: {
key: fs.readFileSync(SSL_KEY_PATH),
cert: fs.readFileSync(SSL_CERT_PATH)
}
}
];
}
default: {
throw new Error(`Unsupported server protocol "${protocol}"`);
}
}
})
);
const abortAllConnections = async () => {
const pendingAborts = [];
for (const socket of sockets) {
pendingAborts.push(abortConnection(socket));
}
return Promise.all(pendingAborts);
};
const listen = async () => {
const pendingListens = [];
for (const [protocol, serveOptions2] of serverInits) {
pendingListens.push(
startHttpServer(serveOptions2).then((server) => {
subscribeToConnections(server, sockets);
servers.set(protocol, server);
emitter.emit("listen", server);
})
);
}
await Promise.all(pendingListens);
};
const api = {
async [Symbol.asyncDispose]() {
await this.close();
},
async close() {
await abortAllConnections();
const pendingClosures = [];
for (const [, server] of servers) {
pendingClosures.push(closeHttpServer(server));
}
await Promise.all(pendingClosures);
servers.clear();
},
get http() {
const server = servers.get("http");
if (server == null) {
throw new Error(
'HTTP server is not defined. Did you forget to include "http" in the "protocols" option?'
);
}
return buildServerApi("http", server);
},
get https() {
const server = servers.get("https");
if (server == null) {
throw new Error(
'HTTPS server is not defined. Did you forget to include "https" in the "protocols" option?'
);
}
return buildServerApi("https", server);
}
};
Object.defineProperty(api, kEmitter, { value: emitter });
Object.defineProperty(api, kApp, { value: app });
Object.defineProperty(api, kServers, { value: servers });
await listen();
return api;
}
async function startHttpServer(options) {
const listenPromise = new DeferredPromise();
const server = serve(options, () => {
listenPromise.resolve(server);
});
server.once("error", (error) => {
console.error(error);
listenPromise.reject(error);
});
return listenPromise;
}
async function closeHttpServer(server) {
if (!server.listening) {
return Promise.resolve();
}
const closePromise = new DeferredPromise();
server.close((error) => {
if (error) {
closePromise.reject(error);
}
closePromise.resolve();
});
return closePromise.then(() => {
server.unref();
});
}
function subscribeToConnections(server, sockets) {
server.on("connection", (socket) => {
sockets.add(socket);
socket.once("close", () => {
sockets.delete(socket);
});
});
}
async function abortConnection(socket) {
if (socket.destroyed) {
return Promise.resolve();
}
const abortPromise = new DeferredPromise();
socket.destroy();
socket.on("close", () => abortPromise.resolve()).once("error", (error) => abortPromise.reject(error));
return abortPromise;
}
function createUrlBuilder(baseUrl, forceRelativePathname) {
return (pathname = "/") => {
return new URL(
forceRelativePathname ? toRelativePathname(pathname) : pathname,
baseUrl
);
};
}
function toRelativePathname(pathname) {
return !pathname.startsWith(".") ? "." + pathname : pathname;
}
function getServerUrl(protocol, server) {
let url;
const address = server.address();
if (address == null) {
throw new Error("Failed to get server URL: server.address() returned null");
}
if (typeof address === "string") {
url = new URL(address);
} else {
const hostname = address.address.includes(":") && !address.address.startsWith("[") && !address.address.endsWith("]") ? `[${address.address}]` : address.address;
url = new URL(`http://${hostname}`);
url.port = address.port.toString() ?? "";
if (protocol === "https") {
url.protocol = "https:";
}
}
return url;
}
function buildServerApi(protocol, server, app) {
const baseUrl = getServerUrl(protocol, server);
const api = {
url: createUrlBuilder(baseUrl)
};
Object.defineProperty(api, kServer, { value: server });
return api;
}
export { DEFAULT_PROTOCOLS, createTestHttpServer, createUrlBuilder, getServerUrl, kApp, kEmitter, kServer, kServers, toRelativePathname };