























@@ -0,0 +1,115 @@
1+import { afterEach, describe, expect, it, vi } from "vitest";
2+3+const mocks = vi.hoisted(() => ({
4+enableTailscaleServe: vi.fn(async (_port: number) => undefined),
5+disableTailscaleServe: vi.fn(async () => undefined),
6+enableTailscaleFunnel: vi.fn(async (_port: number) => undefined),
7+disableTailscaleFunnel: vi.fn(async () => undefined),
8+getTailnetHostname: vi.fn(async () => null),
9+hasTailscaleFunnelRouteForPort: vi.fn(async (_port: number) => false),
10+}));
11+12+vi.mock("../infra/tailscale.js", () => ({
13+enableTailscaleServe: mocks.enableTailscaleServe,
14+disableTailscaleServe: mocks.disableTailscaleServe,
15+enableTailscaleFunnel: mocks.enableTailscaleFunnel,
16+disableTailscaleFunnel: mocks.disableTailscaleFunnel,
17+getTailnetHostname: mocks.getTailnetHostname,
18+hasTailscaleFunnelRouteForPort: mocks.hasTailscaleFunnelRouteForPort,
19+}));
20+21+import { startGatewayTailscaleExposure } from "./server-tailscale.js";
22+23+function createLogger() {
24+return { info: vi.fn(), warn: vi.fn() };
25+}
26+27+afterEach(() => {
28+for (const fn of Object.values(mocks)) {
29+fn.mockReset();
30+}
31+mocks.enableTailscaleServe.mockResolvedValue(undefined);
32+mocks.disableTailscaleServe.mockResolvedValue(undefined);
33+mocks.enableTailscaleFunnel.mockResolvedValue(undefined);
34+mocks.disableTailscaleFunnel.mockResolvedValue(undefined);
35+mocks.getTailnetHostname.mockResolvedValue(null);
36+mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(false);
37+});
38+39+describe("startGatewayTailscaleExposure preserveFunnel", () => {
40+it("calls enableTailscaleServe in serve mode when preserveFunnel is unset", async () => {
41+const logTailscale = createLogger();
42+43+await startGatewayTailscaleExposure({
44+tailscaleMode: "serve",
45+port: 18789,
46+ logTailscale,
47+});
48+49+expect(mocks.enableTailscaleServe).toHaveBeenCalledWith(18789);
50+expect(mocks.hasTailscaleFunnelRouteForPort).not.toHaveBeenCalled();
51+});
52+53+it("skips enableTailscaleServe when preserveFunnel is true and a Funnel route covers the port", async () => {
54+const logTailscale = createLogger();
55+mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(true);
56+57+await startGatewayTailscaleExposure({
58+tailscaleMode: "serve",
59+port: 18789,
60+preserveFunnel: true,
61+ logTailscale,
62+});
63+64+expect(mocks.hasTailscaleFunnelRouteForPort).toHaveBeenCalledWith(18789);
65+expect(mocks.enableTailscaleServe).not.toHaveBeenCalled();
66+expect(logTailscale.info).toHaveBeenCalledWith(expect.stringMatching(/preserv/i));
67+});
68+69+it("notes resetOnExit is a no-op when preserveFunnel skips Serve", async () => {
70+const logTailscale = createLogger();
71+mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(true);
72+73+await startGatewayTailscaleExposure({
74+tailscaleMode: "serve",
75+port: 18789,
76+preserveFunnel: true,
77+resetOnExit: true,
78+ logTailscale,
79+});
80+81+expect(mocks.enableTailscaleServe).not.toHaveBeenCalled();
82+expect(logTailscale.info).toHaveBeenCalledWith(
83+expect.stringMatching(/resetOnExit is a no-op/i),
84+);
85+});
86+87+it("falls back to enableTailscaleServe when preserveFunnel is true but no Funnel route exists for the port", async () => {
88+const logTailscale = createLogger();
89+mocks.hasTailscaleFunnelRouteForPort.mockResolvedValue(false);
90+91+await startGatewayTailscaleExposure({
92+tailscaleMode: "serve",
93+port: 18789,
94+preserveFunnel: true,
95+ logTailscale,
96+});
97+98+expect(mocks.hasTailscaleFunnelRouteForPort).toHaveBeenCalledWith(18789);
99+expect(mocks.enableTailscaleServe).toHaveBeenCalledWith(18789);
100+});
101+102+it("never consults the Funnel route helper when running in funnel mode", async () => {
103+const logTailscale = createLogger();
104+105+await startGatewayTailscaleExposure({
106+tailscaleMode: "funnel",
107+port: 18789,
108+preserveFunnel: true,
109+ logTailscale,
110+});
111+112+expect(mocks.hasTailscaleFunnelRouteForPort).not.toHaveBeenCalled();
113+expect(mocks.enableTailscaleFunnel).toHaveBeenCalledWith(18789);
114+});
115+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。