122 lines
4.4 KiB
TypeScript
122 lines
4.4 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
import { mkdtempSync, statSync, existsSync } from "node:fs";
|
|
import { readFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { tmpdir } from "node:os";
|
|
import { rm } from "node:fs/promises";
|
|
import {
|
|
writeSessionHookScripts,
|
|
cleanupSessionHookDir,
|
|
buildHookScriptContent,
|
|
buildNotifyShimContent,
|
|
HOOK_SCRIPT_NAMES,
|
|
} from "../hook-scripts.js";
|
|
|
|
describe("hook-scripts", () => {
|
|
let tmpDir: string;
|
|
let dir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = mkdtempSync(join(tmpdir(), "fusion-hook-scripts-"));
|
|
dir = join(tmpDir, "session-config");
|
|
});
|
|
|
|
afterEach(async () => {
|
|
await rm(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
const opts = {
|
|
sessionId: "sess-123",
|
|
token: "abc123def456token",
|
|
endpointUrl: "http://127.0.0.1:4040/api/cli-agent/hooks",
|
|
};
|
|
|
|
describe("buildHookScriptContent", () => {
|
|
it("POSTs to the endpoint URL with the token + session headers", () => {
|
|
const script = buildHookScriptContent(opts);
|
|
expect(script).toContain("http://127.0.0.1:4040/api/cli-agent/hooks");
|
|
expect(script).toContain("X-Fusion-Cli-Session-Token: $TOKEN");
|
|
expect(script).toContain("X-Fusion-Cli-Session-Id: $SESSION_ID");
|
|
expect(script).toContain("abc123def456token");
|
|
expect(script).toContain("sess-123");
|
|
});
|
|
|
|
it("uses curl with short timeouts and always exits 0", () => {
|
|
const script = buildHookScriptContent(opts);
|
|
expect(script).toContain("curl");
|
|
expect(script).toContain("--connect-timeout");
|
|
expect(script).toContain("--max-time");
|
|
// Failure tolerance: the curl line is `|| true` and the script ends `exit 0`.
|
|
expect(script).toContain("|| true");
|
|
expect(script.trimEnd().endsWith("exit 0")).toBe(true);
|
|
});
|
|
|
|
it("never sets an Origin header (CSRF-safe)", () => {
|
|
const script = buildHookScriptContent(opts);
|
|
expect(script.toLowerCase()).not.toContain("origin:");
|
|
});
|
|
|
|
it("starts with a sh shebang", () => {
|
|
expect(buildHookScriptContent(opts).startsWith("#!/bin/sh")).toBe(true);
|
|
});
|
|
|
|
it("shell-escapes a token containing a single quote", () => {
|
|
const script = buildHookScriptContent({ ...opts, token: "to'ken" });
|
|
// The single quote is escaped via the '\'' idiom — no raw unbalanced quote.
|
|
expect(script).toContain(`'\\''`);
|
|
});
|
|
});
|
|
|
|
describe("buildNotifyShimContent", () => {
|
|
it("forwards argv[1] (else stdin) and exits 0", () => {
|
|
const script = buildNotifyShimContent(opts);
|
|
expect(script.startsWith("#!/bin/sh")).toBe(true);
|
|
expect(script).toContain('"$1"');
|
|
expect(script).toContain("event=notify");
|
|
expect(script).toContain("X-Fusion-Cli-Session-Token: $TOKEN");
|
|
expect(script.trimEnd().endsWith("exit 0")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("writeSessionHookScripts", () => {
|
|
it("writes both scripts into the dir, marked executable", async () => {
|
|
const result = await writeSessionHookScripts({ ...opts, dir });
|
|
|
|
expect(result.hookScriptPath).toBe(join(dir, HOOK_SCRIPT_NAMES.hook));
|
|
expect(result.notifyScriptPath).toBe(join(dir, HOOK_SCRIPT_NAMES.notify));
|
|
expect(existsSync(result.hookScriptPath)).toBe(true);
|
|
expect(existsSync(result.notifyScriptPath)).toBe(true);
|
|
|
|
// Owner-executable bit set on both files.
|
|
const hookMode = statSync(result.hookScriptPath).mode;
|
|
const notifyMode = statSync(result.notifyScriptPath).mode;
|
|
expect(hookMode & 0o100).toBe(0o100);
|
|
expect(notifyMode & 0o100).toBe(0o100);
|
|
|
|
const hookContent = await readFile(result.hookScriptPath, "utf8");
|
|
expect(hookContent).toContain(opts.endpointUrl);
|
|
expect(hookContent).toContain(opts.token);
|
|
});
|
|
|
|
it("creates the dir if it does not exist", async () => {
|
|
expect(existsSync(dir)).toBe(false);
|
|
await writeSessionHookScripts({ ...opts, dir });
|
|
expect(existsSync(dir)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("cleanupSessionHookDir", () => {
|
|
it("removes the dir and its contents", async () => {
|
|
await writeSessionHookScripts({ ...opts, dir });
|
|
expect(existsSync(dir)).toBe(true);
|
|
|
|
await cleanupSessionHookDir(dir);
|
|
expect(existsSync(dir)).toBe(false);
|
|
});
|
|
|
|
it("is a no-op for a missing dir (never throws)", async () => {
|
|
await expect(cleanupSessionHookDir(join(tmpDir, "does-not-exist"))).resolves.toBeUndefined();
|
|
});
|
|
});
|
|
});
|