import { existsSync, mkdtempSync, readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { type GitAuthServer, startGitAuthServer } from "./gitAuthServer.ts"; let server: GitAuthServer | undefined; afterEach(async () => { if (server) { await server.close(); server = undefined; } }); function makeTmpdir(): string { return mkdtempSync(join(tmpdir(), "askpass-test-")); } describe("git auth server lifecycle", () => { it("starts and listens on a port", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); expect(server.port).toBeGreaterThan(0); }); it("closes cleanly", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const port = server.port; await server.close(); server = undefined; // port should no longer accept connections const err = await fetch(`http://127.0.0.1:${port}/test`).catch((e) => e); expect(err).toBeInstanceOf(Error); }); }); describe("token delivery", () => { it("returns token on first request with valid code", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code = server.register("ghs_test_token_12345"); const res = await fetch(`http://127.0.0.1:${server.port}/${code}`); expect(res.status).toBe(200); const body = await res.text(); expect(body).toBe("ghs_test_token_12345"); }); it("returns 404 for unknown code", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const res = await fetch(`http://127.0.0.1:${server.port}/nonexistent-code`); expect(res.status).toBe(404); }); it("returns 400 for empty code", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const res = await fetch(`http://127.0.0.1:${server.port}/`); expect(res.status).toBe(400); }); it("returns 405 for non-GET methods", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code = server.register("token"); const res = await fetch(`http://127.0.0.1:${server.port}/${code}`, { method: "POST" }); expect(res.status).toBe(405); }); }); describe("single-use enforcement (tamper detection)", () => { it("returns 409 on second use of same code", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code = server.register("ghs_tamper_test"); const first = await fetch(`http://127.0.0.1:${server.port}/${code}`); expect(first.status).toBe(200); const second = await fetch(`http://127.0.0.1:${server.port}/${code}`); expect(second.status).toBe(409); const body = await second.text(); expect(body).toBe("compromised"); }); it("each register() call produces an independent code", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code1 = server.register("token-a"); const code2 = server.register("token-b"); expect(code1).not.toBe(code2); const res1 = await fetch(`http://127.0.0.1:${server.port}/${code1}`); expect(await res1.text()).toBe("token-a"); const res2 = await fetch(`http://127.0.0.1:${server.port}/${code2}`); expect(await res2.text()).toBe("token-b"); }); }); describe("askpass script generation", () => { it("writes an executable script file", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code = server.register("ghs_script_test"); const scriptPath = server.writeAskpassScript(code); expect(existsSync(scriptPath)).toBe(true); expect(scriptPath.startsWith(tmp)).toBe(true); const content = readFileSync(scriptPath, "utf-8"); expect(content).toContain("#!/usr/bin/env node"); expect(content).toContain(String(server.port)); expect(content).toContain(code); // token should NOT be in the script — only port and code expect(content).not.toContain("ghs_script_test"); }); it("script handles Username prompt locally (no server call)", async () => { const tmp = makeTmpdir(); server = await startGitAuthServer(tmp); const code = server.register("ghs_username_test"); const scriptPath = server.writeAskpassScript(code); const content = readFileSync(scriptPath, "utf-8"); // script checks for /^Username/i and returns "x-access-token" without HTTP expect(content).toContain("Username"); expect(content).toContain("x-access-token"); }); });