import assert from "node:assert/strict" import test from "node:test" import { createHash } from "node:crypto" import Fastify from "fastify" import { drizzle } from "drizzle-orm/pg-proxy" import mcpRoutes from "../src/routes/mcp" import authPlugin from "../src/plugins/auth" import { mcpToolMap } from "../src/mcp/registry" import { parseUploadQuery, storeMcpFileUpload } from "../src/mcp/fileUploads" import { MCP_FILE_LIMIT } from "../src/mcp/documentFiles" import { secrets } from "../src/utils/secrets" const id = "00000000-0000-4000-8000-000000000001" const pdf = Buffer.from("%PDF-1.7\nMCP fixture\n%%EOF") const bearer = "fedeo_mcp_test_upload_token" const headers = { authorization: `Bearer ${bearer}` } async function app(t: any) { const uploads: any[] = [] const db = drizzle(async (sql, params) => { if (sql.includes('from "m2m_api_keys"')) { assert.ok(params.includes(createHash("sha256").update(bearer).digest("hex"))) return { rows: [[id, 7, id, true, null, "test@example.invalid", true]] } } if (sql.startsWith('update "m2m_api_keys"')) return { rows: [] } if (sql.includes('from "auth_users"')) return { rows: [[true]] } if (sql.includes('from "tenants"')) return { rows: [[null, null]] } return { rows: [] } }) const server = Fastify() t.after(() => server.close()) server.decorate("db", db as any) await server.register(authPlugin) await server.register(mcpRoutes, { prefix: "/api", storeUpload: async (context: any, upload: any) => { uploads.push({ tenantId: context.tenantId, ...upload }); return { stored: true } }, }) return { server, uploads } } const multipartBody = (filename: string, content: Buffer, field = "file") => { const boundary = "----fedeo-test" const payload = Buffer.concat([ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${field}"; filename="${filename}"\r\nContent-Type: application/pdf\r\n\r\n`), content, Buffer.from(`\r\n--${boundary}--\r\n`), ]) return { payload, headers: { ...headers, "content-type": `multipart/form-data; boundary=${boundary}` } } } test("binary endpoint accepts multipart and raw uploads without Base64", async (t) => { const { server, uploads } = await app(t) const multipart = multipartBody("rechnung.pdf", pdf) const first = await server.inject({ method: "POST", url: "/api/mcp/files?target=incoming_invoice&invoiceId=5&prepare=false", ...multipart }) assert.equal(first.statusCode, 200, first.body) assert.deepEqual(first.json(), { stored: true }) assert.equal(uploads[0].tenantId, 7) assert.equal(uploads[0].target, "incoming_invoice") assert.equal(uploads[0].filename, "rechnung.pdf") assert.equal(uploads[0].invoiceId, 5) assert.equal(uploads[0].prepare, false) assert.deepEqual(uploads[0].body, pdf) const raw = await server.inject({ method: "POST", url: "/api/mcp/files?filename=scan.pdf", headers: { ...headers, "content-type": "application/pdf" }, payload: pdf }) assert.equal(raw.statusCode, 200, raw.body) assert.equal(uploads[1].target, "inbox") assert.equal(uploads[1].mimeType, "application/pdf") assert.deepEqual(uploads[1].body, pdf) }) test("binary endpoint rejects missing auth, filenames, wrong fields, types and oversized files", async (t) => { const { server, uploads } = await app(t) const raw = { "content-type": "application/pdf" } assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files?filename=a.pdf", headers: raw, payload: pdf })).statusCode, 401) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files", headers: { ...headers, ...raw }, payload: pdf })).statusCode, 400) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files?filename=a.pdf", ...multipartBody("a.pdf", pdf, "other") })).statusCode, 400) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files?filename=a.pdf", headers: { ...headers, "content-type": "text/html" }, payload: "

" })).statusCode, 415) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files?filename=a.pdf&target=wiki", headers: { ...headers, ...raw }, payload: pdf })).statusCode, 400) const tooLarge = Buffer.alloc(MCP_FILE_LIMIT + 1) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files?filename=a.pdf", headers: { ...headers, ...raw }, payload: tooLarge })).statusCode, 413) assert.equal((await server.inject({ method: "POST", url: "/api/mcp/files", ...multipartBody("a.pdf", tooLarge) })).statusCode, 413) assert.equal(uploads.length, 0) }) test("query parsing keeps invoice options to the incoming invoice target", () => { assert.deepEqual(parseUploadQuery({}), { target: "inbox", invoiceId: null, prepare: true, filename: null }) assert.throws(() => parseUploadQuery({ invoiceId: "3" }), /nur für target=incoming_invoice/) assert.throws(() => parseUploadQuery({ target: "incoming_invoice", invoiceId: "x" }), /positive ganze Zahl/) assert.throws(() => parseUploadQuery({ target: "incoming_invoice", prepare: "ja" }), /true oder false/) }) test("stored uploads require the permission of the chosen target", async () => { const context: any = { server: { db: null }, tenantId: 7, userId: id, isAdmin: false, permissions: ["documents.read"] } await assert.rejects(storeMcpFileUpload(context, { target: "inbox", filename: "a.pdf", body: pdf }), /documents\.write/) await assert.rejects(storeMcpFileUpload(context, { target: "incoming_invoice", filename: "a.pdf", body: pdf }), /accounting\.incoming_invoices\.write/) await assert.rejects(storeMcpFileUpload(context, { target: "inbox", filename: "a.pdf", body: Buffer.alloc(0) }), /leer/) }) test("upload_url tool returns the absolute endpoint with examples", async (t) => { const apiBase = secrets.API_BASE_URL secrets.API_BASE_URL = "https://app.fedeo.test/backend/" t.after(() => { secrets.API_BASE_URL = apiBase }) const result: any = await mcpToolMap.get("documents.files.upload_url")!.handler({} as any, {}) assert.equal(result.url, "https://app.fedeo.test/backend/api/mcp/files") assert.match(result.examples[0], /-F "file=@rechnung\.pdf"/) })