fix(cli): validate webhooks gmail --tailscale mode at parse time (#120272)

* fix(cli): validate webhooks gmail --tailscale mode at parse time

--tailscale <mode> was stored unchecked and cast to the tailscale union
type, so a typo like --tailscale offf (truthy and !== "off") was treated
as enabled and only failed deep inside tailscale setup with a raw CLI
usage error. Reject modes other than funnel|serve|off in
parseGmailCommonOptions, mirroring the strict numberOption pattern, and
export the setup/run parsers for direct testing.

* fix(cli): reject blank Gmail tailscale modes

* fix(cli): preserve tailscale mode type

---------

Co-authored-by: Dallin Romney <dallinromney@gmail.com>
This commit is contained in:
wanyongstar
2026-08-11 22:51:35 +08:00
committed by GitHub
parent 2183d637a2
commit 71ea11b537
2 changed files with 66 additions and 2 deletions
+53
View File
@@ -61,4 +61,57 @@ describe("webhooks cli", () => {
expect(mocks.runGmailSetup).not.toHaveBeenCalled();
expect(mocks.runGmailService).not.toHaveBeenCalled();
});
it.each([
["setup", "offf"],
["setup", ""],
["setup", " "],
["run", "offf"],
["run", ""],
["run", " "],
])("rejects invalid gmail %s --tailscale mode %j", async (command, mode) => {
const program = createProgram();
const args =
command === "setup"
? ["webhooks", "gmail", command, "--account", "default", "--tailscale", mode]
: ["webhooks", "gmail", command, "--tailscale", mode];
await expect(program.parseAsync(args, { from: "user" })).rejects.toThrow("__exit__:1");
expect(runtimeErrors().join("\n")).toContain(
"Invalid --tailscale (must be funnel, serve, or off).",
);
expect(mocks.runGmailSetup).not.toHaveBeenCalled();
expect(mocks.runGmailService).not.toHaveBeenCalled();
});
it.each([
["setup", "funnel"],
["setup", "serve"],
["setup", "off"],
["run", "funnel"],
["run", "serve"],
["run", "off"],
])("accepts valid gmail %s --tailscale %s", async (command, mode) => {
const program = createProgram();
const args =
command === "setup"
? ["webhooks", "gmail", command, "--account", "default", "--tailscale", mode]
: ["webhooks", "gmail", command, "--tailscale", mode];
await program.parseAsync(args, { from: "user" });
const runner = command === "setup" ? mocks.runGmailSetup : mocks.runGmailService;
expect(runner).toHaveBeenCalledWith(expect.objectContaining({ tailscale: mode }));
});
it("preserves an omitted gmail run --tailscale mode", async () => {
const program = createProgram();
await program.parseAsync(["webhooks", "gmail", "run"], { from: "user" });
expect(mocks.runGmailService).toHaveBeenCalledWith(
expect.objectContaining({ tailscale: undefined }),
);
});
});
+13 -2
View File
@@ -149,7 +149,7 @@ function parseGmailCommonOptions(raw: Record<string, unknown>) {
includeBody: booleanOption(raw.includeBody),
maxBytes: numberOption(raw.maxBytes, "--max-bytes"),
renewEveryMinutes: numberOption(raw.renewMinutes, "--renew-minutes"),
tailscaleRaw: normalizeOptionalString(raw.tailscale),
tailscale: tailscaleModeOption(raw.tailscale),
tailscalePath: normalizeOptionalString(raw.tailscalePath),
tailscaleTarget: normalizeOptionalString(raw.tailscaleTarget),
};
@@ -171,12 +171,23 @@ function gmailOptionsFromCommon(
includeBody: common.includeBody,
maxBytes: common.maxBytes,
renewEveryMinutes: common.renewEveryMinutes,
tailscale: common.tailscaleRaw as GmailRunOptions["tailscale"],
tailscale: common.tailscale,
tailscalePath: common.tailscalePath,
tailscaleTarget: common.tailscaleTarget,
};
}
function tailscaleModeOption(value: unknown): GmailRunOptions["tailscale"] {
if (value === undefined || value === null) {
return undefined;
}
const mode = normalizeOptionalString(value);
if (mode === "funnel" || mode === "serve" || mode === "off") {
return mode;
}
throw new Error("Invalid --tailscale (must be funnel, serve, or off).");
}
function numberOption(value: unknown, label: string): number | undefined {
if (value === undefined || value === null) {
return undefined;