import { beforeEach, describe, expect, it } from "bun:test"; import { Hono } from "hono"; import { globalItems, globalItemTags, items, manufacturers, tags, } from "../../src/db/schema.ts"; import { globalItemRoutes } from "../../src/server/routes/global-items.ts"; import { createTestDb } from "../helpers/db.ts"; type TestDb = Awaited>; async function createTestApp() { const { db, userId } = await createTestDb(); const app = new Hono(); app.use("*", async (c, next) => { c.set("db", db); c.set("userId", userId); await next(); }); app.route("/api/global-items", globalItemRoutes); return { app, db, userId }; } async function insertManufacturer(db: TestDb["db"], name: string) { const slug = name.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""); const [row] = await db .insert(manufacturers) .values({ name, slug, website: `https://${slug}.com` }) .onConflictDoUpdate({ target: manufacturers.slug, set: { name } }) .returning(); return row!; } async function insertGlobalItem( db: TestDb["db"], brand: string, model: string, ) { const m = await insertManufacturer(db, brand); const [row] = await db .insert(globalItems) .values({ manufacturerId: m.id, model, category: "bags" }) .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; } describe("Global Item Routes", () => { let app: Hono; let db: TestDb["db"]; let userId: number; beforeEach(async () => { const testApp = await createTestApp(); app = testApp.app; db = testApp.db; userId = testApp.userId; }); describe("GET /api/global-items", () => { it("returns 200 with all global items", async () => { await insertGlobalItem(db, "Revelate Designs", "Terrapin System"); await insertGlobalItem(db, "Apidura", "Handlebar Pack"); const res = await app.request("/api/global-items"); expect(res.status).toBe(200); const body = await res.json(); expect(body).toHaveLength(2); }); it("filters results by query parameter", async () => { await insertGlobalItem(db, "Revelate Designs", "Terrapin System"); await insertGlobalItem(db, "Apidura", "Handlebar Pack"); const res = await app.request("/api/global-items?q=revelate"); expect(res.status).toBe(200); const body = await res.json(); expect(body).toHaveLength(1); expect(body[0].brand).toBe("Revelate Designs"); }); it("filters results by tags parameter", async () => { const gi1 = await insertGlobalItem( db, "Revelate Designs", "Terrapin System", ); await insertGlobalItem(db, "Apidura", "Handlebar Pack"); const [tag] = await db .insert(tags) .values({ name: "ultralight" }) .returning(); await db .insert(globalItemTags) .values({ globalItemId: gi1.id, tagId: tag.id }); const res = await app.request("/api/global-items?tags=ultralight"); expect(res.status).toBe(200); const body = await res.json(); expect(body).toHaveLength(1); expect(body[0].brand).toBe("Revelate Designs"); }); }); describe("POST /api/global-items", () => { it("returns 200 with item and created=true on new item", async () => { await insertManufacturer(db, "Revelate Designs"); const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ manufacturerSlug: "revelate-designs", model: "Terrapin System", }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.item.model).toBe("Terrapin System"); expect(body.created).toBe(true); }); it("returns 200 with created=false when upserting existing item", async () => { await insertGlobalItem(db, "Revelate Designs", "Terrapin System"); const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ manufacturerSlug: "revelate-designs", model: "Terrapin System", description: "Updated description", }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.created).toBe(false); expect(body.item.description).toBe("Updated description"); }); it("returns 400 when manufacturerSlug is missing", async () => { const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: "Terrapin System" }), }); expect(res.status).toBe(400); }); it("returns 400 when model is missing", async () => { const res = await app.request("/api/global-items", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ manufacturerSlug: "revelate-designs" }), }); expect(res.status).toBe(400); }); }); describe("POST /api/global-items/bulk", () => { it("returns 200 with created/updated counts", async () => { await insertManufacturer(db, "Revelate Designs"); await insertManufacturer(db, "Apidura"); const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, { manufacturerSlug: "apidura", model: "Handlebar Pack" }, ], }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.created).toBe(2); expect(body.updated).toBe(0); expect(body.items).toHaveLength(2); }); it("returns correct counts for mix of new and existing items", async () => { await insertGlobalItem(db, "Revelate Designs", "Terrapin System"); await insertManufacturer(db, "Apidura"); const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, { manufacturerSlug: "apidura", model: "Handlebar Pack" }, ], }), }); expect(res.status).toBe(200); const body = await res.json(); expect(body.created).toBe(1); expect(body.updated).toBe(1); }); it("returns 400 when items array is empty", async () => { const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [] }), }); expect(res.status).toBe(400); }); it("returns 400 when items array exceeds 100", async () => { const items = Array.from({ length: 101 }, (_, i) => ({ manufacturerSlug: `brand${i}`, model: `Model${i}`, })); const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items }), }); expect(res.status).toBe(400); }); it("returns 400 for invalid item in array (missing manufacturerSlug)", async () => { const res = await app.request("/api/global-items/bulk", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ items: [ { manufacturerSlug: "revelate-designs", model: "Terrapin System" }, { model: "Invalid Item without manufacturerSlug" }, ], }), }); expect(res.status).toBe(400); }); }); describe("GET /api/global-items/:id", () => { it("returns item with ownerCount", async () => { const gi = await insertGlobalItem(db, "MSR", "PocketRocket 2"); const res = await app.request(`/api/global-items/${gi.id}`); expect(res.status).toBe(200); const body = await res.json(); expect(body.brand).toBe("MSR"); expect(body.ownerCount).toBe(0); }); it("returns ownerCount from items.globalItemId", async () => { const gi = await insertGlobalItem(db, "MSR", "PocketRocket 2"); await insertItem(db, "My Stove", userId, { globalItemId: gi.id, }); const res = await app.request(`/api/global-items/${gi.id}`); expect(res.status).toBe(200); const body = await res.json(); expect(body.ownerCount).toBe(1); }); it("returns 404 for non-existent id", async () => { const res = await app.request("/api/global-items/999"); expect(res.status).toBe(404); }); it("returns 400 for invalid id", async () => { const res = await app.request("/api/global-items/abc"); expect(res.status).toBe(400); }); }); });