import { beforeEach, describe, expect, it } from "bun:test"; import { createItem, deleteItem, duplicateItem, getAllItems, getItemById, updateItem, } from "../../src/server/services/item.service.ts"; import { createSecondTestUser, createTestDb } from "../helpers/db.ts"; describe("Item Service", () => { let db: any; let userId: number; beforeEach(async () => { ({ db, userId } = await createTestDb()); }); describe("createItem", () => { it("creates item with all fields, returns item with id and timestamps", async () => { const item = await createItem(db, userId, { name: "Tent", weightGrams: 1200, priceCents: 35000, categoryId: 1, notes: "Ultralight 2-person", productUrl: "https://example.com/tent", }); expect(item).toBeDefined(); expect(item?.id).toBeGreaterThan(0); expect(item?.name).toBe("Tent"); expect(item?.weightGrams).toBe(1200); expect(item?.priceCents).toBe(35000); expect(item?.categoryId).toBe(1); expect(item?.notes).toBe("Ultralight 2-person"); expect(item?.productUrl).toBe("https://example.com/tent"); expect(item?.createdAt).toBeDefined(); expect(item?.updatedAt).toBeDefined(); }); it("only name and categoryId are required, other fields optional", async () => { const item = await createItem(db, userId, { name: "Spork", categoryId: 1, }); expect(item).toBeDefined(); expect(item?.name).toBe("Spork"); expect(item?.weightGrams).toBeNull(); expect(item?.priceCents).toBeNull(); expect(item?.notes).toBeNull(); expect(item?.productUrl).toBeNull(); }); }); describe("getAllItems", () => { it("returns all items with category info joined", async () => { await createItem(db, userId, { name: "Tent", categoryId: 1 }); await createItem(db, userId, { name: "Sleeping Bag", categoryId: 1 }); const all = await getAllItems(db, userId); expect(all).toHaveLength(2); expect(all[0].categoryName).toBe("Uncategorized"); expect(all[0].categoryIcon).toBeDefined(); }); }); describe("getItemById", () => { it("returns single item or null", async () => { const created = await createItem(db, userId, { name: "Tent", categoryId: 1, }); const found = await getItemById(db, userId, created?.id); expect(found).toBeDefined(); expect(found?.name).toBe("Tent"); const notFound = await getItemById(db, userId, 9999); expect(notFound).toBeNull(); }); }); describe("updateItem", () => { it("updates specified fields, sets updatedAt", async () => { const created = await createItem(db, userId, { name: "Tent", weightGrams: 1200, categoryId: 1, }); const updated = await updateItem(db, userId, created?.id, { name: "Big Agnes Tent", weightGrams: 1100, }); expect(updated).toBeDefined(); expect(updated?.name).toBe("Big Agnes Tent"); expect(updated?.weightGrams).toBe(1100); }); it("returns null for non-existent id", async () => { const result = await updateItem(db, userId, 9999, { name: "Ghost" }); expect(result).toBeNull(); }); }); describe("duplicateItem", () => { it("creates a copy with '(copy)' suffix in name", async () => { const original = await createItem(db, userId, { name: "Tent", weightGrams: 1200, priceCents: 35000, categoryId: 1, notes: "Ultralight", productUrl: "https://example.com/tent", }); const copy = await duplicateItem(db, userId, original?.id); expect(copy).toBeDefined(); expect(copy?.name).toBe("Tent (copy)"); expect(copy?.weightGrams).toBe(1200); expect(copy?.priceCents).toBe(35000); expect(copy?.categoryId).toBe(1); expect(copy?.notes).toBe("Ultralight"); expect(copy?.productUrl).toBe("https://example.com/tent"); }); it("copy has a different ID from the original", async () => { const original = await createItem(db, userId, { name: "Helmet", categoryId: 1, }); const copy = await duplicateItem(db, userId, original?.id); expect(copy?.id).not.toBe(original?.id); }); it("returns null for non-existent item", async () => { const result = await duplicateItem(db, userId, 9999); expect(result).toBeNull(); }); }); describe("deleteItem", () => { it("removes item from DB, returns deleted item", async () => { const created = await createItem(db, userId, { name: "Tent", categoryId: 1, imageFilename: "tent.jpg", }); const deleted = await deleteItem(db, userId, created?.id); expect(deleted).toBeDefined(); expect(deleted?.name).toBe("Tent"); expect(deleted?.imageFilename).toBe("tent.jpg"); // Verify it's gone const found = await getItemById(db, userId, created?.id); expect(found).toBeNull(); }); it("returns null for non-existent id", async () => { const result = await deleteItem(db, userId, 9999); expect(result).toBeNull(); }); }); describe("cross-user isolation", () => { it("user cannot see other user's items", async () => { const userId2 = await createSecondTestUser(db); await createItem(db, userId, { name: "User 1 Tent", categoryId: 1, }); // User 2 needs their own Uncategorized category; createSecondTestUser seeds one const user2Categories = await db.query.categories.findMany({ where: (cats: any, { eq }: any) => eq(cats.userId, userId2), }); const user2CatId = user2Categories[0].id; await createItem(db, userId2, { name: "User 2 Bag", categoryId: user2CatId, }); const user1Items = await getAllItems(db, userId); const user2Items = await getAllItems(db, userId2); expect(user1Items).toHaveLength(1); expect(user1Items[0].name).toBe("User 1 Tent"); expect(user2Items).toHaveLength(1); expect(user2Items[0].name).toBe("User 2 Bag"); }); it("getItemById returns null for another user's item", async () => { const created = await createItem(db, userId, { name: "My Item", categoryId: 1, }); const userId2 = await createSecondTestUser(db); const result = await getItemById(db, userId2, created?.id); expect(result).toBeNull(); }); }); });