This repository has no description
1import { describe, expect, it, vi } from "vitest";
2import {
3 blobRawUrl,
4 branches,
5 gitTarget,
6 log,
7 resolveDefaultBranch,
8 tag,
9 tags,
10 tree
11} from "./gitclient";
12
13const config = { bobbinUrl: "https://bobbin.example", knotMirrorUrl: "https://km.example" };
14const repo = {
15 uri: "at://did:plc:owner/sh.tangled.repo/core",
16 repoDid: "did:plc:reporepo"
17};
18
19const jsonResponse = (body: unknown): Response =>
20 new Response(JSON.stringify(body), {
21 status: 200,
22 headers: { "content-type": "application/json" }
23 });
24
25describe("gitTarget", () => {
26 it("routes git ops to the knot mirror keyed by repo DID when set", () => {
27 const target = gitTarget(config, repo, globalThis.fetch);
28 expect(target.ctx.serviceUrl).toBe("https://km.example");
29 expect(target.repo).toBe("did:plc:reporepo");
30 expect(target.via).toBe("mirror");
31 });
32
33 it("uses bobbin keyed by at-uri without a mirror", () => {
34 const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, globalThis.fetch);
35 expect(target.ctx.serviceUrl).toBe("https://bobbin.example");
36 expect(target.repo).toBe(repo.uri);
37 expect(target.via).toBe("bobbin");
38 });
39
40 it("falls back to bobbin when the repo DID is unknown", () => {
41 const target = gitTarget(config, { uri: repo.uri }, globalThis.fetch);
42 expect(target.ctx.serviceUrl).toBe("https://bobbin.example");
43 expect(target.repo).toBe(repo.uri);
44 expect(target.via).toBe("bobbin");
45 });
46});
47
48describe("resolveDefaultBranch", () => {
49 it("reads the is_default flag out of the mirror's branch list", async () => {
50 const fetchMock = vi.fn<typeof globalThis.fetch>().mockResolvedValue(
51 jsonResponse({
52 branches: [
53 { reference: { name: "sv-fe", hash: "aaa" } },
54 { reference: { name: "master", hash: "bbb" }, is_default: true }
55 ]
56 })
57 );
58 const target = gitTarget(config, repo, fetchMock);
59
60 await expect(resolveDefaultBranch(target)).resolves.toBe("master");
61
62 const url = new URL(String(fetchMock.mock.calls[0][0]));
63 expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches");
64 expect(url.searchParams.get("repo")).toBe(repo.repoDid);
65 });
66
67 it("asks the knot endpoint directly on the bobbin path", async () => {
68 const fetchMock = vi
69 .fn<typeof globalThis.fetch>()
70 .mockResolvedValue(jsonResponse({ name: "main", hash: "ccc", when: "" }));
71 const target = gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock);
72
73 await expect(resolveDefaultBranch(target)).resolves.toBe("main");
74
75 const url = new URL(String(fetchMock.mock.calls[0][0]));
76 expect(url.pathname).toBe("/xrpc/sh.tangled.repo.getDefaultBranch");
77 });
78
79 it("returns null instead of throwing when the backend is down", async () => {
80 const fetchMock = vi
81 .fn<typeof globalThis.fetch>()
82 .mockRejectedValue(new TypeError("fetch failed"));
83 const target = gitTarget(config, repo, fetchMock);
84
85 await expect(resolveDefaultBranch(target)).resolves.toBeNull();
86 });
87});
88
89describe("endpoint routing", () => {
90 const mirrorTarget = (fetchMock: typeof globalThis.fetch) => gitTarget(config, repo, fetchMock);
91 const bobbinTarget = (fetchMock: typeof globalThis.fetch) =>
92 gitTarget({ ...config, knotMirrorUrl: "" }, repo, fetchMock);
93 const calledUrl = (fetchMock: ReturnType<typeof vi.fn<typeof globalThis.fetch>>, call = 0) =>
94 new URL(String(fetchMock.mock.calls[call][0]));
95
96 it("sends log to the mirror's listCommits keyed by DID, cursor intact", async () => {
97 const fetchMock = vi
98 .fn<typeof globalThis.fetch>()
99 .mockResolvedValue(jsonResponse({ commits: [] }));
100
101 await log(mirrorTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" });
102
103 const url = calledUrl(fetchMock);
104 expect(url.origin).toBe("https://km.example");
105 expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.listCommits");
106 expect(url.searchParams.get("repo")).toBe(repo.repoDid);
107 expect(url.searchParams.get("cursor")).toBe("abc");
108 });
109
110 it("sends log to bobbin's repo.log keyed by at-uri, cursor intact", async () => {
111 const fetchMock = vi
112 .fn<typeof globalThis.fetch>()
113 .mockResolvedValue(jsonResponse({ commits: [] }));
114
115 await log(bobbinTarget(fetchMock), { ref: "main", limit: 10, cursor: "abc" });
116
117 const url = calledUrl(fetchMock);
118 expect(url.origin).toBe("https://bobbin.example");
119 expect(url.pathname).toBe("/xrpc/sh.tangled.repo.log");
120 expect(url.searchParams.get("repo")).toBe(repo.uri);
121 expect(url.searchParams.get("cursor")).toBe("abc");
122 });
123
124 it("sends tree to the mirror's getTree with the path param", async () => {
125 const fetchMock = vi
126 .fn<typeof globalThis.fetch>()
127 .mockResolvedValue(jsonResponse({ files: [] }));
128
129 await tree(mirrorTarget(fetchMock), { ref: "main", path: "src/lib" });
130
131 const url = calledUrl(fetchMock);
132 expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getTree");
133 expect(url.searchParams.get("repo")).toBe(repo.repoDid);
134 expect(url.searchParams.get("path")).toBe("src/lib");
135 });
136
137 it("sends tree to bobbin's repo.tree", async () => {
138 const fetchMock = vi
139 .fn<typeof globalThis.fetch>()
140 .mockResolvedValue(jsonResponse({ files: [] }));
141
142 await tree(bobbinTarget(fetchMock), { ref: "main" });
143
144 const url = calledUrl(fetchMock);
145 expect(url.pathname).toBe("/xrpc/sh.tangled.repo.tree");
146 expect(url.searchParams.get("repo")).toBe(repo.uri);
147 });
148
149 it("sends branches and tags to the legacy list endpoints on the mirror", async () => {
150 const fetchMock = vi
151 .fn<typeof globalThis.fetch>()
152 .mockImplementation(() => Promise.resolve(jsonResponse({ branches: [], tags: [] })));
153 const target = mirrorTarget(fetchMock);
154
155 await branches(target, 5);
156 await tags(target, 5);
157
158 expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.listBranches");
159 expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.git.temp.listTags");
160 });
161
162 it("routes single-tag reads to getTag on the mirror and repo.tag on bobbin", async () => {
163 const fetchMock = vi
164 .fn<typeof globalThis.fetch>()
165 .mockImplementation(() => Promise.resolve(jsonResponse({ tag: undefined })));
166
167 await tag(mirrorTarget(fetchMock), "v1.0.0");
168 expect(calledUrl(fetchMock).pathname).toBe("/xrpc/sh.tangled.git.temp.getTag");
169
170 await tag(bobbinTarget(fetchMock), "v1.0.0");
171 expect(calledUrl(fetchMock, 1).pathname).toBe("/xrpc/sh.tangled.repo.tag");
172 });
173
174 it("builds the raw url against the mirror without a raw flag", () => {
175 const url = new URL(
176 blobRawUrl(mirrorTarget(globalThis.fetch), { ref: "main", path: "README.md" })
177 );
178 expect(url.origin).toBe("https://km.example");
179 expect(url.pathname).toBe("/xrpc/sh.tangled.git.temp.getBlob");
180 expect(url.searchParams.get("repo")).toBe(repo.repoDid);
181 expect(url.searchParams.has("raw")).toBe(false);
182 });
183
184 it("builds the raw url against bobbin with raw=true", () => {
185 const url = new URL(
186 blobRawUrl(bobbinTarget(globalThis.fetch), { ref: "main", path: "README.md" })
187 );
188 expect(url.origin).toBe("https://bobbin.example");
189 expect(url.pathname).toBe("/xrpc/sh.tangled.repo.blob");
190 expect(url.searchParams.get("repo")).toBe(repo.uri);
191 expect(url.searchParams.get("raw")).toBe("true");
192 });
193});