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.

109 lines · 5.3 KBRaw History
1import { mkdirSync, 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("normalizes URLs", () => {
42 expect(normalizeUrl("forge.example.com/")).toBe("https://forge.example.com");
43 expect(normalizeUrl("http://localhost:8787")).toBe("http://localhost:8787");
44 expect(() => normalizeUrl("ftp://x")).toThrow();
45 });
46});
47
48describe("login / whoami / logout", () => {
49 let srv: FakeServer;
50 beforeEach(async () => {
51 srv = await fakeServer();
52 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" } }));
53 });
54 afterEach(() => srv.close());
55
56 it("verifies the token, saves it 0600, and whoami uses it", async () => {
57 const env = isolatedEnv();
58 const r = await run(["login", "--url", srv.url, "--token", "dfy_good"], { env });
59 expect(r.code).toBe(0);
60 expect(r.stdout).toContain("Logged in to");
61 expect(r.stdout).toContain("alice");
62 expect(r.stdout).not.toContain("dfy_good");
63 const file = configPath(env);
64 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: srv.url, token: "dfy_good" });
65 expect(statSync(file).mode & 0o777).toBe(0o600);
66
67 const who = await run(["whoami"], { env });
68 expect(who.code).toBe(0);
69 expect(who.stdout).toContain("alice");
70 expect(srv.find("GET", "/api/v3/user").at(-1)!.headers.authorization).toBe("Bearer dfy_good");
71
72 const out = await run(["logout"], { env });
73 expect(out.code).toBe(0);
74 expect(JSON.parse(readFileSync(file, "utf8"))).toEqual({ url: srv.url });
75 expect((await run(["whoami"], { env })).code).toBe(1);
76 });
77
78 it("reads the token from stdin when it is not a TTY", async () => {
79 const env = isolatedEnv();
80 const r = await run(["login", "--url", srv.url], { env, stdin: "dfy_good\n" });
81 expect(r.code).toBe(0);
82 expect(JSON.parse(readFileSync(configPath(env), "utf8")).token).toBe("dfy_good");
83 });
84
85 it("refuses a token the server rejects and saves nothing", async () => {
86 const env = isolatedEnv();
87 const r = await run(["login", "--url", srv.url, "--token", "dfy_bad"], { env });
88 expect(r.code).toBe(1);
89 expect(r.stderr).toContain("bad credentials");
90 expect(() => readFileSync(configPath(env))).toThrow();
91 });
92
93 it("DIFFINERY_TOKEN/DIFFINERY_URL override the saved config", async () => {
94 const env = isolatedEnv();
95 await writeConfig(env, { url: "http://127.0.0.1:1", token: "dfy_saved" });
96 const r = await run(["whoami", "--json"], { env: { ...env, DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_good" } });
97 expect(r.code).toBe(0);
98 expect(JSON.parse(r.stdout).login).toBe("alice");
99 });
100
101 it("mcp-config prints a snippet without the token unless asked", async () => {
102 const env = isolatedEnv({ DIFFINERY_URL: srv.url, DIFFINERY_TOKEN: "dfy_good" });
103 const r = await run(["mcp-config"], { env });
104 const cfg = JSON.parse(r.stdout);
105 expect(cfg.mcpServers.diffinery).toEqual({ type: "http", url: `${srv.url}/mcp`, headers: { Authorization: "Bearer ${DIFFINERY_TOKEN}" } });
106 const shown = JSON.parse((await run(["mcp-config", "--show-token"], { env })).stdout);
107 expect(shown.mcpServers.diffinery.headers.Authorization).toBe("Bearer dfy_good");
108 });
109});