import { beforeEach, describe, expect, it } from "bun:test"; import { items } from "../../src/db/schema.ts"; import { exportItemsCsv, importItemsCsv, } from "../../src/server/services/csv.service.ts"; import { createItem } from "../../src/server/services/item.service.ts"; import { createTestDb } from "../helpers/db.ts"; describe("CSV Service", () => { let db: ReturnType; beforeEach(() => { db = createTestDb(); }); // ── Export ──────────────────────────────────────────────────────────────── describe("exportItemsCsv", () => { it("returns correct headers on empty collection", () => { const csv = exportItemsCsv(db); const lines = csv.split("\n"); expect(lines[0]).toBe( "name,quantity,weightGrams,priceCents,category,notes,productUrl", ); expect(lines).toHaveLength(1); }); it("exports items with correct values", () => { createItem(db, { name: "Tent", weightGrams: 1200, priceCents: 35000, categoryId: 1, notes: "Ultralight", productUrl: "https://example.com/tent", }); const csv = exportItemsCsv(db); const lines = csv.split("\n"); expect(lines).toHaveLength(2); expect(lines[1]).toContain("Tent"); expect(lines[1]).toContain("1200"); expect(lines[1]).toContain("35000"); expect(lines[1]).toContain("Uncategorized"); expect(lines[1]).toContain("Ultralight"); expect(lines[1]).toContain("https://example.com/tent"); }); it("properly escapes fields with commas", () => { createItem(db, { name: "Tent, Ultralight", categoryId: 1, }); const csv = exportItemsCsv(db); const lines = csv.split("\n"); expect(lines[1]).toContain('"Tent, Ultralight"'); }); it("properly escapes fields with double quotes", () => { createItem(db, { name: 'He said "great tent"', categoryId: 1, }); const csv = exportItemsCsv(db); const lines = csv.split("\n"); expect(lines[1]).toContain('"He said ""great tent"""'); }); it("exports multiple items", () => { createItem(db, { name: "Tent", categoryId: 1 }); createItem(db, { name: "Sleeping Bag", categoryId: 1 }); const csv = exportItemsCsv(db); const lines = csv.split("\n"); expect(lines).toHaveLength(3); // header + 2 items }); it("exports quantity correctly", () => { // Insert directly to set quantity > 1 (createItem service defaults to 1) db.insert(items) .values({ name: "Bolt", categoryId: 1, quantity: 4 }) .run(); const csv = exportItemsCsv(db); const lines = csv.split("\n"); const fields = lines[1].split(","); // quantity is second field expect(fields[1]).toBe("4"); }); }); // ── Import ──────────────────────────────────────────────────────────────── describe("importItemsCsv", () => { it("parses a valid CSV and creates items", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", "Tent,1,1200,35000,Camping,Ultralight,https://example.com/tent", "Sleeping Bag,1,800,25000,Camping,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(2); expect(result.errors).toHaveLength(0); }); it("creates missing category and reports it", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", "Helmet,1,350,12000,Cycling,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.createdCategories).toContain("Cycling"); expect(result.errors).toHaveLength(0); }); it("uses existing category (case-insensitive) without creating a duplicate", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", // "uncategorized" should match the seeded "Uncategorized" "Spork,1,,,uncategorized,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.createdCategories).toHaveLength(0); }); it("skips rows with no name and records an error", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", ",1,200,,,", "Tent,1,1200,,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.errors).toHaveLength(1); expect(result.errors[0]).toMatch(/missing required field "name"/); }); it("defaults quantity to 1 when not provided", () => { const csv = [ "name,weightGrams,priceCents,category,notes,productUrl", "Tent,1200,35000,Camping,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.errors).toHaveLength(0); }); it("handles optional fields being empty", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", "Tent,,,,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.errors).toHaveLength(0); }); it("handles quoted fields containing commas", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", '"Tent, Ultralight",1,1200,,,', ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.errors).toHaveLength(0); }); it("returns zero imported on empty CSV", () => { const result = importItemsCsv(db, ""); expect(result.imported).toBe(0); expect(result.errors).toHaveLength(0); }); it("uses Uncategorized when category column is empty", () => { const csv = [ "name,quantity,weightGrams,priceCents,category,notes,productUrl", "Tent,1,,,,", ].join("\n"); const result = importItemsCsv(db, csv); expect(result.imported).toBe(1); expect(result.createdCategories).toHaveLength(0); }); }); });