Skip to content
Open
210 changes: 208 additions & 2 deletions apps/server/src/preview/PortScanner.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import * as NodeNet from "node:net";

import * as NodeServices from "@effect/platform-node/NodeServices";
import { it as effectIt } from "@effect/vitest";
import {
CONFIGURED_LOCAL_SERVER_URLS_MAX_ITEMS,
PREVIEW_URL_MAX_LENGTH,
type DiscoveredLocalServer,
} from "@t3tools/contracts";
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
import { HostProcessEnvironment, HostProcessPlatform } from "@t3tools/shared/hostProcess";
import * as Net from "@t3tools/shared/Net";
import * as Cause from "effect/Cause";
import * as Clock from "effect/Clock";
Expand All @@ -18,7 +19,7 @@ import * as Layer from "effect/Layer";
import * as PlatformError from "effect/PlatformError";
import * as Scope from "effect/Scope";
import * as TestClock from "effect/testing/TestClock";
import { expect } from "vite-plus/test";
import { describe, expect, it } from "vite-plus/test";
import { FetchHttpClient } from "effect/unstable/http";

import * as ProcessRunner from "../processRunner.ts";
Expand Down Expand Up @@ -59,6 +60,8 @@ const makeProbeFailureLayer = (
PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.succeed(HostProcessEnvironment, {}),
Layer.succeed(ProcessRunner.ProcessRunner, { run }),
Layer.succeed(Net.NetService, {
canListenOnHost: () => Effect.succeed(true),
Expand All @@ -76,6 +79,8 @@ const makeProbeFailureLayer = (
const TestPortDiscoveryLive = PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.succeed(HostProcessEnvironment, {}),
TestProcessRunner,
TestIntegrationNet,
Layer.succeed(HostProcessPlatform, "win32"),
Expand All @@ -93,6 +98,8 @@ const makeLsofScannerLayer = (input: {
PortScanner.layer.pipe(
Layer.provide(
Layer.mergeAll(
NodeServices.layer,
Layer.succeed(HostProcessEnvironment, {}),
Layer.succeed(ProcessRunner.ProcessRunner, {
run: () =>
Effect.succeed({
Expand Down Expand Up @@ -121,6 +128,205 @@ const makeLsofScannerLayer = (input: {
),
);

describe("Portless route enrichment", () => {
it("uses the live Portless URL for its target listener", () => {
const routes = PortScanner.__testing.parsePortlessRouteSnapshot({
routesJson: JSON.stringify([
{
hostname: "eng-1252-simplify-the-onboarding.artelo.localhost",
port: 4058,
pid: 123,
},
]),
proxyPortRaw: "443\n",
tls: true,
isProcessAlive: (pid) => pid === 123,
});

expect(routes.get(4058)).toEqual({
url: "https://eng-1252-simplify-the-onboarding.artelo.localhost",
urlKind: "local-proxy",
});
expect(
PortScanner.__testing.applyNamedRoutes(
[
{
host: "localhost",
port: 4058,
url: "http://localhost:4058",
processName: "node",
pid: 456,
terminal: null,
},
],
routes,
),
).toEqual([
{
host: "localhost",
port: 4058,
url: "https://eng-1252-simplify-the-onboarding.artelo.localhost",
urlKind: "local-proxy",
processName: "node",
pid: 456,
terminal: null,
},
]);
});

it("ignores stale routes and preserves custom proxy settings", () => {
const routes = PortScanner.__testing.parsePortlessRouteSnapshot({
routesJson: JSON.stringify([
{ hostname: "stale.localhost", port: 3000, pid: 111 },
{ hostname: "current.test", port: 3001, pid: 222 },
{ hostname: "not a hostname", port: 3002, pid: 222 },
]),
proxyPortRaw: "8080",
tls: false,
isProcessAlive: (pid) => pid === 222,
});

expect([...routes]).toEqual([
[3001, { url: "http://current.test:8080", urlKind: "local-proxy" }],
]);
});

it.each(["8080abc", "443.5"])("ignores a malformed proxy port of %s", (proxyPortRaw) => {
const routes = PortScanner.__testing.parsePortlessRouteSnapshot({
routesJson: JSON.stringify([{ hostname: "current.test", port: 3001, pid: 222 }]),
proxyPortRaw,
tls: true,
isProcessAlive: (pid) => pid === 222,
});

expect([...routes]).toEqual([[3001, { url: "https://current.test", urlKind: "local-proxy" }]]);
});
});

describe("ngrok route enrichment", () => {
it("maps public HTTP tunnels to their loopback target and prefers HTTPS", () => {
const routes = PortScanner.__testing.parseNgrokTunnelSnapshot({
tunnels: [
{
public_url: "http://feature.ngrok-free.app",
config: { addr: "http://localhost:4058" },
},
{
public_url: "https://feature.ngrok-free.app",
config: { addr: "http://127.0.0.1:4058" },
},
],
});

expect(routes && [...routes]).toEqual([
[4058, { url: "https://feature.ngrok-free.app/", urlKind: "public-tunnel" }],
]);
});

it("accepts current forwards_to entries and ignores unsafe or non-web tunnels", () => {
const routes = PortScanner.__testing.parseNgrokTunnelSnapshot({
tunnels: [
{ public_url: "https://docs.example.com", forwards_to: "[::1]:4312" },
{ public_url: "https://invalid.example.com", forwards_to: "internal.example.com:4313" },
{ public_url: "tcp://1.tcp.ngrok.io:12345", config: { addr: "localhost:4314" } },
{ public_url: "https://user:secret@example.com", config: { addr: "4315" } },
],
});

expect(routes && [...routes]).toEqual([
[4312, { url: "https://docs.example.com/", urlKind: "public-tunnel" }],
]);
});

it("returns null when the agent response does not match the tunnel list contract", () => {
expect(PortScanner.__testing.parseNgrokTunnelSnapshot({ tunnels: "invalid" })).toBeNull();
});
});

effectIt.effect("replaces a discovered listener with its ngrok public URL", () => {
const targetPort = 43_127;
const configuredUrl = `http://localhost:${targetPort}/docs?mode=test#results`;
const requests: string[] = [];
const fetchFn = ((input: Parameters<typeof globalThis.fetch>[0]) => {
const url = String(input);
requests.push(url);
if (url === "http://localhost:4040/api/tunnels") {
return Promise.resolve(
Response.json({
tunnels: [
{
public_url: "https://feature.ngrok-free.app",
config: { addr: `http://localhost:${targetPort}` },
},
],
}),
);
}
return Promise.resolve(new Response("app", { headers: { "content-type": "text/html" } }));
}) as typeof globalThis.fetch;
const layer = makeProbeFailureLayer(
() =>
Effect.succeed({
stdout: `p1234\ncnode\nn*:${targetPort}\np5678\ncngrok\nn127.0.0.1:4040\n`,
stderr: "",
code: null,
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
stdoutInvalidUtf8: false,
stderrInvalidUtf8: false,
}),
fetchFn,
);

return Effect.gen(function* () {
const scanner = yield* PortScanner.PortDiscovery;
yield* scanner.registerTerminalProcesses({
threadId: "thread-ngrok",
terminalId: "term-ngrok",
processIds: [5678],
});
const servers = yield* scanner.scan([configuredUrl]);
expect(servers.map((server) => [server.port, server.url, server.urlKind])).toEqual([
[targetPort, "https://feature.ngrok-free.app/docs?mode=test#results", "public-tunnel"],
]);
expect(servers[0]?.terminal).toEqual({
threadId: "thread-ngrok",
terminalId: "term-ngrok",
});
expect(requests).toContain("http://localhost:4040/api/tunnels");
expect(requests).toContain(configuredUrl);
}).pipe(Effect.provide(layer));
});

effectIt.effect("keeps a non-ngrok web server on the default agent port", () => {
const fetchFn = ((input: Parameters<typeof globalThis.fetch>[0]) =>
Promise.resolve(
String(input) === "http://localhost:4040/api/tunnels"
? Response.json({ tunnels: [] })
: new Response("app", { headers: { "content-type": "text/html" } }),
)) as typeof globalThis.fetch;
const layer = makeProbeFailureLayer(
() =>
Effect.succeed({
stdout: "p1234\ncnode\nn*:4040\n",
stderr: "",
code: null,
timedOut: false,
stdoutTruncated: false,
stderrTruncated: false,
stdoutInvalidUtf8: false,
stderrInvalidUtf8: false,
}),
fetchFn,
);

return Effect.gen(function* () {
const scanner = yield* PortScanner.PortDiscovery;
expect(yield* scanner.scan()).toMatchObject([{ port: 4040, url: "http://localhost:4040" }]);
}).pipe(Effect.provide(layer));
});

const openServer = (
port: number,
onConnection: (socket: NodeNet.Socket) => void,
Expand Down
Loading
Loading