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.

96 lines · 5.2 KBRaw History
1import { createHash } from "node:crypto";
2import { describe, expect, it } from "vitest";
3import { authorizeUrl, clientId, compareVersions, credentialInput, dedupe, editorRedirect, matches, nextLink, normalizeServer, pkce, readCallback, repoNameFrom, repoRef, type RepoJson } from "../src/util";
4
5const S = "https://diffinery.com";
6const repo = (full: string, description: string | null = null): RepoJson => ({
7 full_name: full,
8 name: full.split("/")[1]!,
9 owner: { login: full.split("/")[0]! },
10 description,
11 private: true,
12 clone_url: `${S}/${full}.git`,
13 html_url: `${S}/${full}`,
14});
15
16describe("server and OAuth", () => {
17 it("normalizes the server setting", () => {
18 expect(normalizeServer(undefined)).toBe(S);
19 expect(normalizeServer(" https://diffinery.com/ ")).toBe(S);
20 expect(normalizeServer("diffinery.example.org")).toBe("https://diffinery.example.org");
21 expect(normalizeServer("http://localhost:8787")).toBe("http://localhost:8787");
22 expect(() => normalizeServer("http://diffinery.com")).toThrow(/https/);
23 expect(() => normalizeServer("https://")).toThrow(/valid URL/);
24 });
25
26 it("returns to the editor by its own scheme when the server knows it", () => {
27 expect(editorRedirect("vscode")).toBe("vscode://codician.diffinery/oauth");
28 expect(editorRedirect("cursor")).toBe("cursor://codician.diffinery/oauth");
29 expect(editorRedirect("some-fork")).toBeNull();
30 expect(clientId(S)).toBe("https://diffinery.com/oauth/clients/editor.json");
31 });
32
33 it("makes a PKCE pair and an authorization URL", () => {
34 const p = pkce();
35 expect(p.challenge).toBe(createHash("sha256").update(p.verifier).digest("base64url"));
36 const u = new URL(authorizeUrl(S, "cursor://codician.diffinery/oauth", p.challenge, p.state));
37 expect(u.origin + u.pathname).toBe(`${S}/oauth/authorize`);
38 expect(Object.fromEntries(u.searchParams)).toEqual({ response_type: "code", client_id: clientId(S), redirect_uri: "cursor://codician.diffinery/oauth", code_challenge: p.challenge, code_challenge_method: "S256", state: p.state, scope: "write" });
39 });
40
41 it("accepts only the callback of this sign-in, from this server", () => {
42 const expect_ = { state: "st", server: S };
43 expect(readCallback(`code=dfa_1&state=st&iss=${encodeURIComponent(S)}`, expect_)).toEqual({ code: "dfa_1" });
44 expect(readCallback("code=dfa_1&state=other", expect_)).toHaveProperty("error");
45 expect(readCallback("code=dfa_1&state=st&iss=https%3A%2F%2Fevil.example", expect_)).toEqual({ error: expect.stringMatching(/evil\.example/) });
46 expect(readCallback("error=access_denied&state=st", expect_)).toEqual({ error: "Sign-in was cancelled in the browser." });
47 expect(readCallback("state=st", expect_)).toHaveProperty("error");
48 });
49});
50
51describe("repositories", () => {
52 it("reads owner/repo from names and URLs on this server only", () => {
53 expect(repoRef(S, "codician/diffinery")).toEqual({ owner: "codician", repo: "diffinery" });
54 expect(repoRef(S, "https://diffinery.com/codician/diffinery.git")).toEqual({ owner: "codician", repo: "diffinery" });
55 expect(repoRef(S, "https://diffinery.com/codician/diffinery/tree/main/src")).toEqual({ owner: "codician", repo: "diffinery" });
56 expect(repoRef(S, "https://github.com/codician/diffinery")).toBeNull();
57 expect(repoRef(S, "diffinery")).toBeNull();
58 expect(repoRef(S, "a/b/c")).toBeNull();
59 expect(repoRef(S, "a b/c")).toBeNull();
60 expect(repoRef("http://localhost:8787", "http://localhost:8787/ada/x.git")).toEqual({ owner: "ada", repo: "x" });
61 });
62
63 it("matches every word, merges without duplicates", () => {
64 const a = repo("codician/diffinery", "A git forge for agents");
65 const b = repo("codician/tactfolio", "Backtesting");
66 expect(matches(a, "forge DIFF")).toBe(true);
67 expect(matches(b, "forge")).toBe(false);
68 expect(dedupe([a], [b, repo("Codician/Diffinery")]).map((r) => r.full_name)).toEqual(["codician/diffinery", "codician/tactfolio"]);
69 });
70
71 it("follows Link headers", () => {
72 expect(nextLink('<https://diffinery.com/api/v3/user/repos?page=2>; rel="next", <https://diffinery.com/api/v3/user/repos?page=5>; rel="last"')).toBe("https://diffinery.com/api/v3/user/repos?page=2");
73 expect(nextLink('<https://x/?page=1>; rel="prev"')).toBeNull();
74 expect(nextLink(null)).toBeNull();
75 });
76
77 it("names a repository after a folder", () => {
78 expect(repoNameFrom("My Project (old)")).toBe("My-Project-old");
79 expect(repoNameFrom("...")).toBe("repository");
80 });
81});
82
83describe("updates and Git", () => {
84 it("compares versions", () => {
85 expect(compareVersions("0.2.0", "0.1.9")).toBe(1);
86 expect(compareVersions("0.1.0", "0.1.0")).toBe(0);
87 expect(compareVersions("0.1.0", "0.1.10")).toBe(-1);
88 expect(compareVersions("1.0.0-beta", "1.0.0")).toBe(-1);
89 });
90
91 it("writes what git credential approve reads", () => {
92 expect(credentialInput(S, "ada", "dfy_x", 1_800_000_000_500)).toBe("protocol=https\nhost=diffinery.com\nusername=ada\npassword=dfy_x\npassword_expiry_utc=1800000000\n\n");
93 expect(credentialInput("http://localhost:8787", "ada", "t")).toBe("protocol=http\nhost=localhost:8787\nusername=ada\npassword=t\n\n");
94 expect(() => credentialInput(S, "ada\nhost=evil", "t")).toThrow();
95 });
96});