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 / conformance / blossom.test.ts
blossom.test.ts 3.1K
1// Files, from outside: a Blossom upload, the download with and without a2// range, the listing and the delete. The one door that touches the media3// bucket, so a host whose bucket is not R2 (docs/16) is checked here.4import { describe, it, expect } from "vitest";5import { sha256 } from "@noble/hashes/sha2.js";6import { newEvent, newKey, pub, now, rand, hex, HTTP_URL } from "./helpers.ts";78// A Blossom token: kind 24242 with the verb and the hash, base64 in the header.9function token(sk: Uint8Array, verb: "upload" | "get" | "list" | "delete", sha?: string): string {10 const tags = [["t", verb], ...(sha ? [["x", sha]] : []), ["expiration", String(now() + 300)]];11 return "Nostr " + Buffer.from(JSON.stringify(newEvent(sk, 24242, verb, tags))).toString("base64");12}1314describe("Blossom media", () => {15 it("uploads a file, serves it whole and by range, lists it, and deletes it", async () => {16 const sk = newKey();17 const body = new TextEncoder().encode(`a small file ${rand()} with enough bytes for a range`);18 const sha = hex(sha256(body));1920 const put = await fetch(`${HTTP_URL}/upload`, { method: "PUT", headers: { authorization: token(sk, "upload", sha), "content-type": "text/plain" }, body });21 expect(put.status, await put.clone().text()).toBe(200);22 const desc: any = await put.json();23 expect([desc.sha256, desc.size, desc.type]).toEqual([sha, body.length, "text/plain"]);24 expect(desc.url).toMatch(new RegExp(`/${sha}\\.txt$`));2526 // The whole file, with its type and hash-derived etag.27 const get = await fetch(`${HTTP_URL}/${sha}.txt`);28 expect(get.status).toBe(200);29 expect(get.headers.get("content-type")).toBe("text/plain");30 expect(new Uint8Array(await get.arrayBuffer())).toEqual(body);3132 // A byte range: the second word.33 const part = await fetch(`${HTTP_URL}/${sha}`, { headers: { range: "bytes=2-6" } });34 expect(part.status).toBe(206);35 expect(part.headers.get("content-range")).toBe(`bytes 2-6/${body.length}`);36 expect(await part.text()).toBe("small");3738 // HEAD says it is there without a body.39 const head = await fetch(`${HTTP_URL}/${sha}`, { method: "HEAD" });40 expect(head.status).toBe(200);41 expect(await head.text()).toBe("");4243 // The uploader's listing carries it.44 const list = await fetch(`${HTTP_URL}/list/${pub(sk)}`, { headers: { authorization: token(sk, "list") } });45 expect(list.status).toBe(200);46 expect((await list.json<any>()).map((d: any) => d.sha256)).toContain(sha);4748 // The uploader deletes it; the same bytes can come back afterwards.49 const del = await fetch(`${HTTP_URL}/${sha}`, { method: "DELETE", headers: { authorization: token(sk, "delete", sha) } });50 expect(del.status).toBe(204);51 expect((await fetch(`${HTTP_URL}/${sha}`)).status).toBe(404);52 const again = await fetch(`${HTTP_URL}/upload`, { method: "PUT", headers: { authorization: token(sk, "upload", sha), "content-type": "text/plain" }, body });53 expect(again.status).toBe(200);54 expect((await fetch(`${HTTP_URL}/${sha}`)).status).toBe(200);55 await fetch(`${HTTP_URL}/${sha}`, { method: "DELETE", headers: { authorization: token(sk, "delete", sha) } });56 });57});