clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

feat/ui-updates
tinyrelay / scripts / conformance / grasp.test.ts
grasp.test.ts 5.1K
1// GRASP-01 conformance uses the stock Git client against the public Smart HTTP door.2import { execFile } from "node:child_process";3import { mkdtemp, writeFile, readFile, rm } from "node:fs/promises";4import { tmpdir } from "node:os";5import { promisify } from "node:util";6import { describe, expect, it } from "vitest";7import { finalizeEvent, getPublicKey, generateSecretKey } from "nostr-tools/pure";8import { Client, HTTP_URL, RELAY_URL, sleep, now } from "./helpers.ts";9import { npubEncode } from "nostr-tools/nip19";1011const exec = promisify(execFile);12const KIND_REPO = 30617;13const KIND_REPO_STATE = 30618;1415async function git(cwd: string, ...args: string[]): Promise<string> {16 const result = await exec("git", args, { cwd, env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } });17 return result.stdout.trim();18}1920function repositoryPath(npub: string, identifier: string): string {21 return `/${npub}/${encodeURIComponent(identifier)}.git`;22}2324async function waitForRefs(url: string): Promise<void> {25 for (let attempt = 0; attempt < 60; attempt++) {26 const response = await fetch(`${url}/info/refs?service=git-upload-pack`);27 const text = await response.text();28 if (response.status === 200 && text.includes("# service=git-upload-pack")) return;29 await sleep(250);30 }31 throw new Error("GRASP repository did not become available");32}3334async function publish(c: Client, sk: Uint8Array, kind: number, tags: string[][], createdAt: number): Promise<void> {35 const event = finalizeEvent({ kind, content: "", tags, created_at: createdAt }, sk);36 await c.publish(event);37}3839describe("GRASP-01 stock Git", () => {40 it("clones, pushes an accepted state, fetches incrementally, and supports partial clones", async ({ skip }) => {41 const infoResponse = await fetch(HTTP_URL, { headers: { accept: "application/nostr+json" } });42 const info: any = await infoResponse.json();43 if (!info.supported_grasps?.includes("GRASP-01")) skip();4445 const root = await mkdtemp(`${tmpdir()}/tiny-grasp-`);46 const clone = await mkdtemp(`${tmpdir()}/tiny-grasp-clone-`);47 const partial = await mkdtemp(`${tmpdir()}/tiny-grasp-partial-`);48 const sk = generateSecretKey();49 const npub = npubEncode(getPublicKey(sk));50 const identifier = `stock-${Math.random().toString(36).slice(2, 10)}`;51 const path = repositoryPath(npub, identifier);52 const repoURL = `${HTTP_URL}${path}`;53 const relayURL = RELAY_URL;54 const createdAt = now();55 const c = await Client.connect(RELAY_URL);56 try {57 await git(root, "init", "--initial-branch=main");58 await git(root, "config", "user.email", "grasp-conformance@example.com");59 await git(root, "config", "user.name", "GRASP conformance");60 await writeFile(`${root}/README.md`, "initial GRASP repository\n");61 await git(root, "add", "README.md");62 await git(root, "commit", "-m", "initial");63 const first = await git(root, "rev-parse", "HEAD");64 await git(root, "tag", "-a", "v1", "-m", "first release");65 const tag = await git(root, "rev-parse", "refs/tags/v1^{tag}");6667 await publish(c, sk, KIND_REPO, [68 ["d", identifier],69 ["clone", repoURL],70 ["relays", relayURL],71 ["maintainers", getPublicKey(sk)],72 ], createdAt);73 await publish(c, sk, KIND_REPO_STATE, [74 ["d", identifier],75 ["HEAD", "ref: refs/heads/main"],76 ["refs/heads/main", first],77 ["refs/tags/v1", tag],78 ], createdAt + 1);7980 await waitForRefs(repoURL);81 await git(root, "push", repoURL, "HEAD:refs/heads/main", "refs/tags/v1:refs/tags/v1");82 await git(clone, "clone", repoURL, ".");83 expect(await git(clone, "rev-parse", "HEAD")).toBe(first);84 expect(await git(clone, "describe", "--tags")).toBe("v1");85 expect((await git(clone, "fsck", "--full"))).not.toContain("dangling");8687 await writeFile(`${root}/README.md`, "incremental GRASP repository\n");88 await git(root, "add", "README.md");89 await git(root, "commit", "-m", "incremental");90 const second = await git(root, "rev-parse", "HEAD");91 await publish(c, sk, KIND_REPO_STATE, [92 ["d", identifier],93 ["HEAD", "ref: refs/heads/main"],94 ["refs/heads/main", second],95 ["refs/tags/v1", tag],96 ], createdAt + 2);97 await git(root, "push", repoURL, "HEAD:refs/heads/main");98 await git(clone, "fetch", repoURL, "refs/heads/main:refs/remotes/origin/main");99 expect(await git(clone, "rev-parse", "refs/remotes/origin/main")).toBe(second);100101 await git(partial, "clone", "--filter=blob:none", "--no-checkout", repoURL, ".");102 await git(partial, "fsck", "--full");103 await git(partial, "checkout", "main");104 expect(await readFile(`${partial}/README.md`, "utf8")).toBe("incremental GRASP repository\n");105 } finally {106 await publish(c, sk, 5, [["a", `${KIND_REPO}:${getPublicKey(sk)}:${identifier}`], ["a", `${KIND_REPO_STATE}:${getPublicKey(sk)}:${identifier}`]], createdAt + 3).catch(() => {});107 c.close();108 await Promise.all([rm(root, { recursive: true, force: true }), rm(clone, { recursive: true, force: true }), rm(partial, { recursive: true, force: true })]);109 }110 }, 120_000);111});