import { existsSync } from "node:fs"
import { readdir, rm } from "node:fs/promises"
import { join } from "node:path"
import { Readable } from "node:stream"
import { eq } from "drizzle-orm"
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import { documents, installerProfiles, users } from "../src/db/schema/index.js"
import { hashPassword } from "../src/services/password.js"
import { checkFileKind, InvalidStorageKeyError, LocalDiskStorage, sniffKind } from "../src/services/storage.js"
import { buildTestApp, multipartBody, TEST_UPLOAD_DIR, type TestApp, truncateAll } from "./helpers.js"
const PASSWORD = "Password-di-test-1"
const PDF = Buffer.from("%PDF-1.7\n1 0 obj << >> endobj\ntrailer << >>\n%%EOF\n")
const PNG = Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), Buffer.alloc(32, 1)])
const ZIP = Buffer.concat([Buffer.from("PK\x03\x04", "latin1"), Buffer.alloc(30)])
const DWG = Buffer.concat([Buffer.from("AC1032"), Buffer.alloc(30)])
const DXF = Buffer.from(" 0\r\nSECTION\r\n 2\r\nHEADER\r\n")
const HTML = Buffer.from("")
let t: TestApp
const tokens: Record = {}
async function user(email: string, role: "customer" | "company" | "installer" | "admin", installerStatus?: "pending" | "approved") {
const [u] = await t.app.db
.insert(users)
.values({ email, passwordHash: await hashPassword(PASSWORD), role, firstName: "T", lastName: "U" })
.returning()
if (installerStatus) await t.app.db.insert(installerProfiles).values({ userId: u!.id, businessName: "Srl", status: installerStatus })
const res = await t.app.inject({ method: "POST", url: "/auth/login", payload: { email, password: PASSWORD } })
tokens[email] = res.json().accessToken
return u!
}
const auth = (email?: string) => (email ? { authorization: `Bearer ${tokens[email]}` } : {})
function upload(
file: { filename: string; contentType: string; content: Buffer | string } | null,
fields: Record = { title: "Scheda tecnica", visibility: "installer" },
as = "admin@iti.local",
url = "/admin/documents",
) {
const body = multipartBody(fields, file ? [file] : [])
return t.app.inject({ method: "POST", url, payload: body.payload, headers: { ...body.headers, ...auth(as) } })
}
async function filesOnDisk(): Promise {
const out: string[] = []
const walk = async (dir: string) => {
if (!existsSync(dir)) return
for (const e of await readdir(dir, { withFileTypes: true })) {
const p = join(dir, e.name)
if (e.isDirectory()) await walk(p)
else out.push(p)
}
}
await walk(TEST_UPLOAD_DIR)
return out
}
beforeAll(async () => {
t = await buildTestApp()
})
afterAll(async () => {
await t.app.close()
await rm(TEST_UPLOAD_DIR, { recursive: true, force: true })
})
beforeEach(async () => {
await truncateAll()
await rm(TEST_UPLOAD_DIR, { recursive: true, force: true })
await user("admin@iti.local", "admin")
await user("cliente@example.com", "customer")
await user("inst@example.com", "installer", "approved")
await user("attesa@example.com", "installer", "pending")
})
describe("upload e download per ruolo", () => {
it("un installatore approvato scarica i documenti installer; customer 403, installatore in attesa 403, anonimo 401", async () => {
const res = await upload({ filename: "Scheda PDC 8kW.pdf", contentType: "application/pdf", content: PDF })
expect(res.statusCode).toBe(201)
const doc = res.json()
expect(doc).toMatchObject({ title: "Scheda tecnica", visibility: "installer", mimeType: "application/pdf", sizeBytes: PDF.length })
expect(JSON.stringify(doc)).not.toMatch(/file_?path|documents\//i)
const dl = await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download`, headers: auth("inst@example.com") })
expect(dl.statusCode).toBe(200)
expect(dl.rawPayload.equals(PDF)).toBe(true)
expect(dl.headers["content-type"]).toBe("application/pdf")
expect(dl.headers["content-disposition"]).toBe(`attachment; filename="Scheda tecnica.pdf"; filename*=UTF-8''Scheda%20tecnica.pdf`)
expect(dl.headers["cache-control"]).toBe("private, no-store")
const forbidden = await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download`, headers: auth("cliente@example.com") })
expect(forbidden.statusCode).toBe(403)
expect((await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download`, headers: auth("attesa@example.com") })).statusCode).toBe(403)
expect((await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download` })).statusCode).toBe(401)
expect((await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download`, headers: auth("admin@iti.local") })).statusCode).toBe(200)
})
it("documento pubblico scaricabile da anonimo; documento inesistente 404", async () => {
const doc = (await upload({ filename: "catalogo.pdf", contentType: "application/pdf", content: PDF }, { title: "Catalogo", visibility: "public" })).json()
const dl = await t.app.inject({ method: "GET", url: `/documents/${doc.id}/download` })
expect(dl.statusCode).toBe(200)
expect(dl.headers["cache-control"]).toBe("public, max-age=300")
expect((await t.app.inject({ method: "GET", url: "/documents/00000000-0000-4000-8000-000000000000/download" })).statusCode).toBe(404)
})
it("GET /documents elenca solo i livelli visibili al ruolo", async () => {
for (const visibility of ["public", "customer", "installer", "admin"]) {
await upload({ filename: "x.pdf", contentType: "application/pdf", content: PDF }, { title: visibility, visibility, category: "schede" })
}
const titles = async (email?: string) =>
((await t.app.inject({ method: "GET", url: "/documents", headers: auth(email) })).json().items as { title: string }[])
.map((d) => d.title)
.sort()
expect(await titles()).toEqual(["public"])
expect(await titles("cliente@example.com")).toEqual(["customer", "public"])
expect(await titles("attesa@example.com")).toEqual(["customer", "public"])
expect(await titles("inst@example.com")).toEqual(["customer", "installer", "public"])
expect(await titles("admin@iti.local")).toEqual(["admin", "customer", "installer", "public"])
})
it("tutti i tipi ammessi: immagini, PDF, ZIP, DWG, DXF (MIME canonico)", async () => {
const cases = [
{ filename: "foto.PNG", contentType: "image/png", content: PNG, mime: "image/png" },
{ filename: "progetto.zip", contentType: "application/x-zip-compressed", content: ZIP, mime: "application/zip" },
{ filename: "pianta.dwg", contentType: "application/octet-stream", content: DWG, mime: "image/vnd.dwg" },
{ filename: "pianta.dxf", contentType: "application/dxf", content: DXF, mime: "image/vnd.dxf" },
]
for (const c of cases) {
const res = await upload(c, { title: c.filename, visibility: "admin" })
expect(res.statusCode, c.filename).toBe(201)
expect(res.json().mimeType).toBe(c.mime)
}
})
})
describe("rifiuti", () => {
it("estensione o MIME non consentiti, contenuto non coerente → 415 e niente su disco", async () => {
const rejected = [
{ filename: "virus.exe", contentType: "application/octet-stream", content: Buffer.from("MZ...") },
{ filename: "logo.svg", contentType: "image/svg+xml", content: Buffer.from("