import { describe, expect, it } from "vitest"; import { Identity, NostrSecret, newClaimPayload, signClaim, toMarkdown, } from "@kez/core"; import { GithubChannel, gistsUrl, looksLikeKezFilename, parseGistCandidates, profileReadmeUrls, } from "../src/github.js"; import { ChannelError } from "../src/index.js"; function sign(subject: string) { const secret = NostrSecret.generate(); return signClaim( newClaimPayload(Identity.parse(subject), secret.identity(), new Date()), secret, ); } /** Tiny in-memory router so we don't need nock — keeps the harness pure-Node. */ function makeFakeFetch(routes: Record): typeof fetch { return (async (url: string | URL) => { const key = url.toString(); const route = routes[key]; if (!route) { return new Response("", { status: 404 }); } return new Response(route.body, { status: route.status }); }) as unknown as typeof fetch; } describe("GithubChannel", () => { it("verifies a proof published in a gist", async () => { const signed = sign("github:jason"); const md = toMarkdown(signed); const apiBase = "https://api.test"; const rawBase = "https://raw.test"; const fetch = makeFakeFetch({ [`${apiBase}/users/jason/gists?per_page=100`]: { status: 200, body: JSON.stringify([ { files: { "notes.txt": { raw_url: `${rawBase}/raw/notes.txt` }, "github-jason.kez.md": { raw_url: `${rawBase}/raw/kez.md` }, }, }, ]), }, [`${rawBase}/raw/kez.md`]: { status: 200, body: md }, }); const channel = new GithubChannel({ apiBase, rawBase, fetch }); const hit = await channel.fetchAndVerify(Identity.parse("github:jason")); expect(hit.proof).toEqual(signed); }); it("falls back to profile README on main", async () => { const signed = sign("github:jason"); const md = toMarkdown(signed); const apiBase = "https://api.test"; const rawBase = "https://raw.test"; const fetch = makeFakeFetch({ [`${apiBase}/users/jason/gists?per_page=100`]: { status: 200, body: "[]" }, [`${rawBase}/jason/jason/main/README.md`]: { status: 200, body: md }, }); const channel = new GithubChannel({ apiBase, rawBase, fetch }); const hit = await channel.fetchAndVerify(Identity.parse("github:jason")); expect(hit.proof).toEqual(signed); }); it("falls back to master when main missing", async () => { const signed = sign("github:jason"); const md = toMarkdown(signed); const apiBase = "https://api.test"; const rawBase = "https://raw.test"; const fetch = makeFakeFetch({ [`${apiBase}/users/jason/gists?per_page=100`]: { status: 200, body: "[]" }, [`${rawBase}/jason/jason/master/README.md`]: { status: 200, body: md }, }); const channel = new GithubChannel({ apiBase, rawBase, fetch }); const hit = await channel.fetchAndVerify(Identity.parse("github:jason")); expect(hit.proof).toEqual(signed); }); it("rejects proof signed for wrong subject", async () => { const signed = sign("github:mallory"); const md = toMarkdown(signed); const apiBase = "https://api.test"; const rawBase = "https://raw.test"; const fetch = makeFakeFetch({ [`${apiBase}/users/jason/gists?per_page=100`]: { status: 200, body: JSON.stringify([ { files: { "kez.md": { raw_url: `${rawBase}/raw/kez.md` } } }, ]), }, [`${rawBase}/raw/kez.md`]: { status: 200, body: md }, }); const channel = new GithubChannel({ apiBase, rawBase, fetch }); await expect( channel.fetchAndVerify(Identity.parse("github:jason")), ).rejects.toMatchObject({ kind: "SubjectMismatch" }); }); it("returns NotFound when nothing matches", async () => { const apiBase = "https://api.test"; const rawBase = "https://raw.test"; const fetch = makeFakeFetch({ [`${apiBase}/users/jason/gists?per_page=100`]: { status: 200, body: "[]" }, }); const channel = new GithubChannel({ apiBase, rawBase, fetch }); await expect( channel.fetchAndVerify(Identity.parse("github:jason")), ).rejects.toBeInstanceOf(ChannelError); }); }); describe("github pure helpers", () => { it("looksLikeKezFilename accepts kez files", () => { expect(looksLikeKezFilename("github-jason.kez.md")).toBe(true); expect(looksLikeKezFilename("proof.kez")).toBe(true); expect(looksLikeKezFilename("KEZ-PROOF.txt")).toBe(true); expect(looksLikeKezFilename("README.md")).toBe(false); expect(looksLikeKezFilename(".gitignore")).toBe(false); }); it("gistsUrl includes user and pagination", () => { expect(gistsUrl("https://api.github.com", "jason")).toBe( "https://api.github.com/users/jason/gists?per_page=100", ); }); it("profileReadmeUrls tries main then master", () => { const urls = profileReadmeUrls("https://raw.githubusercontent.com", "jason"); expect(urls).toHaveLength(2); expect(urls[0]).toMatch(/main\/README\.md$/); expect(urls[1]).toMatch(/master\/README\.md$/); }); it("parseGistCandidates extracts kez raw_urls", () => { const body = [ { files: { "notes.txt": { raw_url: "https://example/notes" }, "github-jason.kez.md": { raw_url: "https://example/kez" }, }, }, ]; expect(parseGistCandidates(body)).toEqual(["https://example/kez"]); }); });