Files
GearBox/tests/routes/items.test.ts
Jean-Luc Makiola 5085d8e3f7 feat(16-04): update route tests and MCP tests for multi-user userId
- All 8 route test files destructure { db, userId } from createTestDb()
- All route test middleware sets c.set("userId", userId)
- MCP tools.test.ts passes userId to all registerXTools(db, userId) calls
- MCP tools.test.ts passes userId to getCollectionSummary(db, userId)
- Added 4 cross-user isolation tests for MCP tools (items, item by ID, threads, collection summary)
- OAuth test db type annotation updated for new createTestDb return shape
- Images test now uses createTestDb with userId context

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-05 11:31:05 +02:00

209 lines
5.6 KiB
TypeScript

import { beforeEach, describe, expect, it } from "bun:test";
import { Hono } from "hono";
import { categoryRoutes } from "../../src/server/routes/categories.ts";
import { itemRoutes } from "../../src/server/routes/items.ts";
import { createTestDb } from "../helpers/db.ts";
function createTestApp() {
const { db, userId } = createTestDb();
const app = new Hono();
// Inject test DB and userId into context for all routes
app.use("*", async (c, next) => {
c.set("db", db);
c.set("userId", userId);
await next();
});
app.route("/api/items", itemRoutes);
app.route("/api/categories", categoryRoutes);
return { app, db, userId };
}
describe("Item Routes", () => {
let app: Hono;
beforeEach(() => {
const testApp = createTestApp();
app = testApp.app;
});
it("POST /api/items with valid data returns 201", async () => {
const res = await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Tent",
weightGrams: 1200,
priceCents: 35000,
categoryId: 1,
}),
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.name).toBe("Tent");
expect(body.id).toBeGreaterThan(0);
});
it("POST /api/items with missing name returns 400", async () => {
const res = await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ categoryId: 1 }),
});
expect(res.status).toBe(400);
});
it("GET /api/items returns array", async () => {
// Create an item first
await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Tent", categoryId: 1 }),
});
const res = await app.request("/api/items");
expect(res.status).toBe(200);
const body = await res.json();
expect(Array.isArray(body)).toBe(true);
expect(body.length).toBeGreaterThanOrEqual(1);
});
it("PUT /api/items/:id updates fields", async () => {
// Create first
const createRes = await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Tent",
weightGrams: 1200,
categoryId: 1,
}),
});
const created = await createRes.json();
// Update
const res = await app.request(`/api/items/${created.id}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Big Agnes Tent", weightGrams: 1100 }),
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.name).toBe("Big Agnes Tent");
expect(body.weightGrams).toBe(1100);
});
it("DELETE /api/items/:id returns success", async () => {
// Create first
const createRes = await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Tent", categoryId: 1 }),
});
const created = await createRes.json();
const res = await app.request(`/api/items/${created.id}`, {
method: "DELETE",
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.success).toBe(true);
});
it("GET /api/items/:id returns 404 for non-existent item", async () => {
const res = await app.request("/api/items/9999");
expect(res.status).toBe(404);
});
it("POST /api/items/:id/duplicate returns 201 with the copy", async () => {
const createRes = await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: "Tent", categoryId: 1, weightGrams: 1200 }),
});
const created = await createRes.json();
const res = await app.request(`/api/items/${created.id}/duplicate`, {
method: "POST",
});
expect(res.status).toBe(201);
const body = await res.json();
expect(body.name).toBe("Tent (copy)");
expect(body.weightGrams).toBe(1200);
expect(body.id).not.toBe(created.id);
});
it("POST /api/items/999/duplicate returns 404", async () => {
const res = await app.request("/api/items/999/duplicate", {
method: "POST",
});
expect(res.status).toBe(404);
});
it("GET /api/items/export returns CSV with correct content-type", async () => {
// Create an item first
await app.request("/api/items", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
name: "Tent",
weightGrams: 1200,
priceCents: 35000,
categoryId: 1,
}),
});
const res = await app.request("/api/items/export");
expect(res.status).toBe(200);
expect(res.headers.get("Content-Type")).toContain("text/csv");
const text = await res.text();
const lines = text.split("\n");
expect(lines[0]).toBe(
"name,quantity,weightGrams,priceCents,category,notes,productUrl",
);
expect(lines.length).toBeGreaterThanOrEqual(2);
expect(lines[1]).toContain("Tent");
});
it("POST /api/items/import with CSV file creates items", async () => {
const csvContent = [
"name,quantity,weightGrams,priceCents,category,notes,productUrl",
"Sleeping Bag,1,800,25000,Camping,,",
].join("\n");
const formData = new FormData();
formData.append(
"file",
new Blob([csvContent], { type: "text/csv" }),
"import.csv",
);
const res = await app.request("/api/items/import", {
method: "POST",
body: formData,
});
expect(res.status).toBe(200);
const body = await res.json();
expect(body.imported).toBe(1);
expect(body.errors).toHaveLength(0);
});
it("POST /api/items/import with no file returns 400", async () => {
const res = await app.request("/api/items/import", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
expect(res.status).toBe(400);
});
});