1import * as NodeNet from "node:net";
2
3import { assert, describe, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5
6import * as NetService from "./Net.ts";
7
8const closeServer = (server: NodeNet.Server) =>
9 Effect.sync(() => {
10 try {
11 server.close();
12 } catch {
13 // Ignore cleanup failures in tests.
14 }
15 });
16
17const getPort = (server: NodeNet.Server): number => {
18 const address = server.address();
19 return typeof address === "object" && address !== null ? address.port : 0;
20};
21
22const openServer = (host?: string): Effect.Effect<NodeNet.Server, NetService.NetError> =>
23 Effect.callback<NodeNet.Server, NetService.NetError>((resume) => {
24 const server = NodeNet.createServer();
25 let settled = false;
26
27 const settle = (effect: Effect.Effect<NodeNet.Server, NetService.NetError>) => {
28 if (settled) return;
29 settled = true;
30 resume(effect);
31 };
32
33 server.once("error", (cause) => {
34 settle(
35 Effect.fail(new NetService.NetError({ message: "Failed to open test server", cause })),
36 );
37 });
38
39 if (host) {
40 server.listen(0, host, () => settle(Effect.succeed(server)));
41 } else {
42 server.listen(0, () => settle(Effect.succeed(server)));
43 }
44
45 return closeServer(server);
46 });
47
48it.layer(NetService.layer)("NetService", (it) => {
49 describe("Net helpers", () => {
50 it.effect("reserveLoopbackPort returns a positive loopback port", () =>
51 Effect.gen(function* () {
52 const net = yield* NetService.NetService;
53 const port = yield* net.reserveLoopbackPort();
54
55 assert.ok(port > 0);
56 }),
57 );
58
59 it.effect("isPortAvailableOnLoopback reports false for an occupied port", () =>
60 Effect.acquireUseRelease(
61 openServer("127.0.0.1"),
62 (server) =>
63 Effect.gen(function* () {
64 const net = yield* NetService.NetService;
65 const port = getPort(server);
66
67 const available = yield* net.isPortAvailableOnLoopback(port);
68 assert.equal(available, false);
69 }),
70 closeServer,
71 ),
72 );
73
74 it.effect("findAvailablePort returns preferred when it is free", () =>
75 Effect.gen(function* () {
76 const net = yield* NetService.NetService;
77 const preferred = yield* net.reserveLoopbackPort();
78
79 const resolved = yield* net.findAvailablePort(preferred);
80 assert.equal(resolved, preferred);
81 }),
82 );
83
84 it.effect("findAvailablePort falls back when a wildcard listener occupies IPv4", () =>
85 Effect.acquireUseRelease(
86 openServer("0.0.0.0"),
87 (server) =>
88 Effect.gen(function* () {
89 const net = yield* NetService.NetService;
90 const preferred = getPort(server);
91
92 const resolved = yield* net.findAvailablePort(preferred);
93 assert.ok(resolved > 0);
94 assert.notEqual(resolved, preferred);
95 }),
96 closeServer,
97 ),
98 );
99 });
100});