import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http"; import { FetchHttpClient, HttpClient } from "effect/unstable/http"; import { assert, it } from "@effect/vitest"; import { Effect, Layer, Schema } from "effect"; import { WebPortInspectionError, WebPortInspector } from "../Services/WebPortInspector.ts"; import { WebPortInspectorLive } from "./WebPortInspector.ts"; const closeServer = (server: Server) => Effect.callback((resume) => { server.closeAllConnections?.(); server.closeIdleConnections?.(); server.close(() => { resume(Effect.void); }); return Effect.sync(() => { try { server.closeAllConnections?.(); server.closeIdleConnections?.(); server.close(); } catch { // Ignore cleanup failures in tests. } }); }); const listenServer = (server: Server) => Effect.callback((resume) => { const onError = (error: Error) => { cleanup(); resume(Effect.fail(error)); }; const cleanup = () => { server.off("error", onError); }; server.once("error", onError); server.listen(0, "127.0.0.1", () => { cleanup(); const address = server.address(); if (!address || typeof address !== "object") { resume(Effect.fail(new Error("Server did not provide a valid listening address."))); return; } resume(Effect.succeed(address.port)); }); return Effect.gen(function* () { cleanup(); yield* closeServer(server); }); }); const startServer = ( handler: (request: IncomingMessage, response: ServerResponse) => void, ): Effect.Effect<{ server: Server; port: number }, Error> => Effect.gen(function* () { const server = createServer(handler); const port = yield* listenServer(server); return { server, port }; }); it.layer(WebPortInspectorLive.pipe(Layer.provide(FetchHttpClient.layer)))( "WebPortInspectorLive", (it) => { it.effect("treats slow HTML responses as web ports", () => Effect.acquireUseRelease( startServer((_request, response) => { setTimeout(() => { response.statusCode = 200; response.setHeader("content-type", "text/html; charset=utf-8"); response.end( "okhello", ); }, 800); }), ({ port }) => Effect.gen(function* () { const inspector = yield* WebPortInspector; const isWeb = yield* inspector.inspect(port); assert.equal(isWeb, true); }), ({ server }) => closeServer(server), ), ); it.effect("treats HTML responses with large bodies as web ports", () => Effect.acquireUseRelease( startServer((_request, response) => { response.statusCode = 200; response.setHeader("content-type", "text/html; charset=utf-8"); response.end( `x${"x".repeat(20_000)}`, ); }), ({ port }) => Effect.gen(function* () { const inspector = yield* WebPortInspector; const isWeb = yield* inspector.inspect(port); assert.equal(isWeb, true); }), ({ server }) => closeServer(server), ), ); it.effect("ignores HTTP 404 responses", () => Effect.acquireUseRelease( startServer((_request, response) => { response.statusCode = 404; response.end(); }), ({ port }) => Effect.gen(function* () { const inspector = yield* WebPortInspector; const isWeb = yield* inspector.inspect(port); assert.equal(isWeb, false); }), ({ server }) => closeServer(server), ), ); }, ); it.effect("WebPortInspectorLive preserves typed timeout errors", () => Effect.gen(function* () { const inspector = yield* WebPortInspector; const error = yield* inspector.inspect(3000).pipe(Effect.flip); assert.equal(Schema.is(WebPortInspectionError)(error), true); assert.equal(error.detail, "HTTP probe timed out."); }).pipe( Effect.provide( WebPortInspectorLive.pipe( Layer.provide( Layer.succeed(HttpClient.HttpClient, { execute: () => Effect.fail( new WebPortInspectionError({ port: 3000, host: "127.0.0.1", detail: "HTTP probe timed out.", }), ), } as never), ), ), ), ), );