import { beforeAll, describe, expect, test } from "bun:test"; import { join } from "node:path"; import { BucketWriteError, openBucket } from "../src/bucket/reader.ts"; import { listRepos } from "../src/bucket/repo.ts"; import { Browser, type Instance, instance } from "./browser.ts"; import { makeRepos } from "./fixture.ts"; const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]); const MP4 = new Uint8Array([0, 0, 0, 0x18, ...new TextEncoder().encode("ftypisom"), 1, 2, 3, 4]); describe("editing a repo's description and details while signed in", () => { let inst: Instance; let browser: Browser; let bucketDir: string; const page = async (path: string) => (await inst.app.request(path)).text(); const save = (field: string, value: string) => browser.post("/admin/repos/app/text", { field, value }); const upload = (bytes: Uint8Array, name: string) => { const data = new FormData(); data.append("file", new File([bytes], name)); return browser.post("/admin/repos/app/media", data); }; beforeAll(async () => { const repos = await makeRepos(["app", "shy"]); bucketDir = repos.bucketDir; inst = await instance(bucketDir); inst.kepr.run("UPDATE repos SET public = 1 WHERE name = 'app'"); browser = new Browser(inst.app); }); test("signing in leaves a script-readable mark, the form token, beside the session", async () => { const response = await browser.signIn(); const cookies = response.headers.getSetCookie(); expect(cookies.find((line) => line.startsWith("kepr_editor="))).toMatch( /^kepr_editor=[0-9a-f]{64};/, ); expect(cookies.find((line) => line.startsWith("kepr_editor="))).not.toMatch(/HttpOnly/i); expect(cookies.find((line) => line.startsWith("kepr_session="))).toMatch(/HttpOnly/i); // The public page is the same for everyone, signed in or not. expect(await (await browser.request("/app")).text()).toBe(await page("/app")); }); test("starts from the README's words", async () => { const response = await browser.request("/admin/repos/app/text"); const texts = (await response.json()) as Record<"form" | "description" | "details", string>; expect(texts.description).toBe("The app project."); expect(texts.details).toContain("# app"); // The same token the console's forms carry, so the editor's posts pass the guard. expect(texts.form).toBe(browser.form); }); test("the operator's description replaces the README's on the page, in cards and previews", async () => { expect((await save("description", "My **own** words.")).status).toBe(204); const repo = await page("/app"); expect(repo).toContain('

My own words.

'); expect(repo).toContain(''); expect(await page("/browse")).toContain('

My own words.

'); }); test("details size and place pictures and videos from their options, and keep uploads' URLs", async () => { const details = [ '![shot](/static/assets/media/app/a.png "width=30 right")', '![demo](/static/assets/media/app/b.mp4 "width=50 left loop")', "![talk](/static/assets/media/app/c.webm)", '![plain](/pic.png "a real title")', ].join("\n\n"); expect((await save("details", details)).status).toBe(204); const repo = await page("/app"); expect(repo).toContain( 'shot', ); expect(repo).toContain( '', ); expect(repo).toContain( '', ); // A title that is not options stays a title, and a root path is still the repo's own file. expect(repo).toContain('plain'); expect(repo).not.toContain("# app"); }); test("the preview renders as the page will", async () => { const response = await browser.post("/admin/repos/app/preview", { field: "details", value: "**hi** ", }); expect(await response.text()).toBe( "

hi <script>x</script>

\n", ); }); test("clearing a text brings the README's back", async () => { await save("description", ""); await save("details", ""); const repo = await page("/app"); expect(repo).toContain("

The app project.

"); expect(repo).toContain("

app

"); }); test("refuses an unknown field, an overlong text and an unknown repo", async () => { expect((await save("title", "x")).status).toBe(400); expect((await save("description", "x".repeat(2001))).status).toBe(400); expect((await browser.post("/admin/repos/nope/text", { field: "details" })).status).toBe(404); }); test("uploads pictures and videos by what their bytes are, served as privately as the repo", async () => { const png = await upload(PNG, "shot.txt"); expect(png.status).toBe(200); const { url } = (await png.json()) as { url: string }; expect(url).toMatch(/^\/static\/assets\/media\/app\/[0-9a-f]{64}\.png$/); // Kept in the bucket, in Kepr's own folder, which no sync takes for a repo. const key = `.kepr/media/app/${url.split("/").pop()}`; expect(await Bun.file(join(bucketDir, key)).exists()).toBe(true); expect(await listRepos(openBucket({ kind: "folder", path: bucketDir }))).toEqual([ "app", "shy", ]); const served = await inst.app.request(url); expect([served.status, served.headers.get("content-type")]).toEqual([200, "image/png"]); const video = ((await (await upload(MP4, "demo.mp4")).json()) as { url: string }).url; expect(video).toEndWith(".mp4"); // Bun.serve answers Range for a file response, which Safari needs to play a video. const server = Bun.serve({ port: 0, fetch: (request) => inst.app.fetch(request) }); const range = await fetch(new URL(video, server.url), { headers: { range: "bytes=0-3" } }); expect([range.status, (await range.arrayBuffer()).byteLength]).toEqual([206, 4]); server.stop(); expect((await upload(new TextEncoder().encode(""), "a.svg")).status).toBe(400); inst.kepr.run("UPDATE repos SET public = 0 WHERE name = 'app'"); expect((await inst.app.request(url)).status).toBe(404); inst.kepr.run("UPDATE repos SET public = 1 WHERE name = 'app'"); }); test("signed out, none of it answers", async () => { const stranger = new Browser(inst.app); expect((await stranger.request("/admin/repos/app/text")).status).toBe(401); expect((await stranger.post("/admin/repos/app/media", new FormData())).status).toBe(401); }); }); test("Kepr writes only its own folder and builds, and to S3 only with the write key", async () => { const { bucketDir } = await makeRepos(["app"]); const folder = openBucket({ kind: "folder", path: bucketDir }); const refused = ["app/head", ".kepr/../app/head", ".kepr//x", "x/.kepr/y", "app/releases/.."]; for (const key of [...refused, "app/releases/1.0.0/app", "app/releases/1.0.0/x/release.json"]) { await expect(folder.put(key, new Uint8Array([1]), "image/png")).rejects.toBeInstanceOf( BucketWriteError, ); } for (const key of [...refused, "app/saves/x", "app/releases/1.0.0/"]) { await expect(folder.remove(key)).rejects.toBeInstanceOf(BucketWriteError); } await folder.put("app/releases/latest", new TextEncoder().encode("1.0.0"), "text/plain"); expect(await Bun.file(`${bucketDir}/app/releases/latest`).text()).toBe("1.0.0"); await folder.put("app/releases/1.0.0/release.json", new Uint8Array([1]), "application/json"); await folder.put(".kepr/x", new Uint8Array([1]), "text/plain"); await folder.remove(".kepr/x"); expect(await Bun.file(`${bucketDir}/.kepr/x`).exists()).toBe(false); const readOnly = openBucket({ kind: "s3", endpoint: "http://127.0.0.1:9", name: "b", region: "r", accessKeyId: "k", secretAccessKey: "s", }); await expect( readOnly.put(".kepr/media/app/x.png", new Uint8Array([1]), "image/png"), ).rejects.toThrow("[bucket.write]"); await expect(readOnly.remove(".kepr/media/app/x.png")).rejects.toThrow("[bucket.write]"); });