Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244import { describe, expect, it, vi } from "vitest";import { ClientResponseError } from "#lib/api/client.ts";import { classifyRepoAvailability, withAuthorHandles, withImportAvailability, type RepoAvailability} from "#lib/api/repoIndex.ts";import type { ImportState } from "#lib/api/repo.ts";import { sshFingerprint } from "#lib/api/settings.ts";import type { CommitSummary } from "#lib/components/repo/types.ts";import type { Fetch } from "#lib/fetch.ts";
const notSynced = () => new ClientResponseError({ status: 404, data: { error: "RepoNotFound" } });
describe("classifyRepoAvailability", () => { it("recognizes a repo the mirror has not synced yet", () => { const result = classifyRepoAvailability( [notSynced(), notSynced(), notSynced()].map((cause) => ({ value: null, error: cause })) ); expect(result).toBe("not-synced"); });
it("does not treat other 404s as not synced", () => { const result = classifyRepoAvailability( ["tree", "log", "branches"].map(() => ({ value: null, error: new ClientResponseError({ status: 404, data: { error: "XRPCNotSupported" } }) })) ); expect(result).toBe("error"); });
it("keeps transport failures as errors", () => { const result = classifyRepoAvailability( ["tree", "log", "branches"].map(() => ({ value: null, error: new TypeError("offline") })) ); expect(result).toBe("error"); });
it("does not mistake a missing ref for an unsynced repo", () => { const result = classifyRepoAvailability([ { value: null, error: notSynced() }, { value: null, error: notSynced() }, { value: { branches: [] }, error: null } ]); expect(result).toBe("ok"); });});
describe("withImportAvailability", () => { it.each([ ["ok", { content: "fetching" as const }, "ok"], ["ok", { content: "partial" as const }, "ok"], ["ok", { content: "failed" as const }, "ok"], ["not-synced", null, "not-synced"], ["not-synced", { content: "fetching" as const }, "importing"], ["not-synced", { content: "partial" as const }, "importing"], ["not-synced", { content: "failed" as const }, "import-failed"], ["empty", null, "empty"], ["error", null, "error"], ["empty", { content: "failed" as const }, "import-failed"], ["error", { content: "failed" as const }, "import-failed"], ["empty", { content: "fetching" as const }, "importing"], ["empty", { content: "partial" as const }, "importing"], ["error", { content: "fetching" as const }, "importing"], ["error", { content: "partial" as const }, "importing"] ] satisfies [RepoAvailability, ImportState | null, RepoAvailability][])( "maps (%s, %j) -> %s", (availability, status, expected) => { expect(withImportAvailability(availability, status)).toBe(expected); } );});
const commit = (c: Partial<CommitSummary>): CommitSummary => ({ hash: "h", shortHash: "h", subject: "s", body: "", authorName: "", authorEmail: "", when: "2026-01-01T00:00:00Z", ...c});
// dispatches the two upstreams withAuthorHandles touches: the internal kv// route (relative) and bobbin's resolveMiniDoc.const fetchMock = vi.fn(async (input: string | URL, _init?: RequestInit) => { const url = String(input); if (url.startsWith("/_internal/email-did")) { return new Response( JSON.stringify({ "alice@example.com": "did:plc:alice", "bob@example.com": "did:plc:bob", "MIXED@Example.com": "did:plc:mixed", "ghost@example.com": null }) ); } if (url.includes("blue.microcosm.identity.resolveMiniDoc")) { const did = new URL(url).searchParams.get("identifier") ?? ""; return new Response(JSON.stringify({ did, handle: `${did.replace("did:plc:", "")}.test` })); } return new Response("unexpected", { status: 404 });});
describe("withAuthorHandles", () => { it("resolves verified emails to a did before hydrating handles", async () => { const commits = [ commit({ authorName: "Alice", authorEmail: "alice@example.com" }), commit({ authorName: "Signed", authorEmail: "did:plc:signed", authorDid: "did:plc:signed" }) ]; const out = await withAuthorHandles(commits, "https://bobbin.test", fetchMock as Fetch); expect(out[0]).toMatchObject({ authorDid: "did:plc:alice", authorHandle: "alice.test" }); expect(out[1]).toMatchObject({ authorDid: "did:plc:signed", authorHandle: "signed.test" }); });
it("keeps the requested email case when looking up the kv result", async () => { const out = await withAuthorHandles( [commit({ authorEmail: "MIXED@Example.com" })], "https://bobbin.test", fetchMock as Fetch ); expect(out[0]).toMatchObject({ authorDid: "did:plc:mixed", authorHandle: "mixed.test" }); });
it("leaves unresolved emails as plain authors", async () => { const out = await withAuthorHandles( [commit({ authorEmail: "ghost@example.com" })], "https://bobbin.test", fetchMock as Fetch ); expect(out[0].authorDid).toBeUndefined(); expect(out[0].authorHandle).toBeUndefined(); });
it("fails open when the kv route errors", async () => { const broken = vi.fn(async () => new Response("nope", { status: 500 })); const out = await withAuthorHandles( [commit({ authorEmail: "alice@example.com" })], "https://bobbin.test", broken as Fetch ); expect(out[0].authorDid).toBeUndefined(); });
it("keeps a did from the email pass when handle resolution fails", async () => { const brokenBobbin = vi.fn(async (input: string | URL) => { if (String(input).startsWith("/_internal/email-did")) { return new Response(JSON.stringify({ "alice@example.com": "did:plc:alice" })); } return new Response("nope", { status: 500 }); }); const out = await withAuthorHandles( [commit({ authorEmail: "alice@example.com" })], "https://bobbin.test", brokenBobbin as Fetch ); expect(out[0]).toMatchObject({ authorDid: "did:plc:alice" }); expect(out[0].authorHandle).toBeUndefined(); });});
describe("withAuthorHandles signatures", () => { const signKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHEisipfUi+zRyW1reo72Rt4+EUbM5mKZrI9/PkyHMpR"; const authKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIBKTt0Ub2mIVQbZG0DL1fB6vk1PB8iI5bmNTe7IfBnMO";
const keysFetch = vi.fn(async (input: string | URL, init?: RequestInit) => { if (!String(input).includes("org.tangled.publicKey.listKeys")) return fetchMock(input, init); const subject = new URL(String(input)).searchParams.get("subject"); const items = subject === "did:plc:alice" ? [ { value: { publicKey: signKey } }, { value: { publicKey: authKey, usages: ["auth"] } } ].map((item, i) => ({ uri: `at://${subject}/org.tangled.key.sshKey/${i}`, ...item })) : []; return new Response(JSON.stringify({ items })); });
const verifiedFor = async ( committerEmail: string, key: string | null, authorEmail = "bob@example.com" ) => { const [out] = await withAuthorHandles( [commit({ authorEmail, committerEmail, signatureFingerprint: key ?? undefined })], "https://bobbin.test", keysFetch as Fetch ); return out; };
it("verifies a signature made with one of the committer's signing keys", async () => { const out = await verifiedFor("alice@example.com", await sshFingerprint(signKey)); expect(out).toMatchObject({ authorDid: "did:plc:bob", committerDid: "did:plc:alice", committerHandle: "alice.test", signature: "verified" }); });
it("still maps the author when the signature does not verify", async () => { for (const key of ["SHA256:someoneelse", await sshFingerprint(authKey)]) { const out = await verifiedFor("alice@example.com", key); expect(out).toMatchObject({ authorDid: "did:plc:bob", committerDid: "did:plc:alice", signature: "unverified" }); } const notTheirs = await verifiedFor("bob@example.com", await sshFingerprint(signKey)); expect(notTheirs.signature).toBe("unverified"); });
it("leaves unsigned commits without a signature state", async () => { const out = await verifiedFor("alice@example.com", null); expect(out).toMatchObject({ authorDid: "did:plc:bob", committerDid: "did:plc:alice", committerHandle: "alice.test" }); expect(out.signature).toBeUndefined(); });});