feat(scan): support iX1600 duplex scanning on Linux

This commit is contained in:
2026-09-17 18:22:43 +02:00
parent cfef07b96d
commit dd37622bb6
24 changed files with 1411 additions and 215 deletions

View File

@@ -0,0 +1,89 @@
import assert from "node:assert/strict"
import test from "node:test"
import { createHash } from "node:crypto"
import { readFileSync } from "node:fs"
import { createRequire } from "node:module"
import path from "node:path"
import vm from "node:vm"
import ts from "typescript"
import Fastify from "fastify"
import { getTableColumns, getTableName } from "drizzle-orm"
import { PgDialect } from "drizzle-orm/pg-core"
// Load the real route with only file storage replaced; tests never contact S3 or a live database.
const routePath = path.resolve(__dirname, "../src/routes/instanceAgentGateway.ts")
const routeRequire = createRequire(routePath)
const compiled = ts.transpileModule(readFileSync(routePath, "utf8"), {
compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, esModuleInterop: true },
}).outputText
const fixture = async (status = "running") => {
const token = "test-agent-token"
const agent = { id: "agent", tokenHash: createHash("sha256").update(token).digest("hex"), active: true }
const job: any = { id: "job", agentId: "agent", tenantId: 7, requestedBy: "user", requestedFilename: "scan.png", target: { folder: "folder" }, status }
const saved: any[] = []
const rows: Record<string, any[]> = { instance_agents: [agent], instance_agent_scan_jobs: [job] }
const dialect = new PgDialect()
const matching = (table: any, condition: any) => {
const query = dialect.sqlToQuery(condition)
const columns = getTableColumns(table) as Record<string, any>
const clauses = [...query.sql.matchAll(/"\w+"\."(\w+)" (?:= \$(\d+)|in \(([^)]+)\))/g)]
assert.ok(clauses.length, query.sql)
return rows[getTableName(table)].filter(row => clauses.every(match => {
const key = Object.keys(columns).find(key => columns[key].name === match[1])!
if (match[2]) return row[key] === query.params[Number(match[2]) - 1]
return [...match[3].matchAll(/\$(\d+)/g)].some(parameter => row[key] === query.params[Number(parameter[1]) - 1])
}))
}
const db = {
select: () => ({ from: (table: any) => ({ where: (where: any) => ({ limit: async () => matching(table, where) }) }) }),
update: (table: any) => ({ set: (data: any) => ({ where: (where: any) => ({ returning: async () => matching(table, where).map(row => Object.assign(row, data)) }) }) }),
}
const exports: any = {}
vm.runInNewContext(compiled, {
exports,
require: (name: string) => name === "../utils/files"
? { saveFile: async (...args: any[]) => { saved.push(args); return { id: "saved-file" } } }
: routeRequire(name),
})
const app = Fastify()
app.decorate("db", db)
await app.register(exports.default)
return { app, job, saved, headers: { "x-agent-token": token } }
}
test("late failure status cannot overwrite an already completed scan", async (t) => {
const f = await fixture("completed")
t.after(() => f.app.close())
const response = await f.app.inject({ method: "POST", url: "/scan-jobs/job/status", headers: f.headers, payload: { status: "failed", message: "upload timeout" } })
assert.equal(response.statusCode, 409)
assert.equal(f.job.status, "completed")
})
test("running jobs accept progress and scanner failures", async (t) => {
const f = await fixture()
t.after(() => f.app.close())
for (const status of ["running", "failed"]) {
const response = await f.app.inject({ method: "POST", url: "/scan-jobs/job/status", headers: f.headers, payload: { status, message: "4 Seiten" } })
assert.equal(response.statusCode, 200)
assert.equal(f.job.status, status)
}
})
test("upload preserves the actual PDF filename, tenant and target folder", async (t) => {
const f = await fixture()
t.after(() => f.app.close())
const boundary = "fedeo-test-boundary"
const response = await f.app.inject({
method: "POST", url: "/scan-jobs/job/upload",
headers: { ...f.headers, "content-type": `multipart/form-data; boundary=${boundary}` },
payload: `--${boundary}\r\nContent-Disposition: form-data; name="file"; filename="scan.pdf"\r\nContent-Type: application/pdf\r\n\r\n%PDF-test\r\n--${boundary}--\r\n`,
})
assert.equal(response.statusCode, 200, response.body)
assert.equal(f.saved[0][1], 7)
assert.equal(f.saved[0][3].filename, "scan.pdf")
assert.equal(f.saved[0][3].contentType, "application/pdf")
assert.equal(f.saved[0][4], "folder")
assert.equal(f.job.status, "completed")
assert.equal(f.job.fileId, "saved-file")
})