This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / api / gitclient.test.ts
7.1 kB 193 lines
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});