| 1 | import { execFileSync } from "node:child_process"; |
| 2 | import { chmodSync, readFileSync, writeFileSync } from "node:fs"; |
| 3 | import path from "node:path"; |
| 4 | import { describe, expect, it } from "vitest"; |
| 5 | import { parseCredentialInput } from "../src/commands/auth.js"; |
| 6 | import { isolatedEnv, run, tempDir } from "./helpers.js"; |
| 7 | |
| 8 | describe("git credential helper", () => { |
| 9 | const env = isolatedEnv({ DIFFINERY_URL: "https://forge.example.com", DIFFINERY_TOKEN: "dfy_secret" }); |
| 10 | |
| 11 | it("parses key=value lines up to the blank line", () => { |
| 12 | expect(parseCredentialInput("protocol=https\nhost=a.b\npath=x/y.git\n\nignored=1\n")).toEqual({ protocol: "https", host: "a.b", path: "x/y.git" }); |
| 13 | }); |
| 14 | |
| 15 | it("answers get for the configured host", async () => { |
| 16 | const r = await run(["auth", "git-credential", "get"], { env, stdin: "protocol=https\nhost=forge.example.com\n\n" }); |
| 17 | expect(r.code).toBe(0); |
| 18 | expect(r.stdout).toBe("protocol=https\nhost=forge.example.com\nusername=x-token\npassword=dfy_secret\n"); |
| 19 | }); |
| 20 | |
| 21 | it("stays silent for other hosts, other protocols and other operations", async () => { |
| 22 | for (const stdin of ["protocol=https\nhost=github.com\n\n", "protocol=http\nhost=forge.example.com\n\n"]) { |
| 23 | const r = await run(["auth", "git-credential", "get"], { env, stdin }); |
| 24 | expect(r.code).toBe(0); |
| 25 | expect(r.stdout).toBe(""); |
| 26 | } |
| 27 | for (const op of ["store", "erase"]) { |
| 28 | const r = await run(["auth", "git-credential", op], { env, stdin: "protocol=https\nhost=forge.example.com\nusername=x\npassword=y\n\n" }); |
| 29 | expect(r.code).toBe(0); |
| 30 | expect(r.stdout).toBe(""); |
| 31 | } |
| 32 | }); |
| 33 | |
| 34 | it("matches hosts with ports", async () => { |
| 35 | const e = { ...env, DIFFINERY_URL: "http://127.0.0.1:8787" }; |
| 36 | const r = await run(["auth", "git-credential", "get"], { env: e, stdin: "protocol=http\nhost=127.0.0.1:8787\n" }); |
| 37 | expect(r.stdout).toContain("password=dfy_secret"); |
| 38 | }); |
| 39 | |
| 40 | it("prints nothing without a token", async () => { |
| 41 | const e = { ...env }; |
| 42 | delete e.DIFFINERY_TOKEN; |
| 43 | const r = await run(["auth", "git-credential", "get"], { env: e, stdin: "protocol=https\nhost=forge.example.com\n" }); |
| 44 | expect(r.stdout).toBe(""); |
| 45 | }); |
| 46 | |
| 47 | it("auth setup-git configures a URL-scoped helper in the global git config", async () => { |
| 48 | const r = await run(["auth", "setup-git"], { env }); |
| 49 | expect(r.code).toBe(0); |
| 50 | const helpers = execFileSync("git", ["config", "--global", "--get-all", "credential.https://forge.example.com.helper"], { env }).toString().split("\n"); |
| 51 | expect(helpers.slice(0, 2)).toEqual(["", "!diffinery auth git-credential"]); |
| 52 | // Idempotent. |
| 53 | await run(["auth", "setup-git"], { env }); |
| 54 | expect(execFileSync("git", ["config", "--global", "--get-all", "credential.https://forge.example.com.helper"], { env }).toString().split("\n").slice(0, -1)).toEqual(["", "!diffinery auth git-credential"]); |
| 55 | }); |
| 56 | |
| 57 | it("auth setup-gh signs gh in to the host with the token on stdin, never in its arguments", async () => { |
| 58 | const bin = tempDir("diffinery-fake-gh-"); |
| 59 | const seen = path.join(bin, "seen"); |
| 60 | writeFileSync(path.join(bin, "gh"), `#!/bin/sh\nprintf '%s ' "$@" > "${seen}.args"\ncat > "${seen}.stdin"\n`); |
| 61 | chmodSync(path.join(bin, "gh"), 0o755); |
| 62 | const e = { ...env, PATH: `${bin}${path.delimiter}${env.PATH}` }; |
| 63 | const r = await run(["auth", "setup-gh"], { env: e }); |
| 64 | expect(r.code).toBe(0); |
| 65 | expect(r.stdout).toContain("gh is signed in to forge.example.com"); |
| 66 | expect(r.stdout).not.toContain("dfy_secret"); |
| 67 | expect(readFileSync(`${seen}.args`, "utf8").trim()).toBe("auth login --hostname forge.example.com --with-token --git-protocol https"); |
| 68 | expect(readFileSync(`${seen}.stdin`, "utf8")).toBe("dfy_secret\n"); |
| 69 | }); |
| 70 | |
| 71 | it("auth setup-gh refuses plain HTTP, which gh doesn't speak", async () => { |
| 72 | const r = await run(["auth", "setup-gh"], { env: { ...env, DIFFINERY_URL: "http://127.0.0.1:8787" } }); |
| 73 | expect(r.code).not.toBe(0); |
| 74 | expect(r.stderr).toContain("HTTPS"); |
| 75 | }); |
| 76 | }); |