Piattaforma ITI TECHNOLOGY: monorepo, Medusa, API, sito, area riservata e deploy
Deploy stack su Proxmox / deploy (push) Failing after 3s
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:
@@ -0,0 +1,148 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"
|
||||
import { eq } from "drizzle-orm"
|
||||
import { contactRequests, installerProfiles, quoteRequests, quotes, showroomBookings, tickets, users } from "../src/db/schema/index.js"
|
||||
import { buildTestApp, createUserAndLogin, TEST_PASSWORD, type TestApp, truncateAll } from "./helpers.js"
|
||||
|
||||
let t: TestApp
|
||||
let admin: { user: { id: string }; headers: Record<string, string> }
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await buildTestApp()
|
||||
})
|
||||
afterAll(() => t.app.close())
|
||||
beforeEach(async () => {
|
||||
await truncateAll()
|
||||
t.medusa.reset()
|
||||
t.email.clear()
|
||||
admin = await createUserAndLogin(t.app, "admin@iti.local", "admin")
|
||||
})
|
||||
|
||||
const call = (method: "GET" | "POST", url: string, headers = admin.headers) =>
|
||||
t.app.inject({ method, url, headers, ...(method === "POST" ? { payload: {} } : {}) })
|
||||
|
||||
const login = (email: string) => t.app.inject({ method: "POST", url: "/auth/login", payload: { email, password: TEST_PASSWORD } })
|
||||
|
||||
describe("accesso all'area admin", () => {
|
||||
it("admin disabilitato o declassato: rifiutato subito, anche con un access token ancora valido", async () => {
|
||||
expect((await call("GET", "/admin/summary")).statusCode).toBe(200)
|
||||
|
||||
await t.app.db.update(users).set({ role: "customer" }).where(eq(users.id, admin.user.id))
|
||||
expect((await call("GET", "/admin/summary")).statusCode).toBe(403)
|
||||
|
||||
await t.app.db.update(users).set({ role: "admin", disabledAt: new Date() }).where(eq(users.id, admin.user.id))
|
||||
expect((await call("GET", "/admin/summary")).statusCode).toBe(401)
|
||||
})
|
||||
})
|
||||
|
||||
describe("admin utenti", () => {
|
||||
it("ricerca per testo, ruolo e stato; solo admin", async () => {
|
||||
const mario = await createUserAndLogin(t.app, "mario@example.com", "customer", { firstName: "Mario", lastName: "Rossi" })
|
||||
await createUserAndLogin(t.app, "acme@example.com", "company", { companyName: "Acme Srl", vatNumber: "01234567890" })
|
||||
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
|
||||
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl" })
|
||||
|
||||
const all = await call("GET", "/admin/users")
|
||||
expect(all.statusCode).toBe(200)
|
||||
expect(all.json().total).toBe(4)
|
||||
const item = all.json().items.find((u: { email: string }) => u.email === "inst@example.com")
|
||||
expect(item).toMatchObject({ role: "installer", installerStatus: "pending", disabledAt: null })
|
||||
expect(item).not.toHaveProperty("passwordHash")
|
||||
expect(item).not.toHaveProperty("medusaCustomerId")
|
||||
|
||||
expect((await call("GET", "/admin/users?q=mario%20rossi")).json().items.map((u: { email: string }) => u.email)).toEqual(["mario@example.com"])
|
||||
expect((await call("GET", "/admin/users?q=acme")).json().total).toBe(1)
|
||||
expect((await call("GET", "/admin/users?role=company")).json().items[0].companyName).toBe("Acme Srl")
|
||||
expect((await call("GET", "/admin/users?status=disabled")).json().total).toBe(0)
|
||||
// % e _ sono letterali
|
||||
expect((await call("GET", "/admin/users?q=%25")).json().total).toBe(0)
|
||||
|
||||
expect((await call("GET", `/admin/users/${mario.user.id}`)).json()).toMatchObject({ firstName: "Mario", installerStatus: null })
|
||||
expect((await call("GET", "/admin/users/00000000-0000-4000-8000-000000000000")).statusCode).toBe(404)
|
||||
expect((await call("GET", "/admin/users", mario.headers)).statusCode).toBe(403)
|
||||
expect((await t.app.inject({ method: "GET", url: "/admin/users" })).statusCode).toBe(401)
|
||||
})
|
||||
|
||||
it("disabilita: sessioni chiuse e login rifiutato; riabilita: login di nuovo possibile", async () => {
|
||||
const mario = await createUserAndLogin(t.app, "mario@example.com")
|
||||
const res = await call("POST", `/admin/users/${mario.user.id}/disable`)
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.json()).toMatchObject({ medusaSynced: null })
|
||||
expect(res.json().disabledAt).not.toBeNull()
|
||||
|
||||
expect((await login("mario@example.com")).statusCode).toBe(401)
|
||||
expect((await call("GET", "/me/tickets", mario.headers)).statusCode).toBe(401)
|
||||
expect((await call("GET", "/admin/users?status=disabled")).json().total).toBe(1)
|
||||
|
||||
const enabled = await call("POST", `/admin/users/${mario.user.id}/enable`)
|
||||
expect(enabled.json().disabledAt).toBeNull()
|
||||
expect((await login("mario@example.com")).statusCode).toBe(200)
|
||||
})
|
||||
|
||||
it("installatore disabilitato esce dal gruppo del listino riservato, riabilitato rientra", async () => {
|
||||
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
|
||||
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl", status: "approved", approvedAt: new Date() })
|
||||
expect((await call("POST", `/admin/installers/${inst.user.id}/sync`)).json()).toEqual({ member: true })
|
||||
const customerId = t.medusa.accountByEmail("inst@example.com")!.id
|
||||
|
||||
const off = await call("POST", `/admin/users/${inst.user.id}/disable`)
|
||||
expect(off.json()).toMatchObject({ medusaSynced: true, installerStatus: "approved" })
|
||||
expect(t.medusa.installerGroup.has(customerId)).toBe(false)
|
||||
|
||||
const on = await call("POST", `/admin/users/${inst.user.id}/enable`)
|
||||
expect(on.json()).toMatchObject({ medusaSynced: true })
|
||||
expect(t.medusa.installerGroup.has(customerId)).toBe(true)
|
||||
|
||||
// Medusa giù: l'operazione riesce comunque
|
||||
t.medusa.down = true
|
||||
expect((await call("POST", `/admin/users/${inst.user.id}/disable`)).json()).toMatchObject({ medusaSynced: false })
|
||||
})
|
||||
|
||||
it("un admin non può disabilitare se stesso", async () => {
|
||||
const res = await call("POST", `/admin/users/${admin.user.id}/disable`)
|
||||
expect(res.statusCode).toBe(409)
|
||||
expect(res.json().error.code).toBe("SELF_DISABLE")
|
||||
})
|
||||
})
|
||||
|
||||
describe("admin dashboard", () => {
|
||||
it("contatori", async () => {
|
||||
const empty = await call("GET", "/admin/summary")
|
||||
expect(empty.statusCode).toBe(200)
|
||||
expect(Object.values(empty.json()).every((v) => v === 0)).toBe(true)
|
||||
|
||||
const mario = await createUserAndLogin(t.app, "mario@example.com")
|
||||
const inst = await createUserAndLogin(t.app, "inst@example.com", "installer")
|
||||
await t.app.db.insert(installerProfiles).values({ userId: inst.user.id, businessName: "Impianti Srl" })
|
||||
const base = { email: "x@example.com", name: "X", configuratorInput: {} }
|
||||
await t.app.db.insert(quoteRequests).values([base, base, { ...base, status: "closed" }])
|
||||
await t.app.db.insert(tickets).values([
|
||||
{ userId: mario.user.id, subject: "a", status: "open" },
|
||||
{ userId: mario.user.id, subject: "b", status: "waiting_customer" },
|
||||
{ userId: mario.user.id, subject: "c", status: "closed" },
|
||||
])
|
||||
const contact = { name: "X", email: "x@example.com", message: "Messaggio di prova" }
|
||||
await t.app.db.insert(contactRequests).values([contact, { ...contact, status: "closed" }])
|
||||
const day = 24 * 3600 * 1000
|
||||
const booking = { name: "X", email: "x@example.com", people: 2 }
|
||||
await t.app.db.insert(showroomBookings).values([
|
||||
{ ...booking, requestedAt: new Date(Date.now() + 2 * day), status: "confirmed" },
|
||||
{ ...booking, requestedAt: new Date(Date.now() + 3 * day), status: "cancelled" },
|
||||
{ ...booking, requestedAt: new Date(Date.now() - 2 * day), status: "confirmed" },
|
||||
])
|
||||
await t.app.db.insert(quotes).values([
|
||||
{ userId: mario.user.id, year: 2026, seq: 1, number: "2026-0001", title: "A", amountCents: 1, status: "sent" },
|
||||
{ userId: mario.user.id, year: 2026, seq: 2, number: "2026-0002", title: "B", amountCents: 1, status: "sent", validUntil: "2020-01-01" },
|
||||
])
|
||||
|
||||
expect((await call("GET", "/admin/summary")).json()).toEqual({
|
||||
newQuoteRequests: 2,
|
||||
openTickets: 1,
|
||||
activeTickets: 2,
|
||||
pendingInstallers: 1,
|
||||
upcomingShowroomBookings: 1,
|
||||
newContactRequests: 1,
|
||||
sentQuotes: 1,
|
||||
})
|
||||
expect((await call("GET", "/admin/summary", mario.headers)).statusCode).toBe(403)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user