TRIGGER: Use this skill before writing any validation function, backend service, or Vue composable that contains non-trivial logic. TRIGGER: Use when the user says "write tests for..." or "how do I test...". TRIGGER: Use before implementing an Express endpoint. DO NOT use for purely presentational components or configuration files.
Write the test before the code. If you cannot write the test, the design is wrong.
Registration tests write to users.json. Without a reset between tests, the second run fails because the email is already registered. Always use beforeEach/afterEach:
import fs from 'fs'
const USERS_FILE = './backend/users.json'
beforeEach(() => {
fs.writeFileSync(USERS_FILE, JSON.stringify([]))
})
afterEach(() => {
fs.writeFileSync(USERS_FILE, JSON.stringify([]))
})
Define a complete valid payload once and reuse it across all tests:
const validPayload = {
nome: 'Mario',
cognome: 'Rossi',
codiceFiscale: 'RSSMRA80A01H501U',
email: '[email protected]',
password: 'Password1',
telefono: '3331234567',
indirizzo: 'Via Roma 1',
repertori: ['musica'],
foto: null
}
import request from 'supertest'
import app from '../src/index'
import fs from 'fs'
const USERS_FILE = './backend/users.json'
beforeEach(() => fs.writeFileSync(USERS_FILE, JSON.stringify([])))
afterEach(() => fs.writeFileSync(USERS_FILE, JSON.stringify([])))
describe('POST /api/auth/register', () => {
it('201 with valid payload', async () => {
const res = await request(app).post('/api/auth/register').send(validPayload)
expect(res.status).toBe(201)
expect(res.body.token).toBeDefined()
})
it('409 if email already exists', async () => {
await request(app).post('/api/auth/register').send(validPayload)
const res = await request(app).post('/api/auth/register').send(validPayload)
expect(res.status).toBe(409)
expect(res.body.redirectTo).toBe('login')
})
it('409 if tax code already exists with different email', async () => {
await request(app).post('/api/auth/register').send(validPayload)
const res = await request(app).post('/api/auth/register').send({
...validPayload,
email: '[email protected]'
})
expect(res.status).toBe(409)
expect(res.body.redirectTo).toBe('login')
})
it('400 if tax code is invalid', async () => {
const res = await request(app).post('/api/auth/register').send({
...validPayload,
codiceFiscale: 'INVALID'
})
expect(res.status).toBe(400)
})
it('400 if password is non-compliant', async () => {
const res = await request(app).post('/api/auth/register').send({
...validPayload,
password: 'weak'
})
expect(res.status).toBe(400)
})
it('400 if required field is missing', async () => {
const { nome, ...withoutNome } = validPayload
const res = await request(app).post('/api/auth/register').send(withoutNome)
expect(res.status).toBe(400)
})
})
describe('POST /api/auth/login', () => {
beforeEach(async () => {
await request(app).post('/api/auth/register').send(validPayload)
})
it('200 with correct credentials', async () => {
const res = await request(app).post('/api/auth/login')
.send({ email: '[email protected]', password: 'Password1' })
expect(res.status).toBe(200)
expect(res.body.token).toBeDefined()
})
it('401 with wrong password — generic message', async () => {
const res = await request(app).post('/api/auth/login')
.send({ email: '[email protected]', password: 'wrongpassword' })
expect(res.status).toBe(401)
// RF-03: must not specify whether email or password is wrong
expect(res.body.error).not.toMatch(/email/i)
expect(res.body.error).not.toMatch(/password/i)
})
it('401 with non-existent email — same generic message', async () => {
const res = await request(app).post('/api/auth/login')
.send({ email: '[email protected]', password: 'Password1' })
expect(res.status).toBe(401)
expect(res.body.error).not.toMatch(/email/i)
expect(res.body.error).not.toMatch(/password/i)
})
})
describe('GET /api/user', () => {
let token: string
beforeEach(async () => {
await request(app).post('/api/auth/register').send(validPayload)
const res = await request(app).post('/api/auth/login')
.send({ email: '[email protected]', password: 'Password1' })
token = res.body.token
})
it('200 with valid token', async () => {
const res = await request(app).get('/api/user')
.set('Authorization', `Bearer ${token}`)
expect(res.status).toBe(200)
expect(res.body.nome).toBe('Mario')
expect(res.body.password).toBeUndefined() // never expose the password
})
it('401 without token', async () => {
const res = await request(app).get('/api/user')
expect(res.status).toBe(401)
})
it('401 with malformed token', async () => {
const res = await request(app).get('/api/user')
.set('Authorization', 'Bearer invalidtoken')
expect(res.status).toBe(401)
})
})
import { describe, it, expect } from 'vitest'
import {
validateNome,
validateCodiceFiscale,
validateEmail,
validatePassword,
validateTelefono,
validateRepertori
} from '../src/services/validationService'
describe('validateCodiceFiscale', () => {
it('accepts valid tax code', () => expect(validateCodiceFiscale('RSSMRA80A01H501U')).toBe(true))
it('rejects too-short tax code', () => expect(validateCodiceFiscale('ABC')).toBe(false))
it('is case-insensitive', () => expect(validateCodiceFiscale('rssmra80a01h501u')).toBe(true))
it('rejects empty string', () => expect(validateCodiceFiscale('')).toBe(false))
})
describe('validatePassword', () => {
it('accepts compliant password', () => expect(validatePassword('Password1')).toBe(true))
it('rejects password without uppercase', () => expect(validatePassword('password1')).toBe(false))
it('rejects password without number', () => expect(validatePassword('Password')).toBe(false))
it('rejects password under 8 characters', () => expect(validatePassword('Pa1')).toBe(false))
})
describe('validateRepertori', () => {
it('accepts at least one valid repertory', () => expect(validateRepertori(['musica'])).toBe(true))
it('rejects empty array', () => expect(validateRepertori([])).toBe(false))
it('rejects non-allowed value', () => expect(validateRepertori(['jazz'])).toBe(false))
})
import { describe, it, expect } from 'vitest'
import { useRegistrazione } from '@/composables/useRegistrazione'
describe('useRegistrazione', () => {
it('starts at step 1', () => {
const { currentStep } = useRegistrazione()
expect(currentStep.value).toBe(1)
})
it('advances to the next step', () => {
const { currentStep, nextStep } = useRegistrazione()
nextStep()
expect(currentStep.value).toBe(2)
})
it('does not go below step 1', () => {
const { currentStep, prevStep } = useRegistrazione()
prevStep()
expect(currentStep.value).toBe(1)
})
it('resets state after completion', () => {
const { currentStep, nextStep, reset } = useRegistrazione()
nextStep()
reset()
expect(currentStep.value).toBe(1)
})
})
cd backend && npx vitest run (single runner: vitest)cd frontend && npm run test → coverage target 70%bash plugin/tests/test-*.sh