import { createUser, db, del, get, insertCategory, insertDump, post, put, test, } from "../helpers.ts"; import type { Category } from "../../../shared/category.ts"; import type { Dump } from "../../../shared/dump.ts"; import type { PaginatedData } from "../../../shared/api.ts"; import { assertEquals, assertStringIncludes } from "@std/assert"; test("categories: listed by position, then slug", async () => { insertCategory({ slug: "zeta", position: 0 }); insertCategory({ slug: "alpha", position: 1 }); insertCategory({ slug: "beta", position: 0 }); const list = await get("/api/categories"); assertEquals(list.data.map((c) => c.slug), [ "beta", "zeta", "alpha", ]); }); test("categories: only category:manage may create, update or delete", async () => { const admin = await createUser({ role: "admin" }); const mod = await createUser({ role: "moderator" }); const json = { kind: "dump", slug: "music", name: "Music" }; assertEquals((await post("/api/categories", { json })).status, 401); assertEquals( (await post("/api/categories", { auth: mod.auth, json })).status, 403, ); const created = await post("/api/categories", { auth: admin.auth, json, }); assertEquals(created.status, 201); const id = created.data.id; assertEquals( (await put(`/api/categories/${id}`, { auth: mod.auth, json: { name: "X" }, })).status, 403, ); assertEquals( (await del(`/api/categories/${id}`, { auth: mod.auth })).status, 403, ); assertEquals( (await del(`/api/categories/${id}`, { auth: admin.auth })).status, 200, ); assertEquals( (await del(`/api/categories/${id}`, { auth: admin.auth })).status, 404, ); }); test("categories: slug and name rules, and friendly conflicts", async () => { const admin = await createUser({ role: "admin" }); const create = (json: Record) => post("/api/categories", { auth: admin.auth, json: { kind: "dump", ...json }, }); for ( const [json, message] of [ [{ slug: "Bad Slug", name: "x" }, "lowercase"], [{ slug: "hot", name: "x" }, "reserved"], [{ slug: "settings", name: "x" }, "reserved"], [{ slug: "playlists", name: "x" }, "reserved"], [{ slug: "a".repeat(51), name: "x" }, "at most 50"], [{ slug: "ok", name: " " }, "Name is required"], [{ slug: "ok", name: "x".repeat(51) }, "at most 50"], ] as const ) { const res = await create(json); assertEquals(res.status, 400, JSON.stringify(json)); assertStringIncludes(res.error.message, message); } assertEquals((await create({ slug: "ok" })).status, 400); const first = await create({ slug: "music", name: " Music " }); assertEquals(first.data.name, "Music"); const dupSlug = await create({ slug: "music", name: "Other" }); assertEquals(dupSlug.status, 409); assertStringIncludes(dupSlug.error.message, "Slug"); const dupName = await create({ slug: "other", name: "Music" }); assertEquals(dupName.status, 409); assertStringIncludes(dupName.error.message, "Name"); const second = await create({ slug: "films", name: "Films" }); const rename = await put(`/api/categories/${second.data.id}`, { auth: admin.auth, json: { slug: "music" }, }); assertEquals(rename.status, 409); const ok = await put(`/api/categories/${second.data.id}`, { auth: admin.auth, json: { name: "Movies", position: 3 }, }); assertEquals(ok.data.slug, "films"); assertEquals(ok.data.name, "Movies"); assertEquals(ok.data.position, 3); }); test("GET /api/dumps?category=: members only, private for the owner", async () => { const owner = await createUser(); const cat = insertCategory({ slug: "music" }); const inCat = insertDump({ userId: owner.id, title: "in" }); const priv = insertDump({ userId: owner.id, title: "private", isPrivate: true, }); insertDump({ userId: owner.id, title: "out" }); for (const d of [inCat, priv]) { db.prepare( `INSERT INTO dump_categories (dump_id, category_id) VALUES (?, ?);`, ) .run(d.id, cat); } const anon = await get>("/api/dumps?category=music"); assertEquals(anon.data.items.map((d) => d.title), [ "in", ]); assertEquals(anon.data.items[0].categoryIds, [cat]); assertEquals( (await get>("/api/dumps?category=music", { auth: owner.auth, })).data .items.length, 2, ); assertEquals((await get("/api/dumps?category=nope")).status, 404); // Deleting the category unlinks its dumps without deleting them. const admin = await createUser({ role: "admin" }); await del(`/api/categories/${cat}`, { auth: admin.auth }); assertEquals( (await get(`/api/dumps/${inCat.id}`)).data.categoryIds, [], ); }); test("categories: each kind is a set apart", async () => { const admin = await createUser({ role: "admin" }); const create = (json: Record) => post("/api/categories", { auth: admin.auth, json }); assertEquals((await create({ slug: "music", name: "Music" })).status, 400); const dump = await create({ kind: "dump", slug: "music", name: "Music" }); assertEquals(dump.data.kind, "dump"); // The same slug and name are free in the other kind, taken in the same. const playlist = await create({ kind: "playlist", slug: "music", name: "Music", }); assertEquals(playlist.status, 201); assertEquals(playlist.data.kind, "playlist"); assertEquals( (await create({ kind: "playlist", slug: "music", name: "Other" })).status, 409, ); // A playlist category's page lives under /playlists/: nothing is reserved. assertEquals( (await create({ kind: "playlist", slug: "hot", name: "Hot" })).status, 201, ); const list = await get("/api/categories"); assertEquals(list.data.map((c) => [c.kind, c.slug]), [ ["dump", "music"], ["playlist", "hot"], ["playlist", "music"], ]); });