1import { beforeAll, describe, expect, test } from "bun:test";2import { Browser, type Instance, instance } from "./browser.ts";3import { makeRepos } from "./fixture.ts";45const PNG = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3]);6const MP4 = new Uint8Array([0, 0, 0, 0x18, ...new TextEncoder().encode("ftypisom"), 1, 2, 3, 4]);78describe("editing a repo's description and details while signed in", () => {9 let inst: Instance;10 let browser: Browser;11 const page = async (path: string) => (await inst.app.request(path)).text();12 const save = (field: string, value: string) =>13 browser.post("/admin/repos/app/text", { field, value });14 const upload = (bytes: Uint8Array, name: string) => {15 const data = new FormData();16 data.append("file", new File([bytes], name));17 return browser.post("/admin/repos/app/media", data);18 };1920 beforeAll(async () => {21 const repos = await makeRepos(["app", "shy"]);22 inst = await instance(repos.bucketDir);23 inst.kepr.run("UPDATE repos SET public = 1 WHERE name = 'app'");24 browser = new Browser(inst.app);25 });2627 test("signing in leaves a script-readable mark that says only that, beside the session", async () => {28 const response = await browser.signIn();29 const cookies = response.headers.getSetCookie();30 expect(cookies.find((line) => line.startsWith("kepr_editor=1"))).not.toMatch(/HttpOnly/i);31 expect(cookies.find((line) => line.startsWith("kepr_session="))).toMatch(/HttpOnly/i);32 33 expect(await (await browser.request("/app")).text()).toBe(await page("/app"));34 });3536 test("starts from the README's words", async () => {37 const response = await browser.request("/admin/repos/app/text");38 const texts = (await response.json()) as Record<"form" | "description" | "details", string>;39 expect(texts.description).toBe("The app project.");40 expect(texts.details).toContain("# app");41 42 expect(texts.form).toBe(browser.form);43 });4445 test("the operator's description replaces the README's on the page, in cards and previews", async () => {46 expect((await save("description", "My **own** words.")).status).toBe(204);47 const repo = await page("/app");48 expect(repo).toContain('<div class="repo-desc"><p>My <strong>own</strong> words.</p></div>');49 expect(repo).toContain('<meta name="description" content="My own words.">');50 expect(await page("/browse")).toContain('<p class="repo-card-desc">My own words.</p>');51 });5253 test("details size and place pictures and videos from their options, and keep uploads' URLs", async () => {54 const details = [55 '',56 '',57 "",58 '',59 ].join("\n\n");60 expect((await save("details", details)).status).toBe(204);61 const repo = await page("/app");62 expect(repo).toContain(63 '<img src="/static/assets/media/app/a.png" alt="shot" class="media media-w33 media-right">',64 );65 expect(repo).toContain(66 '<video class="media media-w50 media-left" src="/static/assets/media/app/b.mp4" autoplay muted loop playsinline aria-label="demo"></video>',67 );68 expect(repo).toContain(69 '<video class="media media-w100 media-center" src="/static/assets/media/app/c.webm" controls preload="metadata" aria-label="talk"></video>',70 );71 72 expect(repo).toContain('<img src="/app/raw/pic.png" alt="plain" title="a real title">');73 expect(repo).not.toContain("# app");74 });7576 test("the preview renders as the page will", async () => {77 const response = await browser.post("/admin/repos/app/preview", {78 field: "details",79 value: "**hi** <script>x</script>",80 });81 expect(await response.text()).toBe(82 "<p><strong>hi</strong> <script>x</script></p>\n",83 );84 });8586 test("clearing a text brings the README's back", async () => {87 await save("description", "");88 await save("details", "");89 const repo = await page("/app");90 expect(repo).toContain("<p>The app project.</p>");91 expect(repo).toContain("<h1>app</h1>");92 });9394 test("refuses an unknown field, an overlong text and an unknown repo", async () => {95 expect((await save("title", "x")).status).toBe(400);96 expect((await save("description", "x".repeat(2001))).status).toBe(400);97 expect((await browser.post("/admin/repos/nope/text", { field: "details" })).status).toBe(404);98 });99100 test("uploads pictures and videos by what their bytes are, served as privately as the repo", async () => {101 const png = await upload(PNG, "shot.txt");102 expect(png.status).toBe(200);103 const { url } = (await png.json()) as { url: string };104 expect(url).toMatch(/^\/static\/assets\/media\/app\/[0-9a-f]{64}\.png$/);105 const served = await inst.app.request(url);106 expect([served.status, served.headers.get("content-type")]).toEqual([200, "image/png"]);107 const video = ((await (await upload(MP4, "demo.mp4")).json()) as { url: string }).url;108 expect(video).toEndWith(".mp4");109 110 const server = Bun.serve({ port: 0, fetch: (request) => inst.app.fetch(request) });111 const range = await fetch(new URL(video, server.url), { headers: { range: "bytes=0-3" } });112 expect([range.status, (await range.arrayBuffer()).byteLength]).toEqual([206, 4]);113 server.stop();114 expect((await upload(new TextEncoder().encode("<svg onload=x>"), "a.svg")).status).toBe(400);115 inst.kepr.run("UPDATE repos SET public = 0 WHERE name = 'app'");116 expect((await inst.app.request(url)).status).toBe(404);117 inst.kepr.run("UPDATE repos SET public = 1 WHERE name = 'app'");118 });119120 test("signed out, none of it answers", async () => {121 const stranger = new Browser(inst.app);122 expect((await stranger.request("/admin/repos/app/text")).status).toBe(401);123 expect((await stranger.post("/admin/repos/app/media", new FormData())).status).toBe(401);124 });125});