import { beforeEach, describe, expect, it } from "bun:test"; import { globalItemTags, globalItems, items, tags, } from "../../src/db/schema.ts"; import { seedGlobalItems } from "../../src/db/seed-global-items.ts"; import { getGlobalItemWithOwnerCount, searchGlobalItems, } from "../../src/server/services/global-item.service.ts"; import { createTestDb } from "../helpers/db.ts"; type TestDb = Awaited>; async function insertGlobalItem( db: TestDb["db"], data: { brand: string; model: string; category?: string; weightGrams?: number; priceCents?: number; }, ) { const [row] = await db .insert(globalItems) .values({ brand: data.brand, model: data.model, category: data.category ?? null, weightGrams: data.weightGrams ?? null, priceCents: data.priceCents ?? null, }) .returning(); return row; } async function insertItem( db: TestDb["db"], name: string, userId: number, opts?: { globalItemId?: number }, ) { const [row] = await db .insert(items) .values({ name, categoryId: 1, userId, globalItemId: opts?.globalItemId }) .returning(); return row; } async function insertTag(db: TestDb["db"], name: string) { const [row] = await db.insert(tags).values({ name }).returning(); return row; } async function tagGlobalItem( db: TestDb["db"], globalItemId: number, tagId: number, ) { await db.insert(globalItemTags).values({ globalItemId, tagId }); } describe("Global Item Service", () => { let db: TestDb["db"]; let userId: number; beforeEach(async () => { const testDb = await createTestDb(); db = testDb.db; userId = testDb.userId; }); describe("searchGlobalItems", () => { it("returns all global items when no query provided", async () => { await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const results = await searchGlobalItems(db); expect(results).toHaveLength(2); }); it("returns items matching brand (case-insensitive)", async () => { await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const results = await searchGlobalItems(db, "revelate"); expect(results).toHaveLength(1); expect(results[0].brand).toBe("Revelate Designs"); }); it("returns items matching model (case-insensitive)", async () => { await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const results = await searchGlobalItems(db, "HANDLEBAR"); expect(results).toHaveLength(1); expect(results[0].model).toBe("Handlebar Pack"); }); it("does not match everything with wildcard chars", async () => { await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const results = await searchGlobalItems(db, "100%"); expect(results).toHaveLength(0); }); it("returns all items when no tags provided", async () => { await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const results = await searchGlobalItems(db, undefined, undefined); expect(results).toHaveLength(2); }); it("filters by single tag", async () => { const gi1 = await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); const gi2 = await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const tag = await insertTag(db, "ultralight"); await tagGlobalItem(db, gi1.id, tag.id); const results = await searchGlobalItems(db, undefined, ["ultralight"]); expect(results).toHaveLength(1); expect(results[0].brand).toBe("Revelate Designs"); }); it("filters by multiple tags with AND logic", async () => { const gi1 = await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); const gi2 = await insertGlobalItem(db, { brand: "Apidura", model: "Handlebar Pack", }); const tagUL = await insertTag(db, "ultralight"); const tagBP = await insertTag(db, "bikepacking"); // gi1 has both tags await tagGlobalItem(db, gi1.id, tagUL.id); await tagGlobalItem(db, gi1.id, tagBP.id); // gi2 has only bikepacking await tagGlobalItem(db, gi2.id, tagBP.id); const results = await searchGlobalItems(db, undefined, [ "ultralight", "bikepacking", ]); expect(results).toHaveLength(1); expect(results[0].brand).toBe("Revelate Designs"); }); it("combines text search and tag filtering", async () => { const gi1 = await insertGlobalItem(db, { brand: "Revelate Designs", model: "Terrapin System", }); const gi2 = await insertGlobalItem(db, { brand: "Revelate Designs", model: "Spinelock", }); const tag = await insertTag(db, "bikepacking"); await tagGlobalItem(db, gi1.id, tag.id); await tagGlobalItem(db, gi2.id, tag.id); // Both tagged bikepacking, but only one matches "terrapin" const results = await searchGlobalItems(db, "terrapin", [ "bikepacking", ]); expect(results).toHaveLength(1); expect(results[0].model).toBe("Terrapin System"); }); }); describe("getGlobalItemWithOwnerCount", () => { it("returns item with ownerCount 0 when no items reference it", async () => { const gi = await insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2", }); const result = await getGlobalItemWithOwnerCount(db, gi.id); expect(result).not.toBeNull(); expect(result!.ownerCount).toBe(0); expect(result!.brand).toBe("MSR"); }); it("returns ownerCount matching number of items with globalItemId", async () => { const gi = await insertGlobalItem(db, { brand: "MSR", model: "PocketRocket 2", }); await insertItem(db, "My Stove", userId, { globalItemId: gi.id }); await insertItem(db, "Another Stove", userId, { globalItemId: gi.id, }); const result = await getGlobalItemWithOwnerCount(db, gi.id); expect(result).not.toBeNull(); expect(result!.ownerCount).toBe(2); }); it("returns null for non-existent id", async () => { const result = await getGlobalItemWithOwnerCount(db, 9999); expect(result).toBeNull(); }); }); describe("seedGlobalItems", () => { it("inserts seed data on first call", async () => { await seedGlobalItems(db); const all = await db.select().from(globalItems); expect(all.length).toBeGreaterThan(0); }); it("is idempotent on second call", async () => { await seedGlobalItems(db); const countAfterFirst = (await db.select().from(globalItems)).length; await seedGlobalItems(db); const countAfterSecond = (await db.select().from(globalItems)).length; expect(countAfterSecond).toBe(countAfterFirst); }); }); });