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 } 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) }) })