From 5085d8e3f7162b10b505a79039dd72d539203582 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 5 Apr 2026 11:31:05 +0200 Subject: [PATCH] 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) --- tests/mcp/tools.test.ts | 161 +++++++++++++++++++++++++------- tests/routes/auth.test.ts | 7 +- tests/routes/categories.test.ts | 7 +- tests/routes/images.test.ts | 7 ++ tests/routes/items.test.ts | 7 +- tests/routes/oauth.test.ts | 16 ++-- tests/routes/params.test.ts | 3 +- tests/routes/setups.test.ts | 5 +- tests/routes/threads.test.ts | 7 +- 9 files changed, 165 insertions(+), 55 deletions(-) diff --git a/tests/mcp/tools.test.ts b/tests/mcp/tools.test.ts index 66ad1fd..60699bf 100644 --- a/tests/mcp/tools.test.ts +++ b/tests/mcp/tools.test.ts @@ -4,7 +4,7 @@ import { registerCategoryTools } from "../../src/server/mcp/tools/categories.ts" import { registerItemTools } from "../../src/server/mcp/tools/items.ts"; import { registerSetupTools } from "../../src/server/mcp/tools/setups.ts"; import { registerThreadTools } from "../../src/server/mcp/tools/threads.ts"; -import { createTestDb } from "../helpers/db.ts"; +import { createSecondTestUser, createTestDb } from "../helpers/db.ts"; function parseResult(result: { content: Array<{ type: string; text: string }>; @@ -14,16 +14,16 @@ function parseResult(result: { describe("MCP Item Tools", () => { test("list_items returns array", async () => { - const db = createTestDb(); - const tools = registerItemTools(db); + const { db, userId } = createTestDb(); + const tools = registerItemTools(db, userId); const result = await tools.list_items({}); const data = parseResult(result); expect(Array.isArray(data)).toBe(true); }); test("create_item creates and returns item", async () => { - const db = createTestDb(); - const tools = registerItemTools(db); + const { db, userId } = createTestDb(); + const tools = registerItemTools(db, userId); const result = await tools.create_item({ name: "Test Tent", categoryId: 1, @@ -38,8 +38,8 @@ describe("MCP Item Tools", () => { }); test("get_item retrieves by ID", async () => { - const db = createTestDb(); - const tools = registerItemTools(db); + const { db, userId } = createTestDb(); + const tools = registerItemTools(db, userId); const created = parseResult( await tools.create_item({ name: "Sleeping Bag", categoryId: 1 }), ); @@ -50,16 +50,16 @@ describe("MCP Item Tools", () => { }); test("get_item returns error for missing item", async () => { - const db = createTestDb(); - const tools = registerItemTools(db); + const { db, userId } = createTestDb(); + const tools = registerItemTools(db, userId); const result = await tools.get_item({ id: 999 }); const data = parseResult(result); expect(data.error).toContain("not found"); }); test("delete_item removes item", async () => { - const db = createTestDb(); - const tools = registerItemTools(db); + const { db, userId } = createTestDb(); + const tools = registerItemTools(db, userId); const created = parseResult( await tools.create_item({ name: "To Delete", categoryId: 1 }), ); @@ -76,8 +76,8 @@ describe("MCP Item Tools", () => { describe("MCP Category Tools", () => { test("list_categories returns array with Uncategorized", async () => { - const db = createTestDb(); - const tools = registerCategoryTools(db); + const { db, userId } = createTestDb(); + const tools = registerCategoryTools(db, userId); const result = await tools.list_categories(); const data = parseResult(result); expect(Array.isArray(data)).toBe(true); @@ -86,8 +86,8 @@ describe("MCP Category Tools", () => { }); test("create_category creates a new category", async () => { - const db = createTestDb(); - const tools = registerCategoryTools(db); + const { db, userId } = createTestDb(); + const tools = registerCategoryTools(db, userId); const result = await tools.create_category({ name: "Shelter", icon: "tent", @@ -100,8 +100,8 @@ describe("MCP Category Tools", () => { describe("MCP Thread Tools", () => { test("create_thread starts a thread with status active", async () => { - const db = createTestDb(); - const tools = registerThreadTools(db); + const { db, userId } = createTestDb(); + const tools = registerThreadTools(db, userId); const result = await tools.create_thread({ name: "Handlebar Bag", categoryId: 1, @@ -112,8 +112,8 @@ describe("MCP Thread Tools", () => { }); test("add_candidate adds to thread", async () => { - const db = createTestDb(); - const tools = registerThreadTools(db); + const { db, userId } = createTestDb(); + const tools = registerThreadTools(db, userId); const thread = parseResult( await tools.create_thread({ name: "Saddle Bag", categoryId: 1 }), ); @@ -132,9 +132,9 @@ describe("MCP Thread Tools", () => { }); test("resolve_thread picks winner and creates item", async () => { - const db = createTestDb(); - const threadTools = registerThreadTools(db); - const itemTools = registerItemTools(db); + const { db, userId } = createTestDb(); + const threadTools = registerThreadTools(db, userId); + const itemTools = registerItemTools(db, userId); // Create thread with two candidates const thread = parseResult( @@ -179,8 +179,8 @@ describe("MCP Thread Tools", () => { describe("MCP Setup Tools", () => { test("create_setup and list_setups", async () => { - const db = createTestDb(); - const tools = registerSetupTools(db); + const { db, userId } = createTestDb(); + const tools = registerSetupTools(db, userId); await tools.create_setup({ name: "Weekend Trip" }); const result = await tools.list_setups(); const data = parseResult(result); @@ -189,9 +189,9 @@ describe("MCP Setup Tools", () => { }); test("get_setup returns setup with items", async () => { - const db = createTestDb(); - const setupTools = registerSetupTools(db); - const itemTools = registerItemTools(db); + const { db, userId } = createTestDb(); + const setupTools = registerSetupTools(db, userId); + const itemTools = registerItemTools(db, userId); const setup = parseResult( await setupTools.create_setup({ name: "Overnighter" }), @@ -211,9 +211,9 @@ describe("MCP Setup Tools", () => { describe("MCP Collection Summary Resource", () => { test("returns overview with correct counts", () => { - const db = createTestDb(); + const { db, userId } = createTestDb(); - const summary = getCollectionSummary(db); + const summary = getCollectionSummary(db, userId); expect(summary.overview).toBeDefined(); expect(summary.overview.totalItems).toBe(0); expect(summary.overview.categoryCount).toBe(1); // Uncategorized @@ -223,9 +223,9 @@ describe("MCP Collection Summary Resource", () => { }); test("reflects items and threads after creation", async () => { - const db = createTestDb(); - const itemTools = registerItemTools(db); - const threadTools = registerThreadTools(db); + const { db, userId } = createTestDb(); + const itemTools = registerItemTools(db, userId); + const threadTools = registerThreadTools(db, userId); await itemTools.create_item({ name: "Tent", @@ -242,7 +242,7 @@ describe("MCP Collection Summary Resource", () => { categoryId: 1, }); - const summary = getCollectionSummary(db); + const summary = getCollectionSummary(db, userId); expect(summary.overview.totalItems).toBe(2); expect(summary.overview.totalWeightGrams).toBe(2000); expect(summary.overview.activeThreadCount).toBe(1); @@ -251,3 +251,98 @@ describe("MCP Collection Summary Resource", () => { expect(summary.activeThreads[0].name).toBe("Cook System"); }); }); + +describe("MCP Cross-User Isolation", () => { + test("user 2 cannot see user 1's items via MCP tools", async () => { + const { db, userId } = createTestDb(); + const userId2 = createSecondTestUser(db); + + const user1Tools = registerItemTools(db, userId); + const user2Tools = registerItemTools(db, userId2); + + // User 1 creates an item + await user1Tools.create_item({ + name: "User 1 Tent", + categoryId: 1, + weightGrams: 1200, + }); + + // User 2 creates an item + await user2Tools.create_item({ + name: "User 2 Sleeping Bag", + categoryId: 1, + weightGrams: 800, + }); + + // Each user only sees their own items + const user1Items = parseResult(await user1Tools.list_items({})); + const user2Items = parseResult(await user2Tools.list_items({})); + + expect(user1Items).toHaveLength(1); + expect(user1Items[0].name).toBe("User 1 Tent"); + expect(user2Items).toHaveLength(1); + expect(user2Items[0].name).toBe("User 2 Sleeping Bag"); + }); + + test("user 2 cannot access user 1's item by ID", async () => { + const { db, userId } = createTestDb(); + const userId2 = createSecondTestUser(db); + + const user1Tools = registerItemTools(db, userId); + const user2Tools = registerItemTools(db, userId2); + + const created = parseResult( + await user1Tools.create_item({ + name: "Private Item", + categoryId: 1, + }), + ); + + // User 2 tries to get user 1's item + const result = await user2Tools.get_item({ id: created.id }); + const data = parseResult(result); + expect(data.error).toContain("not found"); + }); + + test("user 2 cannot see user 1's threads via MCP tools", async () => { + const { db, userId } = createTestDb(); + const userId2 = createSecondTestUser(db); + + const user1Tools = registerThreadTools(db, userId); + const user2Tools = registerThreadTools(db, userId2); + + await user1Tools.create_thread({ + name: "User 1 Thread", + categoryId: 1, + }); + + const user1Threads = parseResult( + await user1Tools.list_threads({ includeResolved: false }), + ); + const user2Threads = parseResult( + await user2Tools.list_threads({ includeResolved: false }), + ); + + expect(user1Threads).toHaveLength(1); + expect(user1Threads[0].name).toBe("User 1 Thread"); + expect(user2Threads).toHaveLength(0); + }); + + test("collection summary is scoped to user", async () => { + const { db, userId } = createTestDb(); + const userId2 = createSecondTestUser(db); + + const user1Tools = registerItemTools(db, userId); + await user1Tools.create_item({ + name: "User 1 Item", + categoryId: 1, + weightGrams: 500, + }); + + const user1Summary = getCollectionSummary(db, userId); + const user2Summary = getCollectionSummary(db, userId2); + + expect(user1Summary.overview.totalItems).toBe(1); + expect(user2Summary.overview.totalItems).toBe(0); + }); +}); diff --git a/tests/routes/auth.test.ts b/tests/routes/auth.test.ts index fb56130..c6eb10a 100644 --- a/tests/routes/auth.test.ts +++ b/tests/routes/auth.test.ts @@ -4,16 +4,17 @@ import { authRoutes } from "../../src/server/routes/auth.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); - const app = new Hono<{ Variables: { db?: any } }>(); + const { db, userId } = createTestDb(); + const app = new Hono<{ Variables: { db?: any; userId?: number } }>(); app.use("*", async (c, next) => { c.set("db", db); + c.set("userId", userId); await next(); }); app.route("/api/auth", authRoutes); - return { app, db }; + return { app, db, userId }; } describe("Auth Routes", () => { diff --git a/tests/routes/categories.test.ts b/tests/routes/categories.test.ts index 37bcff9..9ea3ac9 100644 --- a/tests/routes/categories.test.ts +++ b/tests/routes/categories.test.ts @@ -5,18 +5,19 @@ import { itemRoutes } from "../../src/server/routes/items.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); + const { db, userId } = createTestDb(); const app = new Hono(); - // Inject test DB into context for all routes + // 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/categories", categoryRoutes); app.route("/api/items", itemRoutes); - return { app, db }; + return { app, db, userId }; } describe("Category Routes", () => { diff --git a/tests/routes/images.test.ts b/tests/routes/images.test.ts index ee61ae9..97d1670 100644 --- a/tests/routes/images.test.ts +++ b/tests/routes/images.test.ts @@ -1,8 +1,15 @@ import { describe, expect, test } from "bun:test"; import { Hono } from "hono"; import { imageRoutes } from "../../src/server/routes/images"; +import { createTestDb } from "../helpers/db.ts"; +const { db, userId } = createTestDb(); const app = new Hono(); +app.use("*", async (c, next) => { + c.set("db", db); + c.set("userId", userId); + await next(); +}); app.route("/api/images", imageRoutes); describe("POST /api/images/from-url", () => { diff --git a/tests/routes/items.test.ts b/tests/routes/items.test.ts index c0f5b8b..0c3ac24 100644 --- a/tests/routes/items.test.ts +++ b/tests/routes/items.test.ts @@ -5,18 +5,19 @@ import { itemRoutes } from "../../src/server/routes/items.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); + const { db, userId } = createTestDb(); const app = new Hono(); - // Inject test DB into context for all routes + // 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 }; + return { app, db, userId }; } describe("Item Routes", () => { diff --git a/tests/routes/oauth.test.ts b/tests/routes/oauth.test.ts index 9f8a648..3c17537 100644 --- a/tests/routes/oauth.test.ts +++ b/tests/routes/oauth.test.ts @@ -7,28 +7,30 @@ import { createUser } from "../../src/server/services/auth.service.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); - const app = new Hono<{ Variables: { db?: any } }>(); + const { db, userId } = createTestDb(); + const app = new Hono<{ Variables: { db?: any; userId?: number } }>(); app.use("*", async (c, next) => { c.set("db", db); + c.set("userId", userId); await next(); }); app.route("/.well-known", wellKnownRoute); app.route("/oauth", oauthRoutes); - return { app, db }; + return { app, db, userId }; } function createFullTestApp() { - const db = createTestDb(); - const app = new Hono<{ Variables: { db?: any } }>(); + const { db, userId } = createTestDb(); + const app = new Hono<{ Variables: { db?: any; userId?: number } }>(); app.use("*", async (c, next) => { c.set("db", db); + c.set("userId", userId); await next(); }); app.route("/.well-known", wellKnownRoute); app.route("/oauth", oauthRoutes); app.route("/mcp", mcpRoutes); - return { app, db }; + return { app, db, userId }; } function generatePkce() { @@ -39,7 +41,7 @@ function generatePkce() { describe("OAuth Routes", () => { let app: Hono; - let db: ReturnType; + let db: ReturnType["db"]; beforeEach(async () => { const testApp = createTestApp(); diff --git a/tests/routes/params.test.ts b/tests/routes/params.test.ts index 435697f..58da9c2 100644 --- a/tests/routes/params.test.ts +++ b/tests/routes/params.test.ts @@ -7,10 +7,11 @@ import { threadRoutes } from "../../src/server/routes/threads"; import { createTestDb } from "../helpers/db"; function createTestApp() { - const db = createTestDb(); + const { db, userId } = createTestDb(); const app = new Hono(); app.use("*", async (c, next) => { c.set("db", db); + c.set("userId", userId); await next(); }); app.route("/api/items", itemRoutes); diff --git a/tests/routes/setups.test.ts b/tests/routes/setups.test.ts index cb121f6..fb649f8 100644 --- a/tests/routes/setups.test.ts +++ b/tests/routes/setups.test.ts @@ -5,17 +5,18 @@ import { setupRoutes } from "../../src/server/routes/setups.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); + const { db, userId } = createTestDb(); const app = new Hono(); app.use("*", async (c, next) => { c.set("db", db); + c.set("userId", userId); await next(); }); app.route("/api/setups", setupRoutes); app.route("/api/items", itemRoutes); - return { app, db }; + return { app, db, userId }; } async function createSetupViaAPI(app: Hono, name: string) { diff --git a/tests/routes/threads.test.ts b/tests/routes/threads.test.ts index 7c2feda..603bddd 100644 --- a/tests/routes/threads.test.ts +++ b/tests/routes/threads.test.ts @@ -4,17 +4,18 @@ import { threadRoutes } from "../../src/server/routes/threads.ts"; import { createTestDb } from "../helpers/db.ts"; function createTestApp() { - const db = createTestDb(); + const { db, userId } = createTestDb(); const app = new Hono(); - // Inject test DB into context for all routes + // 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/threads", threadRoutes); - return { app, db }; + return { app, db, userId }; } async function createThreadViaAPI(app: Hono, name: string, categoryId = 1) {