Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s

Monorepo npm con apps/public (Angular SSR), apps/private (Angular SPA),
apps/api (Fastify), packages/shared e packages/styles; Medusa 2.21 in medusa/;
infrastruttura Docker (Postgres PostGIS+pgvector, Redis, Caddy, backup) e
workflow Gitea Actions per il deploy via SSH a ogni push su master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jQyT6dR9czzGwqb7FkG6u
This commit is contained in:
Lorenzo Sanesi
2026-10-20 09:30:48 +02:00
co-authored by Claude Opus 5.5
commit b83ce039fb
505 changed files with 84544 additions and 0 deletions
+208
View File
@@ -0,0 +1,208 @@
import { createHmac, randomUUID } from "node:crypto"
import { eq } from "drizzle-orm"
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
import { users, webhookEvents } from "../src/db/schema/index.js"
import { hashPassword } from "../src/services/password.js"
import { buildTestApp, type TestApp, truncateAll } from "./helpers.js"
const SECRET = "w".repeat(32) // WEBHOOK_SECRET di testConfig
let t: TestApp
const sign = (body: string, secret = SECRET) => createHmac("sha256", secret).update(body).digest("hex")
function payload(event: string, data: Record<string, unknown>, id: string = randomUUID()) {
return JSON.stringify({ id, event, occurred_at: new Date().toISOString(), data })
}
const deliver = (body: string, signature: string | undefined = sign(body), app = t.app) =>
app.inject({
method: "POST",
url: "/webhooks/medusa",
headers: { "content-type": "application/json", ...(signature ? { "x-signature": signature } : {}) },
payload: body,
})
const ORDER = {
id: "order_1",
display_id: 7,
status: "pending",
email: "cliente@example.com",
customer_id: null,
currency_code: "eur",
created_at: "2026-10-02T10:00:00.000Z",
item_total: 1190,
shipping_total: 15,
tax_total: 217.29,
discount_total: 0,
total: 1205,
items: [{ title: "Standard", product_title: "Addolcitore", variant_title: "Standard", quantity: 1, unit_price: 1190, total: 1190 }],
shipping_address: { first_name: "Mario", last_name: "Rossi", address_1: "Via Roma 1", city: "Milano", postal_code: "20100", province: "MI" },
shipping_methods: [{ name: "Spedizione standard" }],
fulfillments: [{ id: "ful_1", labels: [{ tracking_number: "TRK123", tracking_url: "https://track.example/TRK123" }] }],
}
const eventRow = async (id: string) => (await t.app.db.select().from(webhookEvents).where(eq(webhookEvents.id, id)))[0]
beforeAll(async () => {
t = await buildTestApp()
})
afterAll(() => t.app.close())
beforeEach(async () => {
await truncateAll()
t.medusa.reset()
t.email.clear()
t.medusa.orderDetails.set(ORDER.id, ORDER)
await t.app.db
.insert(users)
.values({ email: "admin@iti.local", passwordHash: await hashPassword("x".repeat(12)), role: "admin", firstName: "A", lastName: "D" })
})
describe("firma", () => {
it("firma valida → elaborato, evento salvato", async () => {
const id = randomUUID()
const res = await deliver(payload("order.placed", { id: ORDER.id }, id))
expect(res.statusCode).toBe(200)
expect(res.json()).toEqual({ status: "processed" })
const row = await eventRow(id)
expect(row).toMatchObject({ event: "order.placed", error: null })
expect(row?.processedAt).toBeInstanceOf(Date)
})
it("firma errata, assente, con altra chiave o body alterato → 401 e niente in DB", async () => {
const body = payload("order.placed", { id: ORDER.id })
for (const res of [
await deliver(body, "0".repeat(64)),
await deliver(body, ""), // header assente
await deliver(body, "non-esadecimale"),
await deliver(body, sign(body, "z".repeat(32))),
await deliver(body.replace("order_1", "order_2"), sign(body)),
]) {
expect(res.statusCode).toBe(401)
expect(res.json().error.code).toBe("INVALID_SIGNATURE")
}
expect(await t.app.db.select().from(webhookEvents)).toHaveLength(0)
expect(t.email.sent).toHaveLength(0)
})
it("firma valida su JSON non valido → 400; payload senza id uuid → 400", async () => {
expect((await deliver("{non json")).statusCode).toBe(400)
const noId = JSON.stringify({ id: "x", event: "order.placed", occurred_at: "", data: {} })
expect((await deliver(noId)).json().error.code).toBe("VALIDATION_ERROR")
})
})
describe("idempotenza", () => {
it("stesso evento due volte → elaborato una sola volta", async () => {
const body = payload("order.placed", { id: ORDER.id })
expect((await deliver(body)).json().status).toBe("processed")
expect((await deliver(body)).json().status).toBe("duplicate")
expect(t.email.sent.filter((m) => m.tag === "order-confirmation")).toHaveLength(1)
expect(await t.app.db.select().from(webhookEvents)).toHaveLength(1)
})
it("consegne concorrenti dello stesso evento → una sola elaborazione", async () => {
const body = payload("order.placed", { id: ORDER.id })
const results = await Promise.all([deliver(body), deliver(body), deliver(body)])
expect(results.map((r) => r.statusCode)).toEqual([200, 200, 200])
expect(results.map((r) => r.json().status).sort()).toEqual(["duplicate", "duplicate", "processed"])
expect(t.email.sent.filter((m) => m.tag === "order-confirmation")).toHaveLength(1)
})
it("errore di elaborazione → 500, errore salvato; il retry di Medusa lo rielabora", async () => {
const id = randomUUID()
const body = payload("order.placed", { id: ORDER.id }, id)
t.medusa.down = true
const failed = await deliver(body)
expect(failed.statusCode).toBe(500)
expect(failed.json().error.code).toBe("WEBHOOK_PROCESSING_FAILED")
const row = await eventRow(id)
expect(row?.processedAt).toBeNull()
expect(row?.error).toContain("Medusa non raggiungibile")
t.medusa.down = false
expect((await deliver(body)).json().status).toBe("processed")
expect(await eventRow(id)).toMatchObject({ error: null })
expect(t.email.sent.filter((m) => m.tag === "order-confirmation")).toHaveLength(1)
})
})
describe("dispatcher", () => {
it("order.placed → conferma al cliente con righe e totali, notifica agli admin", async () => {
await deliver(payload("order.placed", { id: ORDER.id }))
const mail = t.email.last("order-confirmation")
expect(mail?.to).toBe("cliente@example.com")
expect(mail?.subject).toContain("n. 7")
expect(mail?.text).toContain("1 x Addolcitore - Standard")
expect(mail?.text).toMatch(/Totale \(di cui IVA 217,29\s€\): 1\.205,00\s€/)
expect(mail?.html).toContain("1.205,00")
expect(mail?.text).toContain("Via Roma 1")
expect(t.email.last("order-placed-admin")?.to).toBe("admin@iti.local")
})
it("ADMIN_NOTIFY_EMAILS sostituisce gli admin del DB", async () => {
const other = await buildTestApp({ ADMIN_NOTIFY_EMAILS: "Ordini@ITI.it, magazzino@iti.it" })
other.medusa.orderDetails.set(ORDER.id, ORDER)
try {
const body = payload("order.placed", { id: ORDER.id })
expect((await deliver(body, sign(body), other.app)).statusCode).toBe(200)
} finally {
await other.app.close()
}
expect(other.email.sent.filter((m) => m.tag === "order-placed-admin").map((m) => m.to)).toEqual([
"ordini@iti.it",
"magazzino@iti.it",
])
})
it("order.canceled → email di annullamento", async () => {
await deliver(payload("order.canceled", { id: ORDER.id }))
expect(t.email.last("order-canceled")).toMatchObject({ to: "cliente@example.com" })
})
it("order.fulfillment_created → email di spedizione con tracking; no_notification → nessuna email", async () => {
await deliver(payload("order.fulfillment_created", { order_id: ORDER.id, fulfillment_id: "ful_1", no_notification: false }))
expect(t.email.last("order-shipped")?.text).toContain("TRK123")
t.email.clear()
const res = await deliver(payload("order.fulfillment_created", { order_id: ORDER.id, fulfillment_id: "ful_1", no_notification: true }))
expect(res.json().status).toBe("processed")
expect(t.email.sent).toHaveLength(0)
})
it("evento non gestito → ignored, ma registrato come elaborato", async () => {
const id = randomUUID()
expect((await deliver(payload("order.updated", { id: ORDER.id }, id))).json().status).toBe("ignored")
expect((await eventRow(id))?.processedAt).toBeInstanceOf(Date)
})
it("ordine inesistente in Medusa → 500 (Medusa ritenta)", async () => {
expect((await deliver(payload("order.placed", { id: "order_x" }))).statusCode).toBe(500)
})
})
describe("esclusioni", () => {
it("niente rate limit globale né header CORS sull'endpoint", async () => {
const limited = await buildTestApp({ RATE_LIMIT_MAX: "2" })
limited.medusa.orderDetails.set(ORDER.id, ORDER)
try {
const body = payload("order.placed", { id: ORDER.id })
for (let i = 0; i < 5; i++) {
const res = await limited.app.inject({
method: "POST",
url: "/webhooks/medusa",
headers: { "content-type": "application/json", "x-signature": sign(body), origin: "http://localhost:4200" },
payload: body,
})
expect(res.statusCode).toBe(200)
expect(res.headers["access-control-allow-origin"]).toBeUndefined()
}
// Controllo: le altre route sono limitate
const statuses = []
for (let i = 0; i < 3; i++) statuses.push((await limited.app.inject({ method: "GET", url: "/shop/categories" })).statusCode)
expect(statuses).toContain(429)
} finally {
await limited.app.close()
}
})
})