import { count, createUser, db, del, formWithFile, get, html, insertCategory, insertComment, insertDump, insertPlaylist, PNG, post, put, stubNetwork, test, TEST_DIR, } from "../helpers.ts"; import type { Dump, DumpUrlMatch } from "../../../shared/dump.ts"; import type { PaginatedData } from "../../../shared/api.ts"; import type { Playlist } from "../../../shared/playlist.ts"; import type { PublicUser } from "../../../shared/user.ts"; import { assertEquals, assertExists, assertFalse } from "@std/assert"; const PAGE = html(`Scraped title `); function readFile(path: string): Uint8Array | null { try { return Deno.readFileSync(`${TEST_DIR}/uploads/${path}`); } catch { return null; } } // ── Create ─────────────────────────────────────────────────────────────────── test("POST /api/dumps: a URL dump takes its title from the page", async () => { const user = await createUser(); using _ = stubNetwork(() => PAGE.clone()); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://example.com/a?utm_source=x", comment: "look" }, }); assertEquals(res.status, 201); const dump = res.data; assertEquals(dump.kind, "url"); assertEquals(dump.title, "Scraped title"); assertEquals(dump.slug, "scraped-title"); assertEquals(dump.userId, user.id); assertEquals(dump.ownerUsername, user.username); assertEquals(dump.isPrivate, false); assertEquals(dump.richContent?.thumbnailUrl, "https://example.com/cover.jpg"); const row = db.prepare(`SELECT url, url_canonical FROM dumps WHERE id = ?;`) .get(dump.id) as { url: string; url_canonical: string }; // The exact URL is kept; the canonical key drops the tracking parameter. assertEquals(row.url, "https://example.com/a?utm_source=x"); assertEquals(row.url_canonical, "https://example.com/a"); }); test("POST /api/dumps: a typed title wins; an unreachable page still posts", async () => { const user = await createUser(); { using _ = stubNetwork(() => PAGE.clone()); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://example.com/b", title: " My title " }, }); assertEquals(res.data.title, "My title"); } { using _ = stubNetwork(() => { throw new TypeError("offline"); }); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://www.offline.example/c" }, }); assertEquals(res.status, 201); assertEquals(res.data.title, "offline.example"); } }); test("POST /api/dumps: refuses bad URLs and bodies, and anonymous callers", async () => { const user = await createUser(); using net = stubNetwork(() => PAGE.clone()); for ( const url of ["http://localhost/", "http://192.168.0.1/", "ftp://x/", "no"] ) { const res = await post("/api/dumps", { auth: user.auth, json: { url } }); assertEquals(res.status, 400, url); } assertEquals( (await post("/api/dumps", { auth: user.auth, json: { nope: 1 } })).status, 400, ); assertEquals( (await post("/api/dumps", { json: { url: "https://example.com" } })).status, 401, ); assertEquals(net.calls.length, 0); assertEquals(count(`SELECT COUNT(*) as n FROM dumps;`), 0); }); test("POST /api/dumps: categories are stored and deduplicated", async () => { const user = await createUser(); const music = insertCategory({ slug: "music" }); using _ = stubNetwork(() => PAGE.clone()); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://example.com/d", categoryIds: [music, music] }, }); assertEquals(res.status, 201); assertEquals(res.data.categoryIds, [music]); assertEquals( (await get(`/api/dumps/${res.data.id}`)).data.categoryIds, [music], ); }); test("POST /api/dumps: an unknown category is refused and nothing is left behind", async () => { const user = await createUser(); using _ = stubNetwork(() => PAGE.clone()); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://example.com/e", categoryIds: ["no-such-category"] }, }); assertEquals(res.status, 400); assertEquals(count(`SELECT COUNT(*) as n FROM dumps;`), 0); }); test("POST /api/dumps: a playlist category is refused", async () => { const user = await createUser(); const lists = insertCategory({ slug: "mixes", kind: "playlist" }); using _ = stubNetwork(() => PAGE.clone()); const res = await post("/api/dumps", { auth: user.auth, json: { url: "https://example.com/e", categoryIds: [lists] }, }); assertEquals(res.status, 400); assertEquals(count(`SELECT COUNT(*) as n FROM dumps;`), 0); }); test("POST /api/dumps: a file dump is stored and served back", async () => { const user = await createUser(); const res = await post("/api/dumps", { auth: user.auth, form: formWithFile(PNG, "pixel.png", "image/png", { comment: "tiny" }), }); assertEquals(res.status, 201); const dump = res.data; assertEquals(dump.kind, "file"); assertEquals(dump.title, "pixel.png"); assertEquals(dump.fileMime, "image/png"); assertEquals(dump.fileSize, PNG.byteLength); assertEquals(dump.comment, "tiny"); assertEquals(readFile(`dumps/${dump.id}`), PNG); const file = await get(`/api/dumps/${dump.id}/file`); assertEquals(file.status, 200); assertEquals(file.bytes, PNG); }); test("POST /api/dumps: file type, size of text fields, and a missing file", async () => { const user = await createUser(); const exe = await post("/api/dumps", { auth: user.auth, form: formWithFile(PNG, "x.exe", "application/x-msdownload"), }); assertEquals(exe.status, 400); const longTitle = await post("/api/dumps", { auth: user.auth, form: formWithFile(PNG, "x.png", "image/png", { title: "x".repeat(201) }), }); assertEquals(longTitle.status, 400); const longComment = await post("/api/dumps", { auth: user.auth, form: formWithFile(PNG, "x.png", "image/png", { comment: "x".repeat(5001), }), }); assertEquals(longComment.status, 400); const form = new FormData(); form.set("comment", "no file"); assertEquals( (await post("/api/dumps", { auth: user.auth, form })).status, 400, ); assertEquals(count(`SELECT COUNT(*) as n FROM dumps;`), 0); }); test("POST /api/dumps: a file dump with an unknown category leaves nothing behind", async () => { const user = await createUser(); const res = await post("/api/dumps", { auth: user.auth, form: formWithFile(PNG, "x.png", "image/png", { categoryIds: JSON.stringify(["nope"]), }), }); assertEquals(res.status, 400); assertEquals(count(`SELECT COUNT(*) as n FROM dumps;`), 0); }); test("POST /api/dumps: followers are notified of public dumps, not private ones", async () => { const poster = await createUser(); const follower = await createUser(); db.prepare( `INSERT INTO follows (id, follower_id, followed_user_id, created_at) VALUES (?, ?, ?, ?);`, ).run(crypto.randomUUID(), follower.id, poster.id, new Date().toISOString()); using _ = stubNetwork(() => PAGE.clone()); await post("/api/dumps", { auth: poster.auth, json: { url: "https://example.com/private", isPrivate: true }, }); assertEquals(count(`SELECT COUNT(*) as n FROM notifications;`), 0); const pub = await post("/api/dumps", { auth: poster.auth, json: { url: "https://example.com/public" }, }); const notes = db.prepare( `SELECT user_id, type, data FROM notifications;`, ).all() as { user_id: string; type: string; data: string }[]; assertEquals(notes.length, 1); assertEquals(notes[0].user_id, follower.id); assertEquals(notes[0].type, "user_dump_posted"); assertEquals(JSON.parse(notes[0].data).dumpId, pub.data.id); }); // ── Read ───────────────────────────────────────────────────────────────────── test("GET /api/dumps/:id works by id only", async () => { const user = await createUser(); const { id, slug } = insertDump({ userId: user.id, title: "Hello" }); assertEquals((await get(`/api/dumps/${id}`)).data.id, id); // A slug is only unique under its owner: it is read by page path. assertEquals((await get(`/api/dumps/${slug}`)).status, 404); assertEquals((await get(`/api/dumps/nope`)).status, 404); assertEquals((await get(`/api/dumps/${crypto.randomUUID()}`)).status, 404); }); test("GET /api/dumps/:id: a private dump exists only for its owner", async () => { const owner = await createUser(); const other = await createUser(); const { id } = insertDump({ userId: owner.id, isPrivate: true }); assertEquals( (await get(`/api/dumps/${id}`, { auth: owner.auth })).status, 200, ); assertEquals( (await get(`/api/dumps/${id}`, { auth: other.auth })).status, 404, ); assertEquals((await get(`/api/dumps/${id}`)).status, 404); for (const sub of ["voters", "related", "playlists", "comments"]) { assertEquals((await get(`/api/dumps/${id}/${sub}`)).status, 404, sub); assertEquals( (await get(`/api/dumps/${id}/${sub}`, { auth: owner.auth })).status, 200, sub, ); } }); test("GET /api/dumps: newest first, paginated, private ones for the owner only", async () => { const owner = await createUser(); const other = await createUser(); for (let i = 0; i < 5; i++) { insertDump({ userId: owner.id, title: `d${i}`, createdAt: new Date(Date.UTC(2024, 0, i + 1)).toISOString(), }); } insertDump({ userId: owner.id, title: "secret", isPrivate: true, createdAt: new Date(Date.UTC(2024, 0, 10)).toISOString(), }); const page1 = await get>("/api/dumps?limit=2"); assertEquals(page1.data.items.map((d) => d.title), [ "d4", "d3", ]); assertEquals(page1.data.hasMore, true); // Pages are told apart by the row past the limit, not by a count. assertEquals(page1.data.total, undefined); const page3 = await get>("/api/dumps?limit=2&page=3"); assertEquals(page3.data.items.length, 1); assertEquals(page3.data.hasMore, false); const mine = await get>("/api/dumps", { auth: owner.auth, }); assertEquals(mine.data.items[0].title, "secret"); assertEquals(mine.data.items.length, 6); assertEquals( (await get>("/api/dumps", { auth: other.auth })).data .items.length, 5, ); }); test("GET /api/dumps?url=: matches on the canonical key, respects privacy", async () => { const owner = await createUser(); const other = await createUser(); insertDump({ userId: owner.id, title: "public", url: "https://youtu.be/abc", }); insertDump({ userId: owner.id, title: "private", url: "https://www.youtube.com/watch?v=abc", isPrivate: true, }); const q = encodeURIComponent("https://m.youtube.com/watch?v=abc&t=10"); const anon = await get(`/api/dumps?url=${q}`); assertEquals(anon.data.map((d) => d.title), [ "public", ]); assertEquals(anon.data[0].ownerUsername, owner.username); const mine = await get(`/api/dumps?url=${q}`, { auth: owner.auth, }); assertEquals(mine.data.length, 2); assertEquals( (await get(`/api/dumps?url=${q}`, { auth: other.auth, })).data .length, 1, ); assertEquals( (await get(`/api/dumps?url=garbage`)).data, [], ); }); test("GET /api/dumps/:id/voters lists voters without private fields", async () => { const owner = await createUser(); const voter = await createUser(); const { id } = insertDump({ userId: owner.id }); db.prepare( `INSERT INTO votes (dump_id, user_id, created_at) VALUES (?, ?, ?);`, ).run(id, voter.id, new Date().toISOString()); const res = await get>(`/api/dumps/${id}/voters`); assertEquals(res.data.items.length, 1); assertEquals(res.data.items[0].username, voter.username); assertFalse("email" in res.data.items[0]); assertFalse("passwordHash" in res.data.items[0]); }); test("GET /api/dumps/:id/related: links in both directions, hiding private ones", async () => { const user = await createUser(); const other = await createUser(); const target = insertDump({ userId: user.id, title: "target", url: "https://x.test/t", }); const secret = insertDump({ userId: other.id, title: "secret", isPrivate: true, }); using _ = stubNetwork(() => PAGE.clone()); // A new dump whose comment links the target by another shape of its URL, // and the secret dump by its page — and itself, which is ignored. const src = await post("/api/dumps", { auth: other.auth, json: { url: "https://example.com/src", comment: `see https://www.x.test/t/?utm_source=feed and http://anywhere${secret.path}#comment-x and http://gerbeur.test/@${other.username}/dumps/scraped-title`, }, }); const srcId = src.data.id; const fromSrc = await get(`/api/dumps/${srcId}/related`); assertEquals(fromSrc.data.map((d) => d.title), [ "target", ]); const fromSrcAsOwner = await get(`/api/dumps/${srcId}/related`, { auth: other.auth, }); assertEquals(fromSrcAsOwner.data.length, 2); const toTarget = await get(`/api/dumps/${target.id}/related`); assertEquals(toTarget.data.map((d) => d.id), [srcId]); // Editing the comment away removes the links. await put(`/api/dumps/${srcId}`, { auth: other.auth, json: { comment: "" } }); assertEquals((await get(`/api/dumps/${target.id}/related`)).data, []); }); test("GET /api/dumps/:id/playlists: public playlists, plus the viewer's private ones", async () => { const user = await createUser(); const other = await createUser(); const { id } = insertDump({ userId: user.id }); const pub = insertPlaylist({ userId: user.id, title: "Public" }); const priv = insertPlaylist({ userId: user.id, title: "Private", isPublic: false, }); for (const p of [pub, priv]) { db.prepare( `INSERT INTO playlist_dumps (playlist_id, dump_id, position, added_at) VALUES (?, ?, 0, ?);`, ).run(p.id, id, new Date().toISOString()); } assertEquals( (await get(`/api/dumps/${id}/playlists`, { auth: other.auth })) .data .map((p) => p.title), ["Public"], ); assertEquals( (await get(`/api/dumps/${id}/playlists`, { auth: user.auth })) .data .length, 2, ); }); // ── Update ─────────────────────────────────────────────────────────────────── test("PUT /api/dumps/:id: a slug changes when sent, the old one stays an alias, 3 a day", async () => { const owner = await createUser(); const mod = await createUser({ role: "moderator" }); const { id } = insertDump({ userId: owner.id, title: "First" }); const other = insertDump({ userId: owner.id, title: "Taken" }); const byPath = (slug: string) => get(`/api/users/@${owner.username}/dumps/${slug}`, { auth: owner.auth, }); const setSlug = (slug: string, auth = owner.auth) => put(`/api/dumps/${id}`, { auth, json: { slug } }); assertEquals((await setSlug("Not A Slug")).status, 400); // Another dump's slug, current or former, is refused. assertEquals((await setSlug(other.slug)).status, 409); assertEquals((await setSlug("second")).data.slug, "second"); // The old slug answers the dump, which carries its current one. assertEquals((await byPath("first")).data.slug, "second"); assertEquals((await byPath("second")).data.id, id); // Taking back its own former slug swaps them; the limit counts the aliases // left, so a swap doesn't add to it. assertEquals((await setSlug("first")).data.slug, "first"); assertEquals((await byPath("second")).data.slug, "first"); assertEquals((await setSlug("third")).data.slug, "third"); assertEquals((await setSlug("fourth")).data.slug, "fourth"); // A fourth alias in a day is refused, but not to a moderator. assertEquals((await setSlug("fifth")).status, 429); assertEquals((await setSlug("fifth", mod.auth)).data.slug, "fifth"); // Every former slug still leads to it, without chaining. for (const slug of ["first", "second", "third", "fourth", "fifth"]) { assertEquals((await byPath(slug)).data.slug, "fifth", slug); } // Deleting the dump frees its aliases. await del(`/api/dumps/${id}`, { auth: owner.auth }); assertEquals( count(`SELECT COUNT(*) AS n FROM dump_slug_aliases WHERE dump_id = ?;`, id), 0, ); const reborn = insertDump({ userId: owner.id, title: "Second" }); assertEquals(reborn.slug, "second"); }); test("PUT /api/dumps/:id: owner and moderator may edit, others may not", async () => { const owner = await createUser(); const other = await createUser(); const mod = await createUser({ role: "moderator" }); const { id, slug } = insertDump({ userId: owner.id, title: "Old" }); assertEquals( (await put(`/api/dumps/${id}`, { auth: other.auth, json: { title: "X" } })) .status, 403, ); assertEquals( (await put(`/api/dumps/${id}`, { json: { title: "X" } })).status, 401, ); const res = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: "New title", comment: "hello" }, }); assertEquals(res.status, 200); assertEquals(res.data.title, "New title"); // A new title never changes the slug by itself. assertEquals(res.data.slug, slug); assertExists(res.data.updatedAt); const byMod = await put(`/api/dumps/${id}`, { auth: mod.auth, json: { title: "Moderated" }, }); assertEquals(byMod.status, 200); assertEquals((await get(`/api/dumps/${id}`)).data.title, "Moderated"); }); test("PUT /api/dumps/:id: invalid input is 400, a new URL refetches metadata", async () => { const owner = await createUser(); const { id } = insertDump({ userId: owner.id, title: "Old", url: "https://a.test/", }); assertEquals( (await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: " " } })) .status, 400, ); assertEquals( (await put(`/api/dumps/${id}`, { auth: owner.auth, json: { url: "http://127.0.0.1/" }, })).status, 400, ); using net = stubNetwork(() => PAGE.clone()); // Same URL: no fetch, title kept. await put(`/api/dumps/${id}`, { auth: owner.auth, json: { comment: "c" } }); assertEquals(net.calls.length, 0); const moved = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { url: "https://b.test/?utm_medium=z" }, }); assertEquals(moved.data.title, "Scraped title"); assertEquals(net.calls.length, 1); const row = db.prepare(`SELECT url_canonical FROM dumps WHERE id = ?;`).get( id, ) as { url_canonical: string; }; assertEquals(row.url_canonical, "https://b.test"); }); test("PUT /api/dumps/:id: categories are replaced, or kept when omitted", async () => { const owner = await createUser(); const a = insertCategory({ slug: "a" }); const b = insertCategory({ slug: "b" }); const { id } = insertDump({ userId: owner.id }); await put(`/api/dumps/${id}`, { auth: owner.auth, json: { categoryIds: [a] }, }); const kept = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: "t" }, }); assertEquals(kept.data.categoryIds, [a]); const replaced = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { categoryIds: [b] }, }); assertEquals(replaced.data.categoryIds, [b]); const cleared = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { categoryIds: [] }, }); assertEquals(cleared.data.categoryIds, []); }); test("PUT /api/dumps/:id: an unknown category changes nothing", async () => { const owner = await createUser(); const a = insertCategory({ slug: "a" }); const { id } = insertDump({ userId: owner.id, title: "Before" }); await put(`/api/dumps/${id}`, { auth: owner.auth, json: { categoryIds: [a] }, }); const res = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: "After", categoryIds: [a, "nope"] }, }); assertEquals(res.status, 400); const dump = (await get(`/api/dumps/${id}`)).data; assertEquals(dump.title, "Before"); assertEquals(dump.categoryIds, [a]); }); test("PUT /api/dumps/:id/file: an unknown category keeps the old file", async () => { const owner = await createUser(); const original = new TextEncoder().encode("original"); const { id } = insertDump({ userId: owner.id, title: "orig", fileName: "a.txt", fileData: original, }); const res = await put(`/api/dumps/${id}/file`, { auth: owner.auth, form: formWithFile(PNG, "b.png", "image/png", { categoryIds: JSON.stringify(["nope"]), }), }); assertEquals(res.status, 400); assertEquals(readFile(`dumps/${id}`), original); assertEquals((await get(`/api/dumps/${id}`)).data.fileName, "a.txt"); }); test("PUT /api/dumps/:id: going private hides the dump from everyone else", async () => { const owner = await createUser(); const { id } = insertDump({ userId: owner.id }); await put(`/api/dumps/${id}`, { auth: owner.auth, json: { isPrivate: true }, }); assertEquals((await get(`/api/dumps/${id}`)).status, 404); await put(`/api/dumps/${id}`, { auth: owner.auth, json: { isPrivate: false }, }); assertEquals((await get(`/api/dumps/${id}`)).status, 200); }); test("PUT /api/dumps/:id: going public notifies the poster's and its playlists' followers, once", async () => { const owner = await createUser(); const fan = await createUser(); const listener = await createUser(); db.prepare( `INSERT INTO follows (id, follower_id, followed_user_id, created_at) VALUES (?, ?, ?, ?);`, ).run(crypto.randomUUID(), fan.id, owner.id, new Date().toISOString()); const mix = insertPlaylist({ userId: owner.id, title: "Mix" }); const hidden = insertPlaylist({ userId: owner.id, title: "Hidden", isPublic: false, }); await put(`/api/me/follow/playlists/${mix.id}`, { auth: listener.auth }); const { id } = insertDump({ userId: owner.id, isPrivate: true }); for (const p of [mix, hidden]) { await post(`/api/playlists/${p.id}/dumps/${id}`, { auth: owner.auth }); } db.exec(`DELETE FROM notifications;`); // the follow notified the owner const setPrivate = (isPrivate: boolean) => put(`/api/dumps/${id}`, { auth: owner.auth, json: { isPrivate } }); await setPrivate(false); const notes = db.prepare( `SELECT user_id, type, data FROM notifications ORDER BY type;`, ).all() as { user_id: string; type: string; data: string }[]; assertEquals( notes.map((n) => [n.user_id, n.type, JSON.parse(n.data).playlistId]), [ [listener.id, "playlist_dump_added", mix.id], [fan.id, "user_dump_posted", undefined], ], ); // Neither going private and public again nor an edit notifies twice. await setPrivate(true); await setPrivate(false); await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: "New" } }); assertEquals(count(`SELECT COUNT(*) as n FROM notifications;`), 2); }); test("PUT /api/dumps/:id: a file dump edits title, comment and privacy", async () => { const owner = await createUser(); const { id } = insertDump({ userId: owner.id, title: "file", fileName: "a.txt", }); const res = await put(`/api/dumps/${id}`, { auth: owner.auth, json: { title: "Renamed", comment: "c", isPrivate: true, url: "https://ignored.test", }, }); assertEquals(res.status, 200); assertEquals(res.data.title, "Renamed"); assertEquals(res.data.isPrivate, true); assertEquals(res.data.url, undefined); }); test("PUT /api/dumps/:id/file replaces a file dump's contents", async () => { const owner = await createUser(); const other = await createUser(); const { id } = insertDump({ userId: owner.id, title: "orig", fileName: "a.txt", }); const form = () => formWithFile(PNG, "b.png", "image/png", { title: "Replaced" }); assertEquals( (await put(`/api/dumps/${id}/file`, { auth: other.auth, form: form() })) .status, 403, ); const res = await put(`/api/dumps/${id}/file`, { auth: owner.auth, form: form(), }); assertEquals(res.status, 200); assertEquals(res.data.fileName, "b.png"); assertEquals(res.data.title, "Replaced"); assertEquals(readFile(`dumps/${id}`), PNG); const urlDump = insertDump({ userId: owner.id }); assertEquals( (await put(`/api/dumps/${urlDump.id}/file`, { auth: owner.auth, form: form(), })) .status, 400, ); }); test("PUT /api/dumps/:id/file: visibility changes with the file, or stays", async () => { const owner = await createUser(); const other = await createUser(); const { id } = insertDump({ userId: owner.id, fileName: "a.txt" }); const replace = (fields: Record) => put(`/api/dumps/${id}/file`, { auth: owner.auth, form: formWithFile(PNG, "b.png", "image/png", fields), }); const visible = async () => (await get(`/api/dumps/${id}`, { auth: other.auth })).status === 200; let res = await replace({ isPrivate: "true" }); assertEquals(res.data.isPrivate, true); assertEquals(await visible(), false); assertEquals((await get(`/api/dumps/${id}/file`)).status, 404); // Left out, it stays private. res = await replace({}); assertEquals(res.data.isPrivate, true); assertEquals(await visible(), false); res = await replace({ isPrivate: "false" }); assertEquals(res.data.isPrivate, false); assertEquals(await visible(), true); // A refused replacement changes neither. res = await replace({ isPrivate: "true", categoryIds: '["nope"]' }); assertEquals(res.status, 400); assertEquals(await visible(), true); }); test("custom thumbnails: set, served, removed", async () => { const owner = await createUser(); const other = await createUser(); const { id } = insertDump({ userId: owner.id }); assertEquals( (await put(`/api/dumps/${id}/thumbnail`, { auth: other.auth, form: formWithFile(PNG), })).status, 403, ); assertEquals( (await put(`/api/dumps/${id}/thumbnail`, { auth: owner.auth, form: formWithFile( new TextEncoder().encode(""), "x.svg", "image/svg+xml", ), })).status, 400, ); const set = await put(`/api/dumps/${id}/thumbnail`, { auth: owner.auth, form: formWithFile(PNG), }); assertEquals(set.status, 200); assertEquals(set.data.thumbnailMime, "image/png"); const thumb = await get(`/api/dumps/${id}/thumbnail`); assertEquals(thumb.status, 200); assertEquals(thumb.headers.get("content-type"), "image/png"); assertEquals(thumb.bytes, PNG); const removed = await del(`/api/dumps/${id}/thumbnail`, { auth: owner.auth, }); assertEquals(removed.data.thumbnailMime, undefined); assertEquals(readFile(`thumbnails/${id}-custom`), null); // A URL dump without its own thumbnail has none to serve. assertEquals((await get(`/api/dumps/${id}/thumbnail`)).status, 404); }); test("POST /api/dumps/:id/metadata: updates, or keeps what it had on failure", async () => { const owner = await createUser(); const { id } = insertDump({ userId: owner.id, title: "Kept", url: "https://example.com/r", richContent: { type: "generic", url: "https://example.com/r", title: "Kept", }, }); { using _ = stubNetwork(() => { throw new TypeError("down"); }); const res = await post(`/api/dumps/${id}/metadata`, { auth: owner.auth, }); assertEquals(res.status, 502); assertEquals((await get(`/api/dumps/${id}`)).data.title, "Kept"); } { using _ = stubNetwork(() => PAGE.clone()); const res = await post(`/api/dumps/${id}/metadata`, { auth: owner.auth, }); assertEquals(res.status, 200); assertEquals(res.data.title, "Scraped title"); } const file = insertDump({ userId: owner.id, fileName: "f.txt" }); assertEquals( (await post(`/api/dumps/${file.id}/metadata`, { auth: owner.auth })) .status, 400, ); }); // ── Delete ─────────────────────────────────────────────────────────────────── test("DELETE /api/dumps/:id removes the dump, its file and what hangs off it", async () => { const owner = await createUser(); const other = await createUser(); const mod = await createUser({ role: "moderator" }); const { id } = insertDump({ userId: owner.id, fileName: "f.txt" }); insertComment({ dumpId: id, userId: other.id }); db.prepare( `INSERT INTO votes (dump_id, user_id, created_at) VALUES (?, ?, ?);`, ) .run(id, other.id, new Date().toISOString()); assertEquals( (await del(`/api/dumps/${id}`, { auth: other.auth })).status, 403, ); assertEquals( (await del(`/api/dumps/${id}`, { auth: owner.auth })).status, 200, ); assertEquals((await get(`/api/dumps/${id}`)).status, 404); assertEquals(readFile(`dumps/${id}`), null); assertEquals(count(`SELECT COUNT(*) as n FROM comments;`), 0); assertEquals(count(`SELECT COUNT(*) as n FROM votes;`), 0); const second = insertDump({ userId: owner.id }); assertEquals( (await del(`/api/dumps/${second.id}`, { auth: mod.auth })).status, 200, ); assertEquals( (await del(`/api/dumps/${second.id}`, { auth: mod.auth })).status, 404, ); });