1import * as NodeNet from "node:net";
2
3import { assert, describe, it } from "@effect/vitest";
4import * as Effect from "effect/Effect";
5import { vi } from "vite-plus/test";
6
7import * as NetService from "./Net.ts";
8
9const closeServer = (server: NodeNet.Server) =>
10 Effect.sync(() => {
11 try {
12 server.close();
13 } catch {}
14 });
15
16const getPort = (server: NodeNet.Server): number => {
17 const address = server.address();
18 return typeof address === "object" && address !== null ? address.port : 0;
19};
20
21const openServer = (host?: string): Effect.Effect<NodeNet.Server, NetService.NetError> =>
22 Effect.callback<NodeNet.Server, NetService.NetError>((resume) => {
23 const server = NodeNet.createServer();
24 let settled = false;
25
26 const settle = (effect: Effect.Effect<NodeNet.Server, NetService.NetError>) => {
27 if (settled) return;
28 settled = true;
29 resume(effect);
30 };
31
32 server.once("error", (cause) => {
33 settle(
34 Effect.fail(new NetService.NetError({ message: "Failed to open test server", cause })),
35 );
36 });
37
38 if (host) {
39 server.listen(0, host, () => settle(Effect.succeed(server)));
40 } else {
41 server.listen(0, () => settle(Effect.succeed(server)));
42 }
43
44 return closeServer(server);
45 });
46
47it.layer(NetService.layer)("NetService", (it) => {
48 describe("Net helpers", () => {
49 it.effect.each(["EAFNOSUPPORT", "EADDRNOTAVAIL"])(
50 "keeps ports available when IPv6 binds fail with %s",
51 (code) =>
52 Effect.gen(function* () {
53 const net = yield* NetService.NetService;
54 const port = yield* net.reserveLoopbackPort();
55 const listen = NodeNet.Server.prototype.listen;
56 const spy = vi.spyOn(NodeNet.Server.prototype, "listen").mockImplementation(function (
57 this: NodeNet.Server,
58 ...args
59 ) {
60 const [options] = args;
61 if (options?.host === "::1" || options?.host === "::") {
62 process.nextTick(() => this.emit("error", Object.assign(new Error(code), { code })));
63 return this;
64 }
65 return listen.apply(this, args);
66 });
67 try {
68 assert.equal(yield* net.isPortAvailableOnLoopback(port), true);
69 assert.equal(yield* net.canListenOnHost(port, "::"), true);
70 } finally {
71 spy.mockRestore();
72 }
73 }),
74 );
75
76 it.effect("isPortAvailableOnLoopback reports false for an occupied port", () =>
77 Effect.acquireUseRelease(
78 openServer("127.0.0.1"),
79 (server) =>
80 Effect.gen(function* () {
81 const net = yield* NetService.NetService;
82 const port = getPort(server);
83
84 const available = yield* net.isPortAvailableOnLoopback(port);
85 assert.equal(available, false);
86 }),
87 closeServer,
88 ),
89 );
90
91 it.effect("findAvailablePort falls back when a wildcard listener occupies IPv4", () =>
92 Effect.acquireUseRelease(
93 openServer("0.0.0.0"),
94 (server) =>
95 Effect.gen(function* () {
96 const net = yield* NetService.NetService;
97 const preferred = getPort(server);
98
99 const resolved = yield* net.findAvailablePort(preferred);
100 assert.ok(resolved > 0);
101 assert.notEqual(resolved, preferred);
102 }),
103 closeServer,
104 ),
105 );
106 });
107});