2026-05-19 14:08:03 +02:00
# Suite di Test
Tutti i test automatizzati del progetto stanno qui, separati da `app/` per una scelta precisa: il codice di produzione rimane pulito, la configurazione Vitest è indipendente dal build di Next.js, e i test girano senza avviare Docker o il database.
## Installazione e comandi
I comandi vanno lanciati dalla cartella `app/` :
``` bash
cd ~/ecommerce-platform/app
npm install # installa le dipendenze (incluse quelle di test)
npm run test # esegui tutti i test una volta (modalità CI)
npm run test:watch # riesegui automaticamente al salvataggio (sviluppo)
npm run test:coverage # genera il report HTML in app/coverage/index.html
```
## Stato attuale: 151 test, 10 file
| File | Area | Test |
|------|------|-----:|
| `unit/lib/validate.test.ts` | 10 schemi Zod (login, register, prodotti, ecc.) | 49 |
| `unit/lib/auth.test.ts` | hashToken, hashPassword, sessioni, getCurrentUser | 26 |
| `unit/lib/storage.test.ts` | magic bytes JPEG/PNG/WebP/ICO, saveImage, deleteImageFile | 11 |
| `unit/lib/email.test.ts` | sendEmail, sendOrderConfirmationEmail, sendPasswordResetEmail | 9 |
| `unit/lib/rate-limit.test.ts` | checkRateLimit (finestra 15 min), recordAttempt | 7 |
| `integration/api/auth/login.test.ts` | POST /api/auth/login — rate limit, validazione, credenziali | 9 |
| `integration/api/auth/register.test.ts` | POST /api/auth/register — duplicati, password, sessione | 9 |
| `integration/api/webhooks/stripe.test.ts` | checkout.session.completed, payment_intent, firma mancante | 11 |
| `components/ui/Button.test.tsx` | loading, disabled, varianti, onClick | 10 |
| `components/storefront/ProductCard.test.tsx` | prezzo, slug, immagine, placeholder "No image" | 10 |
## Struttura
```
test/
├── vitest.config.ts # ambiente happy-dom, alias @/, soglie copertura 70%
├── setup.ts # env vars mock, mock globale next/headers e next/navigation
├── tsconfig.json # alias @/ per il type-checker dell'IDE
│
├── unit/lib/ # funzioni pure di app/src/lib/ (nessun DB reale)
├── integration/api/ # API route Next.js 14 App Router (Prisma mockato)
│ ├── auth/
│ └── webhooks/
├── components/ # componenti React con @testing-library/react
│ ├── ui/
│ └── storefront/
├── __mocks__/
│ └── prisma.ts # mock centralizzato del Prisma client (tutti i vi.fn())
└── fixtures/
├── users.ts # mockUser, mockAdmin, mockSession
└── orders.ts # mockOrder, mockPayment, eventi Stripe
```
## Come funziona il mock di Prisma
Il file `test/__mocks__/prisma.ts` esporta un oggetto con `vi.fn()` per ogni metodo Prisma usato nel progetto. Nei test di integrazione basta importarlo all'inizio del file — il `vi.mock('@/lib/prisma')` è già incluso dentro.
``` typescript
import '../../../__mocks__/prisma' // monta il mock E chiama vi.mock internamente
import { prisma } from '@/lib/prisma'
beforeEach ( ( ) = > {
vi . clearAllMocks ( )
vi . mocked ( prisma . user . findUnique ) . mockResolvedValue ( mockUser as any )
} )
```
> **Nota sui percorsi**: i test di integrazione si trovano 3 livelli sotto `test/`
> (`integration/api/auth/`), quindi il percorso relativo verso `__mocks__/` è `../../../__mocks__/prisma`.
> I test unitari in `unit/lib/` usano invece `../../__mocks__/prisma`.
## Come testare una API route
Le route App Router sono funzioni TypeScript che accettano `NextRequest` → `NextResponse` . Si importano e si chiamano direttamente, senza un server HTTP.
``` typescript
import { NextRequest } from 'next/server'
import { POST } from '@/app/api/auth/login/route'
it ( 'restituisce 401 per password errata' , async ( ) = > {
vi . mocked ( prisma . user . findUnique ) . mockResolvedValue ( mockUser as any )
vi . mocked ( verifyPassword ) . mockResolvedValue ( false )
const req = new NextRequest (
new Request ( 'http://localhost/api/auth/login' , {
method : 'POST' ,
headers : { 'Content-Type' : 'application/json' , 'x-forwarded-for' : '1.2.3.4' } ,
body : JSON.stringify ( { email : 'test@example.com' , password : 'WrongPass1!' } ) ,
} )
)
const res = await POST ( req )
expect ( res . status ) . toBe ( 401 )
expect ( ( await res . json ( ) ) . error ) . toBe ( 'Invalid email or password' )
} )
```
Per route con parametri dinamici (es. `/api/admin/products/[id]` ):
``` typescript
import { GET } from '@/app/api/admin/products/[id]/route'
const res = await GET ( req , { params : { id : 'prod-123' } } )
```
## Aggiungere nuovi test
**Test unitario ** (per una nuova funzione in `lib/` ): copia la struttura di `unit/lib/email.test.ts` . Mocka le dipendenze esterne in cima con `vi.mock(...)` , poi testa solo la logica della funzione.
**Test di integrazione ** (per una nuova API route):
1. Crea il file nella cartella corrispondente sotto `integration/api/`
2. Importa `'../../../__mocks__/prisma'` (o `'../../__mocks__/prisma'` a seconda della profondità)
3. Mocka lib esterne (`@/lib/auth` , `@/lib/stripe` , ecc.) con `vi.mock`
4. Prepara i mock nel `beforeEach` con `vi.clearAllMocks()` + valori di ritorno
**Test di componente ** : copia `components/ui/Button.test.tsx` . Usa `render()` e `screen` di `@testing-library/react` . Testa comportamento visibile, non implementazione interna.
## Copertura
La soglia minima configurata in `vitest.config.ts` è **70% di linee e funzioni ** . Il comando `npm run test:coverage` fallisce se non viene rispettata. La copertura attuale è circa **11% di linee e 33% di funzioni ** — la soglia va abbassata temporaneamente o la suite va estesa.
2026-05-19 14:18:23 +02:00
### Output atteso durante l'esecuzione
Durante `npm test` o `npm run test:coverage` possono comparire righe che sembrano errori ma sono normali:
```
stderr | ...stripe.test.ts > returns 400 when signature verification fails
Webhook signature verification failed: stripe_signature_mismatch
```
La route `/api/webhooks/stripe` logga intenzionalmente il messaggio quando la firma è invalida. Il test verifica proprio questo scenario — il `stderr` è corretto.
```
stdout | ...stripe.test.ts > returns 200 for unhandled event types
Unhandled event type: customer.created
```
Anche questo è il log della route per eventi Stripe non gestiti. Comportamento atteso.
```
The CJS build of Vite's Node API is deprecated.
```
Warning di Vitest 1.x, innocuo. Sparirà aggiornando a Vitest 2+.
2026-05-19 14:08:03 +02:00
### Cosa manca (per priorità)
#### Alta priorità — logica di business critica
| File | Cosa testare |
|------|-------------|
| `lib/stripe.ts` | `createCheckoutSession` : parametri passati a Stripe, metadati orderId |
| `api/auth/logout/route.ts` | cancellazione cookie, chiamata a `deleteSession` |
| `api/auth/me/route.ts` | utente autenticato → 200, non autenticato → 401 |
| `api/auth/change-password/route.ts` | password errata → 401, nuova password debole → 400, successo → 200 |
| `api/checkout/route.ts` | utente non autenticato → 401, prodotto inesistente → 400, creazione ordine + sessione Stripe |
#### Media priorità — route admin (CRUD)
| File | Cosa testare |
|------|-------------|
| `api/admin/products/route.ts` | GET con paginazione/filtri, POST crea prodotto, accesso CUSTOMER → 403 |
| `api/admin/products/[id]/route.ts` | GET, PATCH, DELETE — prodotto inesistente → 404 |
| `api/admin/orders/route.ts` | lista ordini con filtri di stato |
| `api/admin/orders/[id]/route.ts` | aggiornamento stato ordine |
| `api/admin/categories/route.ts` | CRUD categorie, slug duplicato → 409 |
| `api/admin/settings/route.ts` | lettura e scrittura impostazioni sito |
| `api/admin/users/route.ts` | creazione admin, email duplicata → 409 |
#### Bassa priorità — componenti UI
| File | Cosa testare |
|------|-------------|
| `components/ui/Input.tsx` | rendering label, messaggio di errore, stato disabled |
| `components/ui/Alert.tsx` | varianti (success, error, warning, info) |
| `components/ui/Badge.tsx` | varianti colore, testo |
| `components/ui/Card.tsx` | rendering children |
### Come implementare i test mancanti
---
#### `unit/lib/stripe.test.ts`
Crea il file in `test/unit/lib/` . Mock dell'intero modulo `stripe` con `vi.hoisted` (necessario perché `stripe.ts` istanzia `new Stripe(...)` a module load time):
``` typescript
const { mockCreate , mockConstructEvent } = vi . hoisted ( ( ) = > ( {
mockCreate : vi.fn ( ) . mockResolvedValue ( { id : 'cs_test' , url : 'https://stripe.com/pay/cs_test' } ) ,
mockConstructEvent : vi.fn ( ) ,
} ) )
vi . mock ( 'stripe' , ( ) = > ( {
default : vi . fn ( ) . mockImplementation ( ( ) = > ( {
checkout : { sessions : { create : mockCreate } } ,
webhooks : { constructEvent : mockConstructEvent } ,
} ) ) ,
} ) )
import { createCheckoutSession , constructWebhookEvent } from '@/lib/stripe'
it ( 'passa orderId nei metadata' , async ( ) = > {
await createCheckoutSession ( {
orderId : 'order-1' ,
lineItems : [ { price_data : { currency : 'eur' , unit_amount : 1000 , product_data : { name : 'T-shirt' } } , quantity : 1 } ] ,
customerEmail : 'user@example.com' ,
successUrl : 'http://localhost/success' ,
cancelUrl : 'http://localhost/cancel' ,
} )
expect ( mockCreate ) . toHaveBeenCalledWith ( expect . objectContaining ( {
metadata : { orderId : 'order-1' } ,
} ) )
} )
```
---
#### `integration/api/auth/logout.test.ts`
Crea in `test/integration/api/auth/` . La route `logout` legge il cookie, cancella la sessione e pulisce il cookie:
``` typescript
import '../../../__mocks__/prisma'
import { NextRequest } from 'next/server'
import { POST } from '@/app/api/auth/logout/route'
import { cookies } from 'next/headers'
vi . mock ( '@/lib/auth' , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( '@/lib/auth' ) > ( )
return { . . . actual , deleteSession : vi.fn ( ) , clearSessionCookie : vi.fn ( ) }
} )
import { deleteSession , clearSessionCookie } from '@/lib/auth'
it ( 'cancella la sessione e il cookie se il token esiste' , async ( ) = > {
const mockCookieStore = { get : vi . fn ( ) . mockReturnValue ( { value : 'mytoken' } ) , set : vi . fn ( ) , delete : vi . fn ( ) }
vi . mocked ( cookies ) . mockReturnValue ( mockCookieStore as any )
const res = await POST ( )
expect ( res . status ) . toBe ( 200 )
expect ( deleteSession ) . toHaveBeenCalledWith ( 'mytoken' )
expect ( clearSessionCookie ) . toHaveBeenCalled ( )
} )
it ( 'risponde 200 anche senza token (utente già sloggato)' , async ( ) = > {
const mockCookieStore = { get : vi . fn ( ) . mockReturnValue ( undefined ) , set : vi . fn ( ) , delete : vi . fn ( ) }
vi . mocked ( cookies ) . mockReturnValue ( mockCookieStore as any )
const res = await POST ( )
expect ( res . status ) . toBe ( 200 )
expect ( deleteSession ) . not . toHaveBeenCalled ( )
} )
```
---
#### `integration/api/auth/me.test.ts`
La route `me` chiama `getCurrentUser()` — mocka solo `@/lib/auth` :
``` typescript
vi . mock ( '@/lib/auth' , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( '@/lib/auth' ) > ( )
return { . . . actual , getCurrentUser : vi.fn ( ) }
} )
import { getCurrentUser } from '@/lib/auth'
it ( 'restituisce i dati utente se autenticato' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( mockUser as any )
const res = await GET ( )
expect ( res . status ) . toBe ( 200 )
expect ( ( await res . json ( ) ) . user . email ) . toBe ( mockUser . email )
expect ( ( await res . json ( ) ) . user ) . not . toHaveProperty ( 'passwordHash' )
} )
it ( 'restituisce 401 se non autenticato' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( null )
const res = await GET ( )
expect ( res . status ) . toBe ( 401 )
} )
```
---
#### `integration/api/auth/change-password.test.ts`
La route richiede `getCurrentUser` , `verifyPassword` , `hashPassword` e `prisma.user.update` :
``` typescript
import '../../../__mocks__/prisma'
import { prisma } from '@/lib/prisma'
vi . mock ( '@/lib/auth' , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( '@/lib/auth' ) > ( )
return { . . . actual , getCurrentUser : vi.fn ( ) , verifyPassword : vi.fn ( ) , hashPassword : vi.fn ( ) . mockResolvedValue ( 'newhash' ) }
} )
import { getCurrentUser , verifyPassword } from '@/lib/auth'
// beforeEach: clearAllMocks, loginAttempt.count → 0, user.update → mockUser
it ( 'restituisce 401 se non autenticato' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( null )
const res = await POST ( makeRequest ( { currentPassword : 'Old1!' , newPassword : 'NewPass123!' } ) )
expect ( res . status ) . toBe ( 401 )
} )
it ( 'restituisce 400 se la password attuale è errata' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( { . . . mockUser , passwordHash : 'hash' } as any )
vi . mocked ( verifyPassword ) . mockResolvedValue ( false )
const res = await POST ( makeRequest ( { currentPassword : 'Wrong1!' , newPassword : 'NewPass123!' } ) )
expect ( res . status ) . toBe ( 400 )
expect ( ( await res . json ( ) ) . error ) . toMatch ( /incorrect/ )
} )
it ( 'aggiorna la password e azzera mustChangePassword' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( { . . . mockUser , passwordHash : 'hash' } as any )
vi . mocked ( verifyPassword ) . mockResolvedValue ( true )
vi . mocked ( prisma . user . update ) . mockResolvedValue ( mockUser as any )
const res = await POST ( makeRequest ( { currentPassword : 'OldPass1!' , newPassword : 'NewPass123!' } ) )
expect ( res . status ) . toBe ( 200 )
expect ( prisma . user . update ) . toHaveBeenCalledWith ( expect . objectContaining ( {
data : expect.objectContaining ( { mustChangePassword : false } ) ,
} ) )
} )
```
---
#### `integration/api/admin/products.test.ts`
Le route admin verificano il ruolo prima di tutto. Pattern chiave da testare: accesso negato, paginazione, creazione con audit log:
``` typescript
import '../../../__mocks__/prisma'
import { prisma } from '@/lib/prisma'
import { mockAdmin , mockUser } from '../../../fixtures/users'
vi . mock ( '@/lib/auth' , async ( importOriginal ) = > {
const actual = await importOriginal < typeof import ( '@/lib/auth' ) > ( )
return { . . . actual , getCurrentUser : vi.fn ( ) }
} )
import { getCurrentUser } from '@/lib/auth'
// Test GET
it ( 'restituisce 403 per utente CUSTOMER' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( mockUser as any ) // role: CUSTOMER
const res = await GET ( makeRequest ( ) )
expect ( res . status ) . toBe ( 403 )
} )
it ( 'restituisce la lista prodotti paginata per ADMIN' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( mockAdmin as any )
vi . mocked ( prisma . product . findMany ) . mockResolvedValue ( [ ] )
vi . mocked ( prisma . product . count ) . mockResolvedValue ( 0 )
const res = await GET ( makeRequest ( ) )
expect ( res . status ) . toBe ( 200 )
const data = await res . json ( )
expect ( data ) . toHaveProperty ( 'pagination' )
} )
// Test POST
it ( 'crea il prodotto e scrive l\'audit log' , async ( ) = > {
vi . mocked ( getCurrentUser ) . mockResolvedValue ( mockAdmin as any )
vi . mocked ( prisma . product . create ) . mockResolvedValue ( mockProduct as any )
vi . mocked ( prisma . auditLog . create ) . mockResolvedValue ( { } as any )
const res = await POST ( makeRequest ( validProduct ) )
expect ( res . status ) . toBe ( 201 )
expect ( prisma . auditLog . create ) . toHaveBeenCalledWith (
expect . objectContaining ( { data : expect.objectContaining ( { action : 'CREATE' , entity : 'Product' } ) } )
)
} )
```
---
#### `components/ui/Input.test.tsx`, `Alert.test.tsx`, `Badge.test.tsx`
Stessa struttura di `Button.test.tsx` . Leggi prima il file sorgente del componente, poi testa solo ciò che è visibile:
``` typescript
import { render , screen } from '@testing-library/react'
import { Input } from '@/components/ui/Input'
it ( 'mostra il messaggio di errore' , ( ) = > {
render ( < Input label = "Email" error = "Campo obbligatorio" / > )
expect ( screen . getByText ( 'Campo obbligatorio' ) ) . toBeTruthy ( )
} )
it ( 'ha attributo disabled quando disabled=true' , ( ) = > {
render ( < Input label = "Email" disabled / > )
expect ( screen . getByRole ( 'textbox' ) ) . toBeDisabled ( )
} )
```
---
#### Da non testare
- Le **23 pagine `page.tsx` ** — Server Components Next.js con fetch dati, richiedono il runtime Next.js completo per essere testati in modo significativo
- **`UserContext.tsx` ** — context React client-side, coperto implicitamente dai test di integrazione
- **`middleware.ts` ** — header di sicurezza statici, nessuna logica da verificare