import { beforeEach, describe, expect, it } from "bun:test"; import { eq } from "drizzle-orm"; import { items as itemsTable } from "../../src/db/schema.ts"; import { createItem } from "../../src/server/services/item.service.ts"; import { createSetup, deleteSetup, getAllSetups, getSetupWithItems, removeSetupItem, syncSetupItems, updateItemClassification, updateSetup, } from "../../src/server/services/setup.service.ts"; import { createSecondTestUser, createTestDb } from "../helpers/db.ts"; describe("Setup Service", () => { let db: any; let userId: number; beforeEach(async () => { ({ db, userId } = await createTestDb()); }); describe("createSetup", () => { it("creates setup with name, returns setup with id/timestamps", async () => { const setup = await createSetup(db, userId, { name: "Day Hike" }); expect(setup).toBeDefined(); expect(setup.id).toBeGreaterThan(0); expect(setup.name).toBe("Day Hike"); expect(setup.createdAt).toBeDefined(); expect(setup.updatedAt).toBeDefined(); }); }); describe("getAllSetups", () => { it("returns setups with itemCount, totalWeight, totalCost", async () => { const setup = await createSetup(db, userId, { name: "Backpacking", }); const item1 = await createItem(db, userId, { name: "Tent", categoryId: 1, weightGrams: 1200, priceCents: 30000, }); const item2 = await createItem(db, userId, { name: "Sleeping Bag", categoryId: 1, weightGrams: 800, priceCents: 20000, }); await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); const setups = await getAllSetups(db, userId); expect(setups).toHaveLength(1); expect(setups[0].name).toBe("Backpacking"); expect(setups[0].itemCount).toBe(2); expect(setups[0].totalWeight).toBe(2000); expect(setups[0].totalCost).toBe(50000); }); it("returns 0 for weight/cost when setup has no items", async () => { await createSetup(db, userId, { name: "Empty Setup" }); const setups = await getAllSetups(db, userId); expect(setups).toHaveLength(1); expect(setups[0].itemCount).toBe(0); expect(setups[0].totalWeight).toBe(0); expect(setups[0].totalCost).toBe(0); }); }); describe("getSetupWithItems", () => { it("returns setup with full item details and category info", async () => { const setup = await createSetup(db, userId, { name: "Day Hike" }); const item = await createItem(db, userId, { name: "Water Bottle", categoryId: 1, weightGrams: 200, priceCents: 2500, }); await syncSetupItems(db, userId, setup.id, [item.id]); const result = await getSetupWithItems(db, userId, setup.id); expect(result).toBeDefined(); expect(result?.name).toBe("Day Hike"); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Water Bottle"); expect(result?.items[0].categoryName).toBe("Uncategorized"); expect(result?.items[0].categoryIcon).toBeDefined(); }); it("returns null for non-existent setup", async () => { const result = await getSetupWithItems(db, userId, 9999); expect(result).toBeNull(); }); }); describe("updateSetup", () => { it("updates setup name, returns updated setup", async () => { const setup = await createSetup(db, userId, { name: "Original" }); const updated = await updateSetup(db, userId, setup.id, { name: "Renamed", }); expect(updated).toBeDefined(); expect(updated?.name).toBe("Renamed"); }); it("returns null for non-existent setup", async () => { const result = await updateSetup(db, userId, 9999, { name: "Ghost", }); expect(result).toBeNull(); }); }); describe("deleteSetup", () => { it("removes setup and cascades to setup_items", async () => { const setup = await createSetup(db, userId, { name: "To Delete" }); const item = await createItem(db, userId, { name: "Item", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item.id]); const deleted = await deleteSetup(db, userId, setup.id); expect(deleted).toBe(true); // Setup gone const result = await getSetupWithItems(db, userId, setup.id); expect(result).toBeNull(); }); it("returns false for non-existent setup", async () => { const result = await deleteSetup(db, userId, 9999); expect(result).toBe(false); }); }); describe("syncSetupItems", () => { it("sets items for a setup (delete-all + re-insert)", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item1 = await createItem(db, userId, { name: "Item 1", categoryId: 1, }); const item2 = await createItem(db, userId, { name: "Item 2", categoryId: 1, }); const item3 = await createItem(db, userId, { name: "Item 3", categoryId: 1, }); // Initial sync await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); let result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(2); // Re-sync with different items await syncSetupItems(db, userId, setup.id, [item2.id, item3.id]); result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(2); const names = result?.items.map((i: any) => i.name).sort(); expect(names).toEqual(["Item 2", "Item 3"]); }); it("syncing with empty array clears all items", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item = await createItem(db, userId, { name: "Item", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item.id]); await syncSetupItems(db, userId, setup.id, []); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(0); }); }); describe("removeSetupItem", () => { it("removes single item from setup", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item1 = await createItem(db, userId, { name: "Item 1", categoryId: 1, }); const item2 = await createItem(db, userId, { name: "Item 2", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); await removeSetupItem(db, userId, setup.id, item1.id); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Item 2"); }); }); describe("getSetupWithItems - classification", () => { it("returns classification field defaulting to 'base' for each item", async () => { const setup = await createSetup(db, userId, { name: "Day Hike" }); const item = await createItem(db, userId, { name: "Water Bottle", categoryId: 1, weightGrams: 200, priceCents: 2500, }); await syncSetupItems(db, userId, setup.id, [item.id]); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].classification).toBe("base"); }); }); describe("syncSetupItems - classification preservation", () => { it("preserves existing classifications when re-syncing items", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item1 = await createItem(db, userId, { name: "Tent", categoryId: 1, }); const item2 = await createItem(db, userId, { name: "Jacket", categoryId: 1, }); const item3 = await createItem(db, userId, { name: "Stove", categoryId: 1, }); // Initial sync await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); // Change classifications await updateItemClassification(db, userId, setup.id, item1.id, "worn"); await updateItemClassification( db, userId, setup.id, item2.id, "consumable", ); // Re-sync with item2 kept and item3 added (item1 removed) await syncSetupItems(db, userId, setup.id, [item2.id, item3.id]); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(2); const item2Result = result?.items.find((i: any) => i.name === "Jacket"); const item3Result = result?.items.find((i: any) => i.name === "Stove"); expect(item2Result?.classification).toBe("consumable"); expect(item3Result?.classification).toBe("base"); }); it("assigns 'base' to newly added items with no prior classification", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item1 = await createItem(db, userId, { name: "Item 1", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item1.id]); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("base"); }); }); describe("updateItemClassification", () => { it("sets classification for a specific item in a specific setup", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item = await createItem(db, userId, { name: "Tent", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item.id]); await updateItemClassification(db, userId, setup.id, item.id, "worn"); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("worn"); }); it("changes item from default 'base' to 'worn'", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item = await createItem(db, userId, { name: "Jacket", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item.id]); // Verify default let result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("base"); // Update await updateItemClassification(db, userId, setup.id, item.id, "worn"); result = await getSetupWithItems(db, userId, setup.id); expect(result?.items[0].classification).toBe("worn"); }); it("same item in two different setups can have different classifications", async () => { const setup1 = await createSetup(db, userId, { name: "Hiking" }); const setup2 = await createSetup(db, userId, { name: "Biking" }); const item = await createItem(db, userId, { name: "Jacket", categoryId: 1, }); await syncSetupItems(db, userId, setup1.id, [item.id]); await syncSetupItems(db, userId, setup2.id, [item.id]); await updateItemClassification(db, userId, setup1.id, item.id, "worn"); await updateItemClassification(db, userId, setup2.id, item.id, "base"); const result1 = await getSetupWithItems(db, userId, setup1.id); const result2 = await getSetupWithItems(db, userId, setup2.id); expect(result1?.items[0].classification).toBe("worn"); expect(result2?.items[0].classification).toBe("base"); }); }); describe("cascade behavior", () => { it("deleting a collection item removes it from all setups", async () => { const setup = await createSetup(db, userId, { name: "Kit" }); const item1 = await createItem(db, userId, { name: "Item 1", categoryId: 1, }); const item2 = await createItem(db, userId, { name: "Item 2", categoryId: 1, }); await syncSetupItems(db, userId, setup.id, [item1.id, item2.id]); // Delete item1 from collection await db.delete(itemsTable).where(eq(itemsTable.id, item1.id)); const result = await getSetupWithItems(db, userId, setup.id); expect(result?.items).toHaveLength(1); expect(result?.items[0].name).toBe("Item 2"); }); }); describe("cross-user isolation", () => { it("user cannot see other user's setups", async () => { const userId2 = await createSecondTestUser(db); await createSetup(db, userId, { name: "User 1 Setup" }); await createSetup(db, userId2, { name: "User 2 Setup" }); const user1Setups = await getAllSetups(db, userId); const user2Setups = await getAllSetups(db, userId2); expect(user1Setups).toHaveLength(1); expect(user1Setups[0].name).toBe("User 1 Setup"); expect(user2Setups).toHaveLength(1); expect(user2Setups[0].name).toBe("User 2 Setup"); }); }); });