import assert from "node:assert/strict" import test from "node:test" import { getTableColumns, getTableName } from "drizzle-orm" import { PgDialect } from "drizzle-orm/pg-core" import * as schema from "../db/schema" import { mcpToolMap } from "../src/mcp/registry" import { archiveEntities, prepareArchiveRestoreInput } from "../src/mcp/tools/archive" import { asToolResult } from "../src/mcp/result" const tenantId = 7 const userId = "00000000-0000-4000-8000-000000000001" const uuid = "00000000-0000-4000-8000-000000000002" const dialect = new PgDialect() /** Minimal Drizzle stand-in which evaluates equality predicates, including tenant filters. */ function fixture(initial: Record = {}, listRows: any[] = []) { const state = structuredClone(initial) const rowsFor = (table: any) => state[getTableName(table)] ||= [] const matches = (table: any, where: any, row: any) => { const query = dialect.sqlToQuery(where) const columns = getTableColumns(table) as Record const keyFor = (name: string) => Object.keys(columns).find((key) => columns[key].name === name)! return [...query.sql.matchAll(/"\w+"\."(\w+)" = \$(\d+)/g)] .every((match) => row[keyFor(match[1])] === query.params[Number(match[2]) - 1]) } const listed: string[] = [] const db: any = { transaction: async (callback: any) => callback(db), select: () => ({ from: (table: any) => { let condition: any const query: any = { where: (where: any) => { condition = where; return query }, orderBy: () => query, offset: () => query, limit: () => query, for: () => query, then: (resolve: any, reject: any) => Promise.resolve().then(() => { if (listRows.length) { listed.push(dialect.sqlToQuery(condition).sql); return listRows } return rowsFor(table).filter((row) => matches(table, condition, row)).map((row) => ({ ...row })) }).then(resolve, reject), } return query } }), insert: (table: any) => ({ values: async (data: any) => { rowsFor(table).push(data) } }), update: (table: any) => ({ set: (data: any) => ({ where: (where: any) => ({ returning: async () => rowsFor(table).filter((row) => matches(table, where, row)).map((row) => ({ ...Object.assign(row, data) })), }) }) }), } const context: any = { server: { db, log: { warn: () => undefined } }, tenantId, userId, isAdmin: true, permissions: [] } return { context, rows: rowsFor, listed } } const call = async (context: any, name: string, args: Record) => await mcpToolMap.get(`archive.entities.${name}`)!.handler(context, args) as any for (const [key, entity] of Object.entries(archiveEntities)) { test(`${key}: restore unarchives within the tenant and records history`, async () => { const id = entity.uuid ? uuid : 1 const f = fixture({ [getTableName(entity.table)]: [{ id, tenant: tenantId, archived: true }], // Restoring documents requires an internal (non-portal) profile. auth_profiles: [{ tenant_id: tenantId, user_id: userId, customer_for_portal: null }], }) const result = await call(f.context, "restore", { entity: key, id }) assert.deepEqual(asToolResult(result).structuredContent?.success, true) assert.equal(result.record.archived, undefined) const [row] = f.rows(entity.table) assert.equal(row.archived, false) assert.equal(row.updatedBy ?? row.updated_by, userId) await assert.rejects(call(f.context, "restore", { entity: key, id }), /nicht archiviert/) }) } test("restore rejects foreign tenants, unknown entities, invalid IDs and missing write permission", async () => { const f = fixture({ customers: [{ id: 1, tenant: 8, archived: true }], tasks: [{ id: 2, tenant: tenantId, archived: true }] }) await assert.rejects(call(f.context, "restore", { entity: "customers", id: 1 }), /nicht gefunden/) assert.equal(f.rows(schema.customers)[0].archived, true) await assert.rejects(call(f.context, "restore", { entity: "accounts", id: 1 }), /unbekannte Entität/) assert.throws(() => prepareArchiveRestoreInput({ entity: "cost_centres", id: 1 }), /UUID/) assert.throws(() => prepareArchiveRestoreInput({ entity: "tasks", id: "abc" }), /ganze Zahl/) f.context.isAdmin = false f.context.permissions = ["organisation.tasks.read"] await assert.rejects(call(f.context, "restore", { entity: "tasks", id: 2 }), /Fehlende Berechtigung.*organisation\.tasks\.write/) f.context.permissions.push("organisation.tasks.write") await call(f.context, "restore", { entity: "tasks", id: 2 }) assert.equal(f.rows(schema.tasks)[0].archived, false) }) test("list returns archived rows visibly and requires the read permission", async () => { const f = fixture({}, [{ id: 1, tenant: tenantId, archived: true, name: "Alt" }]) const result = asToolResult(await call(f.context, "list", { entity: "customers", search: "Al" })) assert.deepEqual(result.structuredContent?.rows, [{ id: 1, tenant: tenantId, name: "Alt", isArchived: true }]) assert.match(f.listed[0], /"customers"\."archived" = \$2/) f.context.isAdmin = false f.context.permissions = [] await assert.rejects(call(f.context, "list", { entity: "customers" }), /masterdata\.customers\.read/) }) test("restore is advertised as a write tool", () => { assert.equal(mcpToolMap.get("archive.entities.restore")!.readOnly, false) })