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.

132 lines · 6.6 KBRaw History
1import { chmodSync, mkdirSync, readdirSync, statSync, writeFileSync, readFileSync } from "node:fs";
2import path from "node:path";
3import { afterEach, beforeEach, describe, expect, it } from "vitest";
4import { configPath, normalizeUrl, resolveConfig, writeConfig } from "../src/config.js";
5import type { Io } from "../src/io.js";
6import { fakeServer, isolatedEnv, run, type FakeServer } from "./helpers.js";
7
8const ioWith = (env: NodeJS.ProcessEnv) => ({ env }) as unknown as Io;
9
10describe("config", () => {
11 it("defaults to https://diffinery.com with no token", async () => {
12 const cfg = await resolveConfig(ioWith(isolatedEnv()));
13 expect(cfg).toMatchObject({ url: "https://diffinery.com", token: null, urlSource: "default", tokenSource: null });
14 });
15
16 it("reads the file under XDG_CONFIG_HOME and lets env vars override it", async () => {
17 const env = isolatedEnv();
18 const file = path.join(env.XDG_CONFIG_HOME!, "diffinery", "config.json");
19 expect(configPath(env)).toBe(file);
20 mkdirSync(path.dirname(file), { recursive: true });
21 writeFileSync(file, JSON.stringify({ url: "https://forge.example.com/", token: "dfy_file" }));
22
23 expect(await resolveConfig(ioWith(env))).toMatchObject({ url: "https://forge.example.com", token: "dfy_file", urlSource: "config", tokenSource: "config" });
24 expect(await resolveConfig(ioWith({ ...env, DIFFINERY_TOKEN: "dfy_env" }))).toMatchObject({ url: "https://forge.example.com", token: "dfy_env", tokenSource: "env" });
25 expect(await resolveConfig(ioWith({ ...env, DIFFINERY_URL: "other.example.com" }))).toMatchObject({ url: "https://other.example.com", token: "dfy_file", urlSource: "env" });
26 });
27
28 it("falls back to ~/.config when XDG_CONFIG_HOME is unset", () => {
29 const env = isolatedEnv();
30 delete env.XDG_CONFIG_HOME;
31 expect(configPath(env)).toBe(path.join(env.HOME!, ".config", "diffinery", "config.json"));
32 });
33
34 it("writes the config with mode 0600", async () => {
35 const env = isolatedEnv();
36 const file = await writeConfig(env, { url: "https://a.example", token: "dfy_x" });
37 expect(statSync(file).mode & 0o777).toBe(0o600);
38 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: "https://a.example", token: "dfy_x" });
39 });
40
41 it("tightens an existing loose config directory and file, and leaves no temporary file", async () => {
42 const env = isolatedEnv();
43 const file = configPath(env);
44 mkdirSync(path.dirname(file), { recursive: true, mode: 0o755 });
45 chmodSync(path.dirname(file), 0o755);
46 writeFileSync(file, "{}", { mode: 0o644 });
47 chmodSync(file, 0o644);
48 await writeConfig(env, { url: "https://a.example", token: "dfy_y" });
49 expect(statSync(path.dirname(file)).mode & 0o777).toBe(0o700);
50 expect(statSync(file).mode & 0o777).toBe(0o600);
51 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: "https://a.example", token: "dfy_y" });
52 expect(readdirSync(path.dirname(file))).toEqual(["config.json"]);
53 });
54
55 it("normalizes URLs", () => {
56 expect(normalizeUrl("forge.example.com/")).toBe("https://forge.example.com");
57 expect(normalizeUrl("http://localhost:8787")).toBe("http://localhost:8787");
58 expect(normalizeUrl("http://127.0.0.1:8787")).toBe("http://127.0.0.1:8787");
59 expect(normalizeUrl("http://[::1]:8787")).toBe("http://[::1]:8787");
60 expect(normalizeUrl("http://dev.localhost")).toBe("http://dev.localhost");
61 expect(() => normalizeUrl("ftp://x")).toThrow();
62 });
63
64 it("refuses plain http anywhere but this machine, since tokens go with every request", () => {
65 expect(() => normalizeUrl("http://forge.example.com")).toThrow(/unencrypted/);
66 expect(() => normalizeUrl("http://10.0.0.5:8787")).toThrow(/unencrypted/);
67 expect(() => normalizeUrl("http://localhost.example.com")).toThrow(/unencrypted/);
68 });
69});
70
71describe("login / whoami / logout", () => {
72 let srv: FakeServer;
73 beforeEach(async () => {
74 srv = await fakeServer();
75 srv.on("GET", "/api/v3/user", (r) => (r.headers.authorization === "Bearer dfy_good" ? { body: { login: "alice", name: "Alice", type: "User" } } : { status: 401, body: { message: "bad credentials" } }));
76 });
77 afterEach(() => srv.close());
78
79 it("verifies the token, saves it 0600, and whoami uses it", async () => {
80 const env = isolatedEnv();
81 const r = await run(["login", "--url", srv.url, "--token", "dfy_good"], { env });
82 expect(r.code).toBe(0);
83 expect(r.stdout).toContain("Logged in to");
84 expect(r.stdout).toContain("alice");
85 expect(r.stdout).not.toContain("dfy_good");
86 const file = configPath(env);
87 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: srv.url, token: "dfy_good" });
88 expect(statSync(file).mode & 0o777).toBe(0o600);
89
90 const who = await run(["whoami"], { env });
91 expect(who.code).toBe(0);
92 expect(who.stdout).toContain("alice");
93 expect(srv.find("GET", "/api/v3/user").at(-1)!.headers.authorization).toBe("Bearer dfy_good");
94
95 const out = await run(["logout"], { env });
96 expect(out.code).toBe(0);
97 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: srv.url });
98 expect((await run(["whoami"], { env })).code).toBe(1);
99 });
100
101 it("reads the token from stdin when it is not a TTY", async () => {
102 const env = isolatedEnv();
103 const r = await run(["login", "--url", srv.url], { env, stdin: "dfy_good\n" });
104 expect(r.code).toBe(0);
105 expect(JSON.parse(readFileSync(configPath(env), "utf8")).token).toBe("dfy_good");
106 });
107
108 it("refuses a token the server rejects and saves nothing", async () => {
109 const env = isolatedEnv();
110 const r = await run(["login", "--url", srv.url, "--token", "dfy_bad"], { env });
111 expect(r.code).toBe(1);
112 expect(r.stderr).toContain("bad credentials");
113 expect(() => readFileSync(configPath(env))).toThrow();
114 });
115
116 it("DIFFINERY_TOKEN/DIFFINERY_URL override the saved config", async () => {
117 const env = isolatedEnv();
118 await writeConfig(env, { url: "http://127.0.0.1:1", token: "dfy_saved" });
119 const r = await run(["whoami", "--json"], { env: { ...env, DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_good" } });
120 expect(r.code).toBe(0);
121 expect(JSON.parse(r.stdout).login).toBe("alice");
122 });
123
124 it("mcp-config prints a snippet without the token unless asked", async () => {
125 const env = isolatedEnv({ DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_good" });
126 const r = await run(["mcp-config"], { env });
127 const cfg = JSON.parse(r.stdout);
128 expect(cfg.mcpServers.diffinery).toEqual({ type: "http", url: `${srv.url}/mcp`, headers: { Authorization: "Bearer ${DIFFINERY_TOKEN}" } });
129 const shown = JSON.parse((await run(["mcp-config", "--show-token"], { env })).stdout);
130 expect(shown.mcpServers.diffinery.headers.Authorization).toBe("Bearer dfy_good");
131 });
132});