Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

116 lines · 5.7 KBRaw History
1import { execFileSync } from "node:child_process";
2import { chmodSync, readFileSync, symlinkSync, writeFileSync } from "node:fs";
3import path from "node:path";
4import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5import { configPath, writeConfig } from "../src/config.js";
6import { fakeServer, isolatedEnv, run, tempDir, type FakeServer } from "./helpers.js";
7
8const helpers = (env: NodeJS.ProcessEnv, url: string) => execFileSync("git", ["config", "--global", "--get-all", `credential.${url}.helper`], { env }).toString().split("\n").slice(0, -1);
9
10/** A gh that records how it was called; `fail` makes `auth login` fail. */
11function fakeGh(fail = false): { dir: string; seen: string } {
12 const dir = tempDir("diffinery-fake-gh-");
13 const seen = path.join(dir, "seen");
14 writeFileSync(path.join(dir, "gh"), `#!/bin/sh\n[ "$1" = --version ] && exit 0\nprintf '%s ' "$@" > "${seen}.args"\ncat > "${seen}.stdin"\n${fail ? "echo 'gh: nope' >&2; exit 1\n" : ""}`);
15 chmodSync(path.join(dir, "gh"), 0o755);
16 return { dir, seen };
17}
18
19describe("diffinery setup", () => {
20 let srv: FakeServer;
21 beforeEach(async () => {
22 srv = await fakeServer();
23 srv.on("GET", "/api/v3/user", (r) => (r.headers.authorization === "Bearer dfy_good" ? { body: { login: "alice", type: "User" } } : { status: 401, body: { message: "bad credentials" } }));
24 srv.on("POST", "/-/cli/token", { body: { access_token: "dfy_good", token_type: "bearer", scope: "write", expires_at: "2027-01-12T00:00:00Z", login: "alice" } });
25 });
26 afterEach(() => {
27 vi.unstubAllGlobals();
28 return srv.close();
29 });
30
31 /** A browser where the person clicks Authorize. */
32 const approve = async (url: string) => {
33 const u = new URL(url);
34 await fetch(`http://127.0.0.1:${u.searchParams.get("port")}/callback?code=c&state=${u.searchParams.get("state")}`, { redirect: "manual" });
35 return true;
36 };
37
38 it("signs in through the browser, then sets up git, even when an agent runs it without a terminal", async () => {
39 const env = isolatedEnv();
40 let opened = 0;
41 const r = await run(["setup", "--url", srv.url], { env, stdinTTY: false, openUrl: (u) => (opened++, approve(u)) });
42 expect(r.stderr).not.toContain("error");
43 expect(r.code).toBe(0);
44 expect(opened).toBe(1);
45 expect(r.stdout).toContain(`Logged in to ${srv.url} as alice`);
46 expect(r.stdout).toContain(`git will use diffinery for ${srv.url}`);
47 expect(r.stdout).toContain(`${srv.url}/docs/connect`);
48 expect(r.stderr).toContain("Skipped gh");
49 expect(r.stdout + r.stderr).not.toContain("dfy_good");
50 expect(JSON.parse(readFileSync(configPath(env), "utf8"))).toEqual({ url: srv.url, token: "dfy_good" });
51 expect(helpers(env, srv.url)).toEqual(["", "!diffinery auth git-credential"]);
52 });
53
54 it("keeps a sign-in that still works, and is safe to run again", async () => {
55 const env = isolatedEnv();
56 await writeConfig(env, { url: srv.url, token: "dfy_good" });
57 for (let i = 0; i < 2; i++) {
58 const r = await run(["setup"], { env, openUrl: async () => expect.fail("opened a browser") });
59 expect(r.code).toBe(0);
60 expect(r.stdout).toContain(`Signed in to ${srv.url} as alice`);
61 }
62 expect(srv.find("POST", "/-/cli/token")).toHaveLength(0);
63 expect(helpers(env, srv.url)).toEqual(["", "!diffinery auth git-credential"]);
64 });
65
66 it("signs in again when the saved token no longer works", async () => {
67 const env = isolatedEnv();
68 await writeConfig(env, { url: srv.url, token: "dfy_expired" });
69 const r = await run(["setup"], { env, openUrl: approve });
70 expect(r.code).toBe(0);
71 expect(r.stdout).toContain("Logged in to");
72 expect(JSON.parse(readFileSync(configPath(env), "utf8")).token).toBe("dfy_good");
73 });
74
75 describe("with gh", () => {
76 const host = "https://forge.example.com";
77 // Requests to the HTTPS host reach the fake server, since gh only signs in to HTTPS hosts.
78 beforeEach(() => {
79 const real = fetch;
80 vi.stubGlobal("fetch", (input: string | URL | Request, init?: RequestInit) => real(String(input instanceof Request ? input.url : input).replace(host, srv.url), init));
81 });
82
83 it("signs gh in with the token on stdin", async () => {
84 const gh = fakeGh();
85 const env = isolatedEnv({ PATH: `${gh.dir}${path.delimiter}${process.env.PATH}` });
86 await writeConfig(env, { url: host, token: "dfy_good" });
87 const r = await run(["setup"], { env });
88 expect(r.code).toBe(0);
89 expect(r.stdout).toContain("gh is signed in to forge.example.com");
90 expect(readFileSync(`${gh.seen}.args`, "utf8").trim()).toBe("auth login --hostname forge.example.com --with-token --git-protocol https");
91 expect(readFileSync(`${gh.seen}.stdin`, "utf8")).toBe("dfy_good\n");
92 });
93
94 it("says when gh isn't installed", async () => {
95 // A PATH with git and nothing else, wherever gh lives on this machine.
96 const dir = tempDir("diffinery-no-gh-");
97 symlinkSync(execFileSync("sh", ["-c", "command -v git"]).toString().trim(), path.join(dir, "git"));
98 const env = isolatedEnv({ PATH: dir });
99 await writeConfig(env, { url: host, token: "dfy_good" });
100 const r = await run(["setup"], { env });
101 expect(r.code).toBe(0);
102 expect(r.stderr).toContain("Skipped gh, which isn't installed");
103 });
104
105 it("finishes the rest and exits 1 when gh fails", async () => {
106 const gh = fakeGh(true);
107 const env = isolatedEnv({ PATH: `${gh.dir}${path.delimiter}${process.env.PATH}` });
108 await writeConfig(env, { url: host, token: "dfy_good" });
109 const r = await run(["setup"], { env });
110 expect(r.code).toBe(1);
111 expect(r.stderr).toContain("gh auth login failed: gh: nope");
112 expect(r.stdout).toContain("git will use diffinery");
113 expect(r.stdout).toContain(`${host}/docs/connect`);
114 });
115 });
116});