1import { EnvironmentId } from "@t3tools/contracts";
2import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
3
4const readPreparedConnection = vi.fn();
5const env = vi.hoisted(() => ({ isElectron: false }));
6
7vi.mock("~/state/session", () => ({ readPreparedConnection }));
8vi.mock("~/env", () => env);
9
10describe("browser target resolver", () => {
11 beforeEach(() => readPreparedConnection.mockReset());
12
13 it("maps environment ports onto a private network host", async () => {
14 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://192.168.1.25:3773" });
15 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
16 expect(
17 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
18 kind: "environment-port",
19 port: 5173,
20 path: "/dashboard",
21 }),
22 ).toEqual({
23 requestedUrl: "http://localhost:5173/dashboard",
24 resolvedUrl: "http://192.168.1.25:5173/dashboard",
25 resolutionKind: "direct-private-network",
26 environmentId: "environment-1",
27 });
28 });
29
30 it("preserves explicit loopback URL navigation for a remote Tailscale environment", async () => {
31 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://100.65.180.100:3773" });
32 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
33 expect(
34 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
35 kind: "url",
36 url: "http://localhost:5173/dashboard?mode=test#results",
37 }),
38 ).toEqual({
39 requestedUrl: "http://localhost:5173/dashboard?mode=test#results",
40 resolvedUrl: "http://localhost:5173/dashboard?mode=test#results",
41 resolutionKind: "direct",
42 environmentId: "environment-1",
43 });
44 });
45
46 it("preserves explicit IPv4 loopback URL navigation for a private network environment", async () => {
47 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://192.168.1.50:3773" });
48 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
49 expect(
50 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
51 kind: "url",
52 url: "http://127.0.0.1:5999/",
53 }),
54 ).toEqual({
55 requestedUrl: "http://127.0.0.1:5999/",
56 resolvedUrl: "http://127.0.0.1:5999/",
57 resolutionKind: "direct",
58 environmentId: "environment-1",
59 });
60 });
61
62 it("preserves URL credentials on explicit loopback navigation", async () => {
63 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://100.65.180.100:3773" });
64 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
65 expect(
66 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
67 kind: "url",
68 url: "http://user:p%40ss@localhost:5173/dashboard",
69 }).resolvedUrl,
70 ).toBe("http://user:p%40ss@localhost:5173/dashboard");
71 });
72
73 it("preserves credentialed loopback URLs for private IPv6 environments", async () => {
74 readPreparedConnection.mockReturnValue({
75 httpBaseUrl: "http://[fd7a:115c:a1e0::53]:3773",
76 });
77 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
78 expect(
79 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
80 kind: "url",
81 url: "http://user:p%40ss@localhost:5173/dashboard?mode=test#results",
82 }).resolvedUrl,
83 ).toBe("http://user:p%40ss@localhost:5173/dashboard?mode=test#results");
84 });
85
86 it("preserves schemeless localhost navigation for a remote environment", async () => {
87 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://192.168.1.25:3773" });
88 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
89 expect(
90 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
91 kind: "url",
92 url: "localhost:3000/app",
93 }).resolvedUrl,
94 ).toBe("localhost:3000/app");
95 });
96
97 it("keeps localhost navigation local for a local environment", async () => {
98 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://127.0.0.1:3773" });
99 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
100 expect(
101 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
102 kind: "url",
103 url: "localhost:3000/app",
104 }),
105 ).toEqual({
106 requestedUrl: "localhost:3000/app",
107 resolvedUrl: "localhost:3000/app",
108 resolutionKind: "direct",
109 environmentId: "environment-1",
110 });
111 });
112
113 it("keeps localhost navigation local for the full IPv4 loopback range", async () => {
114 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://127.0.0.2:3773" });
115 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
116 expect(
117 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
118 kind: "url",
119 url: "http://localhost:3000/app",
120 }),
121 ).toEqual({
122 requestedUrl: "http://localhost:3000/app",
123 resolvedUrl: "http://localhost:3000/app",
124 resolutionKind: "direct",
125 environmentId: "environment-1",
126 });
127 });
128
129 it("refuses public relay hosts until the authenticated gateway exists", async () => {
130 readPreparedConnection.mockReturnValue({ httpBaseUrl: "https://relay.example.com" });
131 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
132 expect(() =>
133 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
134 kind: "environment-port",
135 port: 5173,
136 }),
137 ).toThrow(/authenticated preview gateway/);
138 expect(
139 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
140 kind: "url",
141 url: "http://localhost:5173",
142 }),
143 ).toMatchObject({ resolvedUrl: "http://localhost:5173", resolutionKind: "direct" });
144 });
145
146 it("normalizes schemeless localhost server-picker values", async () => {
147 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://localhost:3773" });
148 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
149 expect(resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "localhost:5173")).toBe(
150 "http://localhost:5173/",
151 );
152 expect(
153 resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "0.0.0.0:3000/app"),
154 ).toBe("http://localhost:3000/app");
155 });
156
157 it("maps discovered loopback servers onto a remote environment host", async () => {
158 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://192.168.1.25:3773" });
159 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
160 expect(
161 resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "localhost:3000/app"),
162 ).toBe("http://192.168.1.25:3000/app");
163 });
164
165 it("keeps localhost for servers on the desktop's own WSL or primary backend", async () => {
166 // WSL NAT mode serves the backend on the distro IP, but a loopback-only dev
167 // server there answers only on localhost from the Windows desktop.
168 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
169 env.isElectron = true;
170 try {
171 for (const target of [
172 { _tag: "BearerConnectionTarget", connectionId: "local:wsl:Ubuntu" },
173 { _tag: "PrimaryConnectionTarget" },
174 ]) {
175 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://172.24.66.27:3773", target });
176 expect(
177 resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "localhost:3001/"),
178 ).toBe("http://localhost:3001/");
179 }
180 readPreparedConnection.mockReturnValue({
181 httpBaseUrl: "http://192.168.1.25:3773",
182 target: { _tag: "BearerConnectionTarget", connectionId: "saved:remote" },
183 });
184 expect(
185 resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "localhost:3001/"),
186 ).toBe("http://192.168.1.25:3001/");
187 } finally {
188 env.isElectron = false;
189 }
190 });
191
192 it("preserves localhost server-picker values when the prepared base is 127.0.0.1", async () => {
193 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://127.0.0.1:3773" });
194 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
195 expect(
196 resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "localhost:5173/app?x=1#top"),
197 ).toBe("http://localhost:5173/app?x=1#top");
198 });
199
200 it("normalizes public URLs without treating them as environment ports", async () => {
201 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
202 expect(resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), "example.com/app")).toBe(
203 "https://example.com/app",
204 );
205 });
206
207 it("supports private IPv6 environment hosts", async () => {
208 readPreparedConnection.mockReturnValue({
209 httpBaseUrl: "http://[fd7a:115c:a1e0::53]:3773",
210 });
211 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
212 expect(
213 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
214 kind: "environment-port",
215 port: 5173,
216 path: "/app?mode=test",
217 }).resolvedUrl,
218 ).toBe("http://[fd7a:115c:a1e0::53]:5173/app?mode=test");
219 });
220
221 it("supports a local IPv6 environment host", async () => {
222 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://[::1]:3773" });
223 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
224 expect(
225 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
226 kind: "environment-port",
227 port: 5173,
228 }).resolvedUrl,
229 ).toBe("http://localhost:5173/");
230 });
231
232 it("maps local IPv4 environment ports onto localhost for dual-stack guests", async () => {
233 readPreparedConnection.mockReturnValue({ httpBaseUrl: "http://127.0.0.1:3773" });
234 const { resolveBrowserNavigationTarget } = await import("./browserTargetResolver");
235 expect(
236 resolveBrowserNavigationTarget(EnvironmentId.make("environment-1"), {
237 kind: "environment-port",
238 port: 5173,
239 path: "/app",
240 }).resolvedUrl,
241 ).toBe("http://localhost:5173/app");
242 });
243
244 it("leaves malformed input for the normal navigation error path", async () => {
245 const { resolveDiscoveredServerUrl } = await import("./browserTargetResolver");
246 expect(resolveDiscoveredServerUrl(EnvironmentId.make("environment-1"), " ")).toBe(" ");
247 });
248
249 it("classifies exact private IPv4 and IPv6 boundaries", async () => {
250 const { isPrivateNetworkHost } = await import("./browserTargetResolver");
251 const privateHosts = [
252 "0.0.0.0",
253 "10.0.0.0",
254 "10.255.255.255",
255 "100.64.0.0",
256 "100.127.255.255",
257 "127.0.0.0",
258 "127.255.255.255",
259 "169.254.0.0",
260 "169.254.255.255",
261 "172.16.0.0",
262 "172.31.255.255",
263 "192.168.0.0",
264 "192.168.255.255",
265 "198.18.0.0",
266 "198.19.255.255",
267 "fc00::",
268 "fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
269 "fe80::",
270 "febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
271 "::ffff:192.168.1.1",
272 "localhost.",
273 "localhost..",
274 "devbox.",
275 "devbox..",
276 "printer.local.",
277 "printer.local..",
278 "printer.home.arpa.",
279 "printer.home.arpa..",
280 "devbox.example.ts.net.",
281 "devbox.example.ts.net..",
282 ];
283 const publicHosts = [
284 "1.0.0.0",
285 "100.63.255.255",
286 "100.128.0.0",
287 "169.253.255.255",
288 "169.255.0.0",
289 "172.15.255.255",
290 "172.32.0.0",
291 "192.167.255.255",
292 "192.169.0.0",
293 "198.17.255.255",
294 "198.20.0.0",
295 "fbff:ffff::",
296 "fec0::",
297 "2001:4860:4860::8888",
298 "::ffff:8.8.8.8",
299 "example.com.",
300 ];
301 expect(privateHosts.filter((host) => !isPrivateNetworkHost(host))).toEqual([]);
302 expect(publicHosts.filter(isPrivateNetworkHost)).toEqual([]);
303 });
304
305 it("allows only globally routable hosts to reach a public favicon provider", async () => {
306 const { isPublicFaviconHost } = await import("./browserTargetResolver");
307 const nonPublic = [
308 "192.0.0.0",
309 "192.0.0.255",
310 "192.0.2.0",
311 "192.0.2.255",
312 "192.88.99.0",
313 "192.88.99.255",
314 "198.51.100.0",
315 "198.51.100.255",
316 "203.0.113.0",
317 "203.0.113.255",
318 "224.0.0.0",
319 "255.255.255.255",
320 "::2",
321 "100::",
322 "100::ffff:ffff:ffff:ffff",
323 "100:0:0:1::",
324 "100:0:0:1:ffff:ffff:ffff:ffff",
325 "64:ff9b:1::1",
326 "64:ff9b::a00:1",
327 "64:ff9b::7f00:1",
328 "64:ff9b::c0a8:101",
329 "64:ff9b::c000:201",
330 "2001:5::1",
331 "2001:2::",
332 "2001:2:0:ffff:ffff:ffff:ffff:ffff",
333 "2001:db8::",
334 "2001:db8:ffff:ffff:ffff:ffff:ffff:ffff",
335 "3fff::",
336 "3fff:fff:ffff:ffff:ffff:ffff:ffff:ffff",
337 "5f00::1",
338 "fec0::",
339 "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
340 "::ffff:192.0.2.1",
341 "app.test",
342 "app.test..",
343 "printer.local..",
344 "printer.home.arpa..",
345 "devbox.example.ts.net..",
346 "127.0.0.1..",
347 "127.1..",
348 "10.1..",
349 "172.16.1..",
350 "192.168.1..",
351 "service.internal",
352 "hidden.onion",
353 ];
354 const publicHosts = [
355 "191.255.255.255",
356 "192.0.1.255",
357 "192.0.3.0",
358 "198.51.99.255",
359 "198.51.101.0",
360 "203.0.112.255",
361 "203.0.114.0",
362 "223.255.255.255",
363 "1.1.1.1",
364 "2001:4860:4860::8888",
365 "2606:4700:4700::1111",
366 "64:ff9b::808:808",
367 "2001:1::1",
368 "2001:3::1",
369 "2001:4:112::1",
370 "2001:20::1",
371 "2001:30::1",
372 "::ffff:8.8.8.8",
373 "example.com",
374 "example.com.",
375 ];
376 expect(nonPublic.filter(isPublicFaviconHost)).toEqual([]);
377 expect(publicHosts.filter((host) => !isPublicFaviconHost(host))).toEqual([]);
378 });
379});