feat(mcp): add document inbox tools and PDF transfers
This commit is contained in:
@@ -18,6 +18,8 @@ import inboxRoutes from "../src/routes/documentInbox"
|
||||
import corsPlugin from "../src/plugins/cors"
|
||||
import { createScannerInboxService, SCANNER_UPLOAD_LIMIT, validateScannerFile, type ScannerInboxService } from "../src/modules/scanner-inbox.service"
|
||||
import type { FilingSuggestionResult } from "../src/modules/scanner-inbox-suggestions"
|
||||
import { createDocumentTools } from "../src/mcp/tools/documents"
|
||||
import { assertToolPermission } from "../src/mcp/authz"
|
||||
|
||||
const execute = promisify(execFile)
|
||||
const basic = (username = "scanner", password = "secret") => ({ authorization: `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` })
|
||||
@@ -611,4 +613,64 @@ test("scanner inbox integration: migration, persistence, isolation, retries and
|
||||
assert.equal((await send("new.pdf", pdf, basic(first.username, rotated.json().connection.password))).statusCode, 201)
|
||||
assert.equal((await service.list(1, 50, 0, "")).total, 2)
|
||||
})
|
||||
await t.test("MCP uploads, downloads and filing use the same tenant-scoped document lifecycle", async () => {
|
||||
const tools = createDocumentTools({ inbox: () => service, readFile: async key => {
|
||||
assert.ok(stored.has(key))
|
||||
return stored.get(key)!
|
||||
} })
|
||||
const call = async (name: string, args: any = {}, tenantId = 1) => {
|
||||
const tool = tools.find(tool => tool.name === `documents.${name}`)!
|
||||
const context = { server: { db }, userId, tenantId, isAdmin: false, permissions: ["documents.read", "documents.write"] } as any
|
||||
assertToolPermission(context, tool)
|
||||
return await tool.handler(context, args) as any
|
||||
}
|
||||
const upload = (filename: string, body = pdf, tenant = 1) => call("files.upload", { filename, contentBase64: body.toString("base64") }, tenant)
|
||||
await service.disable(1)
|
||||
const created = await upload("mcp.pdf")
|
||||
assert.equal(created.created, true, "MCP uploads do not require scanner credentials")
|
||||
assert.equal((await upload("mcp.pdf")).id, created.id)
|
||||
assert.equal((await upload("mcp.pdf")).created, false)
|
||||
await assert.rejects(upload("mcp.pdf", samplePdf), { statusCode: 409 })
|
||||
assert.notEqual((await upload("mcp.pdf", pdf, 2)).id, created.id)
|
||||
const row = (await pool.query("SELECT * FROM files WHERE id=$1", [created.id])).rows[0]
|
||||
assert.equal(row.created_by, userId)
|
||||
assert.deepEqual(stored.get(row.path), pdf)
|
||||
assert.equal(row.extracted_text, null)
|
||||
const list = await call("inbox.list", { search: "mcp.pdf", limit: 1 })
|
||||
assert.equal(list.rows[0].id, created.id)
|
||||
assert.equal(list.rows[0].suggestionStatus, "pending")
|
||||
assert.equal((await call("inbox.suggestion.get", { id: created.id })).status, "pending")
|
||||
const downloaded = await call("files.download", { id: created.id, format: "base64" })
|
||||
assert.deepEqual(Buffer.from(downloaded.contentBase64, "base64"), pdf)
|
||||
await assert.rejects(call("files.download", { id: created.id }, 2), { statusCode: 404 })
|
||||
await assert.rejects(call("inbox.archive", { id: created.id }, 2), { statusCode: 404 })
|
||||
await assert.rejects(call("inbox.file", { id: created.id, folder: foreignFolder, type: targetType }), { statusCode: 400 })
|
||||
await assert.rejects(call("inbox.file", { id: created.id, folder: targetFolder, type: alternativeType }), { statusCode: 400 })
|
||||
await pool.query("INSERT INTO projects (id, tenant, name) VALUES (901, 1, 'MCP project'), (902, 2, 'Foreign MCP project')")
|
||||
await assert.rejects(call("inbox.file", { id: created.id, folder: targetFolder, type: targetType, assignments: { project: 902 } }), { statusCode: 400 })
|
||||
const filed = await call("inbox.file", { id: created.id, folder: targetFolder, type: targetType, assignments: { project: 901 } })
|
||||
assert.equal(filed.id, created.id)
|
||||
assert.equal((await call("inbox.list", { search: "mcp.pdf" })).rows.length, 0)
|
||||
assert.equal((await call("files.list", { project: 901 })).rows[0].id, created.id)
|
||||
assert.equal((await call("files.get", { id: created.id })).file.path, undefined)
|
||||
await assert.rejects(call("inbox.archive", { id: created.id }), { statusCode: 409 })
|
||||
const archived = await upload("mcp-archive.pdf")
|
||||
await call("inbox.archive", { id: archived.id })
|
||||
await assert.rejects(call("files.download", { id: archived.id }), { statusCode: 404 })
|
||||
assert.equal((await call("files.list", { search: "mcp-archive.pdf" })).rows.length, 0)
|
||||
const before = stored.size
|
||||
storageFailure = true
|
||||
await assert.rejects(upload("mcp-storage-error.pdf"), /Simulated object store failure/)
|
||||
storageFailure = false
|
||||
await assert.rejects(upload("reject.pdf"))
|
||||
assert.equal(stored.size, before)
|
||||
assert.equal((await pool.query("SELECT count(*) FROM files WHERE name IN ('mcp-storage-error.pdf', 'reject.pdf')")).rows[0].count, "0")
|
||||
await pool.query("UPDATE tenants SET locked='maintenance_tenant' WHERE id=1")
|
||||
await assert.rejects(upload("mcp-maintenance.pdf"), { statusCode: 423 })
|
||||
await pool.query("UPDATE tenants SET locked=null WHERE id=1")
|
||||
await pool.query("INSERT INTO auth_profiles (tenant_id, user_id, customer_for_portal) VALUES (1, $1, 123)", [userId])
|
||||
await assert.rejects(upload("mcp-portal.pdf"), { statusCode: 403 })
|
||||
await assert.rejects(call("files.download", { id: created.id }), { statusCode: 403 })
|
||||
await pool.query("DELETE FROM auth_profiles")
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user