mirror of
https://github.com/TautvydasDerzinskas/Thingport.git
synced 2026-10-21 22:22:24 -05:00
133 lines
5.3 KiB
TypeScript
133 lines
5.3 KiB
TypeScript
import { beforeAll, beforeEach, describe, expect, it } from "vitest";
|
|
import request from "supertest";
|
|
import { createApp } from "../src/app";
|
|
|
|
const app = createApp();
|
|
const stamp = Date.now();
|
|
let token: string;
|
|
let otherToken: string;
|
|
|
|
type CategoryOut = { id: string; name: string; parent_id: string | null; position: number };
|
|
|
|
const auth = (t: string) => ({ Authorization: `Bearer ${t}` });
|
|
|
|
async function create(name: string, parentId: string | null = null): Promise<string> {
|
|
const res = await request(app).post("/api/categories").set(auth(token)).send({ name, parent_id: parentId });
|
|
expect(res.status).toBe(200);
|
|
return res.body.id;
|
|
}
|
|
|
|
const move = (id: string, parentId: string | null, position: number, as = token) =>
|
|
request(app).post(`/api/category/${id}/move`).set(auth(as)).send({ parent_id: parentId, position });
|
|
|
|
/** Child names under `parentId`, in display order. */
|
|
async function childNames(parentId: string): Promise<string[]> {
|
|
const res = await request(app).get("/api/categories").set(auth(token));
|
|
return (res.body as CategoryOut[])
|
|
.filter((c) => c.parent_id === parentId)
|
|
.toSorted((a, b) => a.position - b.position)
|
|
.map((c) => c.name);
|
|
}
|
|
|
|
beforeAll(async () => {
|
|
token = (
|
|
await request(app)
|
|
.post("/api/register")
|
|
.send({ displayName: "Mover", email: `mover-${stamp}@example.com`, password: "password123" })
|
|
).body.token;
|
|
otherToken = (
|
|
await request(app)
|
|
.post("/api/register")
|
|
.send({ displayName: "Other", email: `mover-other-${stamp}@example.com`, password: "password123" })
|
|
).body.token;
|
|
});
|
|
|
|
describe("POST /api/category/:id/move", () => {
|
|
let rootA: string;
|
|
let rootB: string;
|
|
const subs: Record<string, string> = {};
|
|
|
|
beforeEach(async () => {
|
|
rootA = await create(`Root A ${Math.random()}`);
|
|
rootB = await create(`Root B ${Math.random()}`);
|
|
for (const name of ["a1", "a2", "a3"]) subs[name] = await create(name, rootA);
|
|
for (const name of ["b1", "b2"]) subs[name] = await create(name, rootB);
|
|
// Fresh categories all start at position 0; give them a definite order first.
|
|
await request(app)
|
|
.post("/api/categories/reorder")
|
|
.set(auth(token))
|
|
.send({ category_ids: [subs.a1, subs.a2, subs.a3] });
|
|
await request(app)
|
|
.post("/api/categories/reorder")
|
|
.set(auth(token))
|
|
.send({ category_ids: [subs.b1, subs.b2] });
|
|
});
|
|
|
|
it("reorders within the same category", async () => {
|
|
expect((await move(subs.a3, rootA, 0)).status).toBe(200);
|
|
expect(await childNames(rootA)).toEqual(["a3", "a1", "a2"]);
|
|
expect((await move(subs.a3, rootA, 2)).status).toBe(200);
|
|
expect(await childNames(rootA)).toEqual(["a1", "a2", "a3"]);
|
|
});
|
|
|
|
it("moves into another category at the given position, closing the gap behind", async () => {
|
|
const res = await move(subs.a2, rootB, 1);
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.parent_id).toBe(rootB);
|
|
expect(await childNames(rootB)).toEqual(["b1", "a2", "b2"]);
|
|
expect(await childNames(rootA)).toEqual(["a1", "a3"]);
|
|
});
|
|
|
|
it("appends when the position is past the end", async () => {
|
|
expect((await move(subs.a1, rootB, 99)).status).toBe(200);
|
|
expect(await childNames(rootB)).toEqual(["b1", "b2", "a1"]);
|
|
});
|
|
|
|
it("moves into an empty category", async () => {
|
|
const empty = await create(`Empty ${Math.random()}`);
|
|
expect((await move(subs.a1, empty, 0)).status).toBe(200);
|
|
expect(await childNames(empty)).toEqual(["a1"]);
|
|
});
|
|
|
|
it("nests to any depth, whole subtrees included", async () => {
|
|
expect((await move(subs.a1, subs.b1, 0)).status).toBe(200);
|
|
expect((await move(subs.b1, subs.a2, 0)).status).toBe(200);
|
|
expect(await childNames(subs.a2)).toEqual(["b1"]);
|
|
expect(await childNames(subs.b1)).toEqual(["a1"]);
|
|
// A category with subcategories can itself become one.
|
|
expect((await move(rootB, rootA, 0)).status).toBe(200);
|
|
expect((await childNames(rootA))[0]).toMatch(/^Root B/);
|
|
});
|
|
|
|
it("rejects cycles", async () => {
|
|
expect((await move(subs.a1, subs.a1, 0)).status).toBe(400);
|
|
// Under its own child, and under its own grandchild.
|
|
expect((await move(rootA, subs.a1, 0)).status).toBe(400);
|
|
const grandchild = await create("a1-child", subs.a1);
|
|
expect((await move(rootA, grandchild, 0)).status).toBe(400);
|
|
});
|
|
|
|
it("moves subcategories up one level when their category is deleted", async () => {
|
|
await create("a2-child-1", subs.a2);
|
|
await create("a2-child-2", subs.a2);
|
|
expect((await request(app).delete(`/api/category/${subs.a2}`).set(auth(token))).status).toBe(200);
|
|
const names = await childNames(rootA);
|
|
expect(names.slice(0, 2)).toEqual(["a1", "a3"]);
|
|
expect(names.slice(2).toSorted()).toEqual(["a2-child-1", "a2-child-2"]);
|
|
});
|
|
|
|
it("only touches the user's own categories", async () => {
|
|
expect((await move(subs.a1, rootB, 0, otherToken)).status).toBe(404);
|
|
const otherRoot = (await request(app).post("/api/categories").set(auth(otherToken)).send({ name: "Other root" }))
|
|
.body.id;
|
|
expect((await move(subs.a1, otherRoot, 0)).status).toBe(400);
|
|
});
|
|
|
|
it("validates the body", async () => {
|
|
expect((await move(subs.a1, rootB, -1)).status).toBe(400);
|
|
expect(
|
|
(await request(app).post(`/api/category/${subs.a1}/move`).set(auth(token)).send({ position: 0 })).status,
|
|
).toBe(400);
|
|
});
|
|
});
|