
























1-// Covers Tailscale install, whois, Serve, and Funnel helpers.
1+// Covers Tailscale whois, Serve, and Funnel helpers.
22import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
33import { captureEnv } from "../test-utils/env.js";
44import * as tailscale from "./tailscale.js";
5566const {
7- ensureGoInstalled,
8- ensureTailscaledInstalled,
97 getTailnetHostname,
108 getTestTailscaleBinaryOverride,
119 readTailscaleWhoisIdentity,
1210 enableTailscaleServe,
1311 disableTailscaleServe,
14- ensureFunnel,
1512 hasTailscaleFunnelRouteForPort,
1613 tailscaleFunnelStatusCoversPort,
1714} = tailscale;
1815const tailscaleBin = "tailscale";
191620-function createRuntimeWithExitError() {
21-return {
22-error: vi.fn(),
23-log: vi.fn(),
24-exit: ((code: number) => {
25-throw new Error(`exit ${code}`);
26-}) as (code: number) => never,
27-};
28-}
29-3017function expectExecCall(
3118exec: ReturnType<typeof vi.fn>,
3219callNumber: number,
@@ -169,52 +156,6 @@ describe("tailscale helpers", () => {
169156expect(getTestTailscaleBinaryOverride()).toBeNull();
170157});
171158172-it.each([
173-{
174-name: "ensureGoInstalled installs when missing and user agrees",
175-fn: ensureGoInstalled,
176-missingError: new Error("no go"),
177-installCommand: ["brew", ["install", "go"]] as const,
178-promptResult: true,
179-},
180-{
181-name: "ensureTailscaledInstalled installs when missing and user agrees",
182-fn: ensureTailscaledInstalled,
183-missingError: new Error("missing"),
184-installCommand: ["brew", ["install", "tailscale"]] as const,
185-promptResult: true,
186-},
187-])("$name", async ({ fn, missingError, installCommand, promptResult }) => {
188-const exec = vi.fn().mockRejectedValueOnce(missingError).mockResolvedValue({});
189-const prompt = vi.fn().mockResolvedValue(promptResult);
190-const runtime = createRuntimeWithExitError();
191-await fn(exec as never, prompt, runtime);
192-expect(exec).toHaveBeenCalledWith(installCommand[0], installCommand[1]);
193-});
194-195-it.each([
196-{
197-name: "ensureGoInstalled exits when missing and user declines install",
198-fn: ensureGoInstalled,
199-missingError: new Error("no go"),
200-errorMessage: "Go is required to build tailscaled from source. Aborting.",
201-},
202-{
203-name: "ensureTailscaledInstalled exits when missing and user declines install",
204-fn: ensureTailscaledInstalled,
205-missingError: new Error("missing"),
206-errorMessage: "tailscaled is required for user-space funnel. Aborting.",
207-},
208-])("$name", async ({ fn, missingError, errorMessage }) => {
209-const exec = vi.fn().mockRejectedValueOnce(missingError);
210-const prompt = vi.fn().mockResolvedValue(false);
211-const runtime = createRuntimeWithExitError();
212-213-await expect(fn(exec as never, prompt, runtime)).rejects.toThrow("exit 1");
214-expect(runtime.error).toHaveBeenCalledWith(errorMessage);
215-expect(exec).toHaveBeenCalledTimes(1);
216-});
217-218159it("enableTailscaleServe attempts normal first, then sudo", async () => {
219160const exec = vi
220161.fn()
@@ -291,68 +232,6 @@ describe("tailscale helpers", () => {
291232});
292233});
293234294-it("ensureFunnel uses fallback for enabling", async () => {
295-const exec = vi
296-.fn()
297-.mockResolvedValueOnce({ stdout: JSON.stringify({ BackendState: "Running" }) }) // status
298-.mockRejectedValueOnce(new Error("permission denied")) // enable normal
299-.mockResolvedValueOnce({ stdout: "" }); // enable sudo
300-301-const runtime = {
302-error: vi.fn(),
303-log: vi.fn(),
304-exit: vi.fn() as unknown as (code: number) => never,
305-};
306-const prompt = vi.fn();
307-308-await ensureFunnel(8080, exec as never, runtime, prompt);
309-310-expect(exec).toHaveBeenCalledTimes(3);
311-expectExecCall(exec, 1, tailscaleBin, ["funnel", "status", "--json"]);
312-expectExecCall(exec, 2, tailscaleBin, ["funnel", "--yes", "--bg", "8080"], {
313-maxBuffer: 200_000,
314-timeoutMs: 15_000,
315-});
316-expectExecCall(exec, 3, "sudo", ["-n", tailscaleBin, "funnel", "--yes", "--bg", "8080"], {
317-maxBuffer: 200_000,
318-timeoutMs: 15_000,
319-});
320-});
321-322-it("ensureFunnel accepts noisy JSON status output", async () => {
323-const exec = vi
324-.fn()
325-.mockResolvedValueOnce({
326-stdout: 'warning: stale state\n{"BackendState":"Running"}\n',
327-})
328-.mockResolvedValueOnce({ stdout: "" });
329-const runtime = createRuntimeWithExitError();
330-const prompt = vi.fn();
331-332-await ensureFunnel(8080, exec as never, runtime, prompt);
333-334-expect(exec).toHaveBeenCalledTimes(2);
335-expectExecCall(exec, 2, tailscaleBin, ["funnel", "--yes", "--bg", "8080"], {
336-maxBuffer: 200_000,
337-timeoutMs: 15_000,
338-});
339-expect(prompt).not.toHaveBeenCalled();
340-});
341-342-it("ensureFunnel treats malformed status output as a failure", async () => {
343-const exec = vi.fn().mockResolvedValueOnce({ stdout: "warning: stale state\n{not json}\n" });
344-const runtime = createRuntimeWithExitError();
345-const prompt = vi.fn();
346-347-await expect(ensureFunnel(8080, exec as never, runtime, prompt)).rejects.toThrow("exit 1");
348-349-expect(exec).toHaveBeenCalledTimes(1);
350-expect(prompt).not.toHaveBeenCalled();
351-expect(runtime.error).toHaveBeenCalledWith(
352-"Failed to enable Tailscale Funnel. Is it allowed on your tailnet?",
353-);
354-});
355-356235it("enableTailscaleServe skips sudo on non-permission errors", async () => {
357236const exec = vi.fn().mockRejectedValueOnce(new Error("boom"));
358237此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。