UNPKG

@epic-web/test-server

Version:

Utility for creating HTTP and WebSocket servers for testing

206 lines (203 loc) 6.18 kB
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 };