clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/github_actions/actions-9b61906d8b
bindws / test / object / git-storage.test.ts
git-storage.test.ts 5.9K
1// Owner-only Git storage reports describe the SQLite repository without2// opening the R2 bucket or granting a maintenance action.3import { env, runInDurableObject } from "cloudflare:test";4import { describe, expect, it } from "vitest";5import { generateSecretKey } from "nostr-tools/pure";6import { npubEncode } from "nostr-tools/nip19";7import { encodePack } from "ntig";8import { KIND_REPO } from "../../src/kinds.ts";9import { gitRepositoryPath } from "../../src/grasp-policy.ts";10import { repository } from "../../src/grasp-state.ts";11import { gitRepository } from "../../src/grasp.ts";12import type { Relay } from "../../src/relay.ts";13import { ev, nip98, pk, rpc } from "../helpers/relay.ts";14import { WS } from "../helpers/ws.ts";1516const encoder = new TextEncoder();17const objectID = async (type: string, bytes: Uint8Array) => {18 const head = encoder.encode(`${type} ${bytes.length}\0`);19 const input = new Uint8Array(head.length + bytes.length);20 input.set(head); input.set(bytes, head.length);21 return Array.from(new Uint8Array(await crypto.subtle.digest("SHA-1", input)), (x) => x.toString(16).padStart(2, "0")).join("");22};23async function tinyPack() {24 const blob = encoder.encode("inventory\n"), blobID = await objectID("blob", blob);25 const tree = new Uint8Array([...encoder.encode("100644 README\0"), ...Uint8Array.from(blobID.match(/../g)!, (x) => parseInt(x, 16))]);26 const treeID = await objectID("tree", tree);27 const commit = encoder.encode(`tree ${treeID}\nauthor inventory <inventory@example.com> 1 +0000\ncommitter inventory <inventory@example.com> 1 +0000\n\ninitial\n`);28 return { commitID: await objectID("commit", commit), pack: await encodePack([{ type: "blob", data: blob }, { type: "tree", data: tree }, { type: "commit", data: commit }]) };29}30async function seed(host: string) {31 const owner = generateSecretKey(), stranger = generateSecretKey(), moderator = generateSecretKey(), identifier = "inventory";32 await rpc(host, owner, "claim");33 await rpc(host, owner, "applypreset", "grasp");34 await rpc(host, owner, "setmember", pk(moderator), { role: "moderator" });35 const path = gitRepositoryPath(npubEncode(pk(owner)), identifier), c = await WS.connect(host);36 const announcement = ev(owner, KIND_REPO, "", [["d", identifier], ["clone", `https://${host}${path}`], ["relays", `wss://${host}`], ["maintainers", pk(owner)]]);37 expect((await c.ok(announcement)).ok).toBe(true); c.ws.close();38 const pack = await tinyPack();39 await runInDurableObject(env.RELAY.getByName(host.split(".")[0]), async (relay) => {40 const repo = repository(relay, pk(owner), identifier)!;41 await relay.repositoryAccess.run("control", async () => {42 const git = await gitRepository(relay, repo);43 await git.commit({ id: "inventory-initial", updates: [{ name: "refs/heads/main", old: null, new: pack.commitID }], pack: pack.pack });44 }, () => Promise.reject(new Error("repository access unexpectedly refused during fixture setup")));45 });46 return { owner, stranger, moderator, identifier };47}48async function scan(relay: Relay, host: string, owner: Uint8Array, identifier: string) {49 const url = `http://${host}/`, payload = { method: "gitstorage", params: [pk(owner), identifier] };50 const authorization = await nip98(owner, url, "POST", payload);51 return relay.fetch(new Request(url, { method: "POST", headers: { authorization, "content-type": "application/nostr+json+rpc" }, body: JSON.stringify(payload) }));52}5354describe("Git storage inventory management", () => {55 it("returns the SQLite object, ref and receipt totals only to the owner", async () => {56 const host = "git-storage-sql-owner.bind.ws", f = await seed(host);57 const report = await rpc(host, f.owner, "gitstorage", pk(f.owner), f.identifier);58 expect(report.status, JSON.stringify(report)).toBe(200);59 expect(report.result.backend).toBe("sqlite");60 expect(report.result.objects.count).toBeGreaterThanOrEqual(3);61 expect(report.result.objects.rawBytes).toBeGreaterThan(0);62 expect(report.result.objects.compressedBytes).toBeGreaterThan(0);63 expect(report.result.objects.metadataBytes).toBeGreaterThan(0);64 expect(report.result.refs).toBe(1);65 expect(report.result.receipts).toBe(1);66 expect(report.result.operations).toEqual({ gets: 0, lists: 0 });67 expect(report.result.physicalDatabaseBytes).toBeGreaterThan(0);68 expect(report.result.cooldownSeconds).toBe(60);69 expect((await rpc(host, null, "gitstorage", pk(f.owner), f.identifier)).status).toBe(401);70 expect((await rpc(host, f.stranger, "gitstorage", pk(f.owner), f.identifier)).status).toBe(403);71 expect((await rpc(host, f.moderator, "gitstorage", pk(f.owner), f.identifier)).status).toBe(403);72 expect((await rpc(host, f.owner, "gitstorage", "not-a-pubkey", f.identifier)).status).toBe(400);73 expect((await rpc(host, f.owner, "gitstorage", pk(f.owner), "")).status).toBe(400);74 expect((await rpc(host, f.owner, "gitstorage", pk(f.owner), "../data")).status).toBe(404);75 expect((await rpc(host, f.owner, "gitstorage", pk(f.owner), f.identifier)).status).toBe(429);76 });7778 it("does not use R2 for a successful SQLite report and releases its control scope", async () => {79 const host = "git-storage-sql-methods.bind.ws", f = await seed(host);80 const result = await runInDurableObject(env.RELAY.getByName(host.split(".")[0]), async (relay) => {81 const original = relay.media;82 Object.defineProperty(relay, "media", { value: new Proxy(original, { get() { throw new Error("SQLite inventory must not touch R2"); } }), configurable: true });83 try {84 const response = await scan(relay, host, f.owner, f.identifier);85 expect(response.status).toBe(200);86 const body = await response.json<any>();87 expect(body.result.operations).toEqual({ gets: 0, lists: 0 });88 expect(relay.repositoryAccess.busy).toBe(false);89 return body.result;90 } finally { Object.defineProperty(relay, "media", { value: original, configurable: true }); }91 });92 expect(result.backend).toBe("sqlite");93 });94});