import { beforeEach, describe, expect, it } from "bun:test"; import { globalItems, itemGlobalLinks, items } from "../../src/db/schema.ts"; import { getGlobalItemWithOwnerCount, linkItemToGlobal, searchGlobalItems, unlinkItemFromGlobal, } from "../../src/server/services/global-item.service.ts"; import { seedGlobalItems } from "../../src/db/seed-global-items.ts"; import { createTestDb } from "../helpers/db.ts"; type TestDb = ReturnType; function insertGlobalItem( db: TestDb, data: { brand: string; model: string; category?: string; weightGrams?: number; priceCents?: number; }, ) { return db .insert(globalItems) .values({ brand: data.brand, model: data.model, category: data.category ?? null, weightGrams: data.weightGrams ?? null, priceCents: data.priceCents ?? null, }) .returning() .get(); } function insertItem(db: TestDb, name: string) { return db .insert(items) .values({ name, categoryId: 1 }) .returning() .get(); } describe("Global Item Service", () => { let db: TestDb; beforeEach(() => { db = createTestDb(); }); describe("searchGlobalItems", () => { it("returns all global items when no query provided", () => { insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db); expect(results).toHaveLength(2); }); it("returns items matching brand (case-insensitive)", () => { insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "revelate"); expect(results).toHaveLength(1); expect(results[0].brand).toBe("Revelate Designs"); }); it("returns items matching model (case-insensitive)", () => { insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "HANDLEBAR"); expect(results).toHaveLength(1); expect(results[0].model).toBe("Handlebar Pack"); }); it("does not match everything with wildcard chars", () => { insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System" }); insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack" }); const results = searchGlobalItems(db, "100%"); expect(results).toHaveLength(0); }); }); describe("getGlobalItemWithOwnerCount", () => { it("returns item with ownerCount 0 when no links", () => { const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); const result = getGlobalItemWithOwnerCount(db, gi.id); expect(result).not.toBeNull(); expect(result!.ownerCount).toBe(0); expect(result!.brand).toBe("MSR"); }); it("returns ownerCount matching number of linked items", () => { const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); const item1 = insertItem(db, "My Stove"); const item2 = insertItem(db, "Another Stove"); db.insert(itemGlobalLinks) .values({ itemId: item1.id, globalItemId: gi.id }) .run(); db.insert(itemGlobalLinks) .values({ itemId: item2.id, globalItemId: gi.id }) .run(); const result = getGlobalItemWithOwnerCount(db, gi.id); expect(result).not.toBeNull(); expect(result!.ownerCount).toBe(2); }); it("returns null for non-existent id", () => { const result = getGlobalItemWithOwnerCount(db, 9999); expect(result).toBeNull(); }); }); describe("linkItemToGlobal", () => { it("creates link and returns link row", () => { const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); const item = insertItem(db, "My Stove"); const link = linkItemToGlobal(db, item.id, gi.id); expect(link.itemId).toBe(item.id); expect(link.globalItemId).toBe(gi.id); }); it("throws when item already linked", () => { const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); const item = insertItem(db, "My Stove"); linkItemToGlobal(db, item.id, gi.id); expect(() => linkItemToGlobal(db, item.id, gi.id)).toThrow(); }); }); describe("unlinkItemFromGlobal", () => { it("removes the link", () => { const gi = insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2" }); const item = insertItem(db, "My Stove"); linkItemToGlobal(db, item.id, gi.id); const deleted = unlinkItemFromGlobal(db, item.id); expect(deleted).toBe(1); // Verify link is gone const result = getGlobalItemWithOwnerCount(db, gi.id); expect(result!.ownerCount).toBe(0); }); }); describe("seedGlobalItems", () => { it("inserts seed data on first call", () => { seedGlobalItems(db); const all = db.select().from(globalItems).all(); expect(all.length).toBeGreaterThan(0); }); it("is idempotent on second call", () => { seedGlobalItems(db); const countAfterFirst = db.select().from(globalItems).all().length; seedGlobalItems(db); const countAfterSecond = db.select().from(globalItems).all().length; expect(countAfterSecond).toBe(countAfterFirst); }); }); });