import { beforeEach, describe, expect, it } from "bun:test"; import { tags } from "../../src/db/schema.ts"; import { createTag, deleteTag, getAdminTags, getAllTags, updateTag, } from "../../src/server/services/tag.service.ts"; import { createTestDb } from "../helpers/db.ts"; async function insertTag(db: any, name: string, parentId?: number | null) { const [row] = await db .insert(tags) .values({ name, parentId: parentId ?? null }) .returning(); return row!; } describe("Tag Service", () => { let db: Awaited>["db"]; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; }); it("returns empty array when no tags exist", async () => { const result = await getAllTags(db); expect(result).toEqual([]); }); it("returns all tags as { id, name } ordered alphabetically", async () => { await db .insert(tags) .values([ { name: "bikepacking" }, { name: "ultralight" }, { name: "accessories" }, ]); const result = await getAllTags(db); expect(result).toHaveLength(3); expect(result[0].name).toBe("accessories"); expect(result[1].name).toBe("bikepacking"); expect(result[2].name).toBe("ultralight"); // Should NOT include createdAt expect(result[0]).toEqual({ id: expect.any(Number), name: "accessories" }); }); }); describe("getAdminTags", () => { let db: Awaited>["db"]; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; }); it("returns tags with parentId and itemCount fields", async () => { await insertTag(db, "bikepacking"); const result = await getAdminTags(db); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ id: expect.any(Number), name: "bikepacking", parentId: null, itemCount: expect.any(Number), }); }); it("returns parentId as null for top-level tags", async () => { await insertTag(db, "ultralight"); const result = await getAdminTags(db); expect(result[0].parentId).toBeNull(); }); it("returns correct parentId for child tags", async () => { const parent = await insertTag(db, "gear"); const child = await insertTag(db, "clothing", parent.id); const result = await getAdminTags(db); const childResult = result.find((t) => t.id === child.id); expect(childResult?.parentId).toBe(parent.id); }); }); describe("createTag", () => { let db: Awaited>["db"]; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; }); it("creates a tag without parent and returns it", async () => { const tag = await createTag(db, { name: "bikepacking" }); expect(tag).toMatchObject({ id: expect.any(Number), name: "bikepacking", parentId: null, }); }); it("creates a tag with parentId set to an existing tag id", async () => { const parent = await createTag(db, { name: "gear" }); const child = await createTag(db, { name: "clothing", parentId: parent.id, }); expect(child.parentId).toBe(parent.id); }); }); describe("updateTag / cycle detection", () => { let db: Awaited>["db"]; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; }); it("renames a tag", async () => { const tag = await insertTag(db, "old-name"); const updated = await updateTag(db, tag.id, { name: "new-name" }); expect(updated?.name).toBe("new-name"); }); it("sets parentId on a tag", async () => { const parent = await insertTag(db, "parent"); const child = await insertTag(db, "child"); const updated = await updateTag(db, child.id, { parentId: parent.id }); expect(updated?.parentId).toBe(parent.id); }); it("sets parentId to null (reparent to top-level)", async () => { const parent = await insertTag(db, "parent"); const child = await insertTag(db, "child", parent.id); const updated = await updateTag(db, child.id, { parentId: null }); expect(updated?.parentId).toBeNull(); }); it("throws 'Cycle detected' when setting a descendant as parent", async () => { const a = await insertTag(db, "A"); const b = await insertTag(db, "B", a.id); const c = await insertTag(db, "C", b.id); // Try to set C (a descendant of A) as parent of A — cycle await expect(updateTag(db, a.id, { parentId: c.id })).rejects.toThrow( "Cycle detected", ); }); }); describe("deleteTag", () => { let db: Awaited>["db"]; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; }); it("deletes a tag and returns true", async () => { const tag = await insertTag(db, "bikepacking"); const result = await deleteTag(db, tag.id); expect(result).toBe(true); }); it("returns false for a non-existent tag", async () => { const result = await deleteTag(db, 99999); expect(result).toBe(false); }); it("deleting a parent causes children parentId to become null", async () => { const parent = await insertTag(db, "parent"); const child = await insertTag(db, "child", parent.id); await deleteTag(db, parent.id); const [_childRow] = await db .select({ parentId: tags.parentId }) .from(tags) .where((t: any) => t.id === child.id); // Re-fetch child to verify parentId is now null const adminTags = await getAdminTags(db); const childResult = adminTags.find((t) => t.id === child.id); expect(childResult?.parentId).toBeNull(); }); });