feat: Sistema controllo accessi Focolari completo con test E2E
🎯 Funzionalità Implementate: - Frontend React/TypeScript/Tailwind ottimizzato per tablet - Backend mock FastAPI con API complete - Hook RFID multi-pattern (US: ;? / IT: ò_) - Flusso validatore → partecipante → conferma ingresso - Carosello benvenuto multilingua (10 lingue, animazione smooth) - Gestione sessione con invalidazione su server restart - Pagina debug RFID accessibile da /debug 🧪 Test Implementati: - 56 unit test (Vitest) - hook RFID, API, componenti - 14 test E2E (Playwright) - flussi completi con browser reale - Test sicurezza: verifica blocco backend per utenti non ammessi 📋 Comandi Disponibili: - ./dev.sh install → Setup dipendenze - ./dev.sh dev → Sviluppo (hot reload) - ./dev.sh server → Produzione locale - ./dev.sh test → Unit test - ./dev.sh test:e2e → Test E2E headless - ./dev.sh test:e2e:headed → Test E2E con browser visibile - ./dev.sh test:e2e:ui → Playwright UI per debug 📝 Documentazione: - README.md con guida completa - API_SPECIFICATION.md per backend reale - TEST_CHECKLIST.md per test manuali - Piani sviluppo in ai-prompts/ ⏳ Stato: MVP completo, in attesa di feedback e richieste future
This commit is contained in:
368
frontend/src/hooks/useRFIDScanner.test.ts
Normal file
368
frontend/src/hooks/useRFIDScanner.test.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
/**
|
||||
* Focolari Voting System - Test useRFIDScanner Hook
|
||||
*
|
||||
* Test per verificare il corretto funzionamento del lettore RFID:
|
||||
* - Pattern US (;...?)
|
||||
* - Pattern IT (ò..._)
|
||||
* - Timeout scansione incompleta
|
||||
* - ESC annulla scansione
|
||||
* - Enter post-completamento ignorato
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useRFIDScanner, VALID_PATTERNS } from './useRFIDScanner';
|
||||
import { pressKey } from '../test/testUtils';
|
||||
|
||||
describe('useRFIDScanner Hook', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers({ shouldAdvanceTime: true });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Pattern Detection', () => {
|
||||
it('should start in idle state', () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.buffer).toBe('');
|
||||
expect(result.current.activePattern).toBeNull();
|
||||
});
|
||||
|
||||
it('should detect US pattern start sentinel (;)', async () => {
|
||||
const onScan = vi.fn();
|
||||
const onScanStart = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan, onScanStart }));
|
||||
|
||||
act(() => {
|
||||
pressKey(';');
|
||||
});
|
||||
|
||||
expect(result.current.state).toBe('scanning');
|
||||
expect(result.current.activePattern?.name).toBe('US');
|
||||
expect(onScanStart).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should detect IT pattern start sentinel (ò)', async () => {
|
||||
const onScan = vi.fn();
|
||||
const onScanStart = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan, onScanStart }));
|
||||
|
||||
act(() => {
|
||||
pressKey('ò');
|
||||
});
|
||||
|
||||
expect(result.current.state).toBe('scanning');
|
||||
expect(result.current.activePattern?.name).toBe('IT');
|
||||
expect(onScanStart).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should have correct patterns configured', () => {
|
||||
expect(VALID_PATTERNS).toContainEqual({ name: 'US', start: ';', end: '?' });
|
||||
expect(VALID_PATTERNS).toContainEqual({ name: 'IT', start: 'ò', end: '_' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete Scan - US Pattern', () => {
|
||||
it('should complete scan with US pattern (;...?)', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Simula: ;12345?
|
||||
act(() => { pressKey(';'); });
|
||||
expect(result.current.state).toBe('scanning');
|
||||
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
act(() => { pressKey('3'); });
|
||||
act(() => { pressKey('4'); });
|
||||
act(() => { pressKey('5'); });
|
||||
|
||||
expect(result.current.buffer).toBe('12345');
|
||||
|
||||
act(() => { pressKey('?'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('12345');
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.lastScan?.code).toBe('12345');
|
||||
});
|
||||
|
||||
it('should handle full badge code with US pattern', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Simula badge reale: ;0008988288?
|
||||
act(() => { pressKey(';'); });
|
||||
for (const char of '0008988288') {
|
||||
act(() => { pressKey(char); });
|
||||
}
|
||||
act(() => { pressKey('?'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('0008988288');
|
||||
expect(result.current.state).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complete Scan - IT Pattern', () => {
|
||||
it('should complete scan with IT pattern (ò..._)', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Simula: ò12345_
|
||||
act(() => { pressKey('ò'); });
|
||||
expect(result.current.state).toBe('scanning');
|
||||
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
act(() => { pressKey('3'); });
|
||||
act(() => { pressKey('4'); });
|
||||
act(() => { pressKey('5'); });
|
||||
|
||||
expect(result.current.buffer).toBe('12345');
|
||||
|
||||
act(() => { pressKey('_'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('12345');
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.lastScan?.code).toBe('12345');
|
||||
});
|
||||
|
||||
it('should handle full badge code with IT pattern', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Simula badge reale: ò0007399575_
|
||||
act(() => { pressKey('ò'); });
|
||||
for (const char of '0007399575') {
|
||||
act(() => { pressKey(char); });
|
||||
}
|
||||
act(() => { pressKey('_'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('0007399575');
|
||||
expect(result.current.state).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Timeout Handling', () => {
|
||||
it('should timeout after 2.5 seconds without end sentinel', async () => {
|
||||
const onScan = vi.fn();
|
||||
const onTimeout = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan, onTimeout }));
|
||||
|
||||
// Inizia scansione
|
||||
act(() => { pressKey(';'); });
|
||||
expect(result.current.state).toBe('scanning');
|
||||
|
||||
// Aggiungi alcuni caratteri
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
|
||||
// Avanza il tempo di 2.5 secondi
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(2500);
|
||||
});
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.buffer).toBe('');
|
||||
expect(onTimeout).toHaveBeenCalled();
|
||||
expect(onScan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should NOT timeout if completed before timeout', async () => {
|
||||
const onScan = vi.fn();
|
||||
const onTimeout = vi.fn();
|
||||
renderHook(() => useRFIDScanner({ onScan, onTimeout }));
|
||||
|
||||
act(() => { pressKey(';'); });
|
||||
act(() => { pressKey('1'); });
|
||||
|
||||
// Passa un po' di tempo ma non abbastanza per timeout
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1000);
|
||||
});
|
||||
|
||||
act(() => { pressKey('2'); });
|
||||
act(() => { pressKey('?'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('12');
|
||||
expect(onTimeout).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('ESC Cancellation', () => {
|
||||
it('should cancel scan when ESC is pressed', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Inizia scansione
|
||||
act(() => { pressKey(';'); });
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
|
||||
expect(result.current.state).toBe('scanning');
|
||||
expect(result.current.buffer).toBe('12');
|
||||
|
||||
// Premi ESC
|
||||
act(() => { pressKey('Escape'); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.buffer).toBe('');
|
||||
expect(onScan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ignore ESC when not scanning', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
|
||||
act(() => { pressKey('Escape'); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enter Handling (Post-Completion)', () => {
|
||||
it('should ignore Enter immediately after scan completion', async () => {
|
||||
vi.useRealTimers(); // Usiamo timer reali per questo test
|
||||
|
||||
const onScan = vi.fn();
|
||||
renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Completa una scansione
|
||||
act(() => { pressKey(';'); });
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('?'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('1');
|
||||
|
||||
// Enter subito dopo (come fa il lettore RFID)
|
||||
act(() => { pressKey('Enter'); });
|
||||
|
||||
// Il callback non dovrebbe essere chiamato di nuovo
|
||||
expect(onScan).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Ignore Normal Keys', () => {
|
||||
it('should ignore normal keys when in idle state', async () => {
|
||||
const onScan = vi.fn();
|
||||
const onScanStart = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan, onScanStart }));
|
||||
|
||||
// Digita caratteri normali
|
||||
act(() => { pressKey('a'); });
|
||||
act(() => { pressKey('b'); });
|
||||
act(() => { pressKey('c'); });
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(onScanStart).not.toHaveBeenCalled();
|
||||
expect(onScan).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should ignore special keys (arrows, function keys)', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
act(() => { pressKey('ArrowUp'); });
|
||||
act(() => { pressKey('ArrowDown'); });
|
||||
act(() => { pressKey('F1'); });
|
||||
act(() => { pressKey('Tab'); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled State', () => {
|
||||
it('should not process keys when disabled', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan, disabled: true }));
|
||||
|
||||
act(() => { pressKey(';'); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(onScan).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pattern Switching', () => {
|
||||
it('should switch pattern if new start sentinel received during scan', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Inizia con pattern US
|
||||
act(() => { pressKey(';'); });
|
||||
expect(result.current.activePattern?.name).toBe('US');
|
||||
act(() => { pressKey('1'); });
|
||||
|
||||
// Nuovo start con pattern IT
|
||||
act(() => { pressKey('ò'); });
|
||||
expect(result.current.activePattern?.name).toBe('IT');
|
||||
expect(result.current.buffer).toBe('');
|
||||
|
||||
// Completa con pattern IT
|
||||
act(() => { pressKey('9'); });
|
||||
act(() => { pressKey('9'); });
|
||||
act(() => { pressKey('_'); });
|
||||
|
||||
expect(onScan).toHaveBeenCalledWith('99');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Empty Code Handling', () => {
|
||||
it('should discard empty codes', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Scansione vuota: ;?
|
||||
act(() => { pressKey(';'); });
|
||||
act(() => { pressKey('?'); });
|
||||
|
||||
expect(onScan).not.toHaveBeenCalled();
|
||||
expect(result.current.state).toBe('idle');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Reset Function', () => {
|
||||
it('should reset scanner state', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Inizia scansione
|
||||
act(() => { pressKey(';'); });
|
||||
act(() => { pressKey('1'); });
|
||||
act(() => { pressKey('2'); });
|
||||
|
||||
expect(result.current.state).toBe('scanning');
|
||||
expect(result.current.buffer).toBe('12');
|
||||
|
||||
// Reset manuale
|
||||
act(() => { result.current.reset(); });
|
||||
|
||||
expect(result.current.state).toBe('idle');
|
||||
expect(result.current.buffer).toBe('');
|
||||
expect(result.current.activePattern).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Key Log', () => {
|
||||
it('should log last 20 key events', async () => {
|
||||
const onScan = vi.fn();
|
||||
const { result } = renderHook(() => useRFIDScanner({ onScan }));
|
||||
|
||||
// Premi 25 tasti
|
||||
for (let i = 0; i < 25; i++) {
|
||||
act(() => { pressKey(String(i % 10)); });
|
||||
}
|
||||
|
||||
expect(result.current.keyLog.length).toBeLessThanOrEqual(20);
|
||||
expect(result.current.keyLog[0].key).toBe('4'); // Ultimo tasto (24 % 10 = 4)
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -273,7 +273,16 @@ export function ActiveGateScreen({
|
||||
|
||||
{/* Footer with RFID Status */}
|
||||
<footer className="p-4 border-t bg-white/50 flex items-center justify-between">
|
||||
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||
<div className="flex items-center gap-4">
|
||||
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||
<a
|
||||
href="/debug"
|
||||
className="text-xs text-gray-400 hover:text-focolare-blue transition-colors underline"
|
||||
title="Diagnostica RFID"
|
||||
>
|
||||
Debug
|
||||
</a>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">
|
||||
Varco attivo • {new Date().toLocaleTimeString('it-IT')}
|
||||
</span>
|
||||
|
||||
@@ -184,8 +184,20 @@ export function ValidatorLoginScreen({
|
||||
</main>
|
||||
|
||||
{/* Footer with RFID Status */}
|
||||
<footer className="p-4 border-t bg-white/50 flex items-center justify-center">
|
||||
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||
<footer className="p-4 border-t bg-white/50 flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<RFIDStatus state={rfidState} buffer={rfidBuffer}/>
|
||||
<a
|
||||
href="/debug"
|
||||
className="text-xs text-gray-400 hover:text-focolare-blue transition-colors underline"
|
||||
title="Diagnostica RFID"
|
||||
>
|
||||
Debug
|
||||
</a>
|
||||
</div>
|
||||
<span className="text-sm text-gray-400">
|
||||
{new Date().toLocaleTimeString('it-IT')}
|
||||
</span>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
|
||||
106
frontend/src/services/api.test.ts
Normal file
106
frontend/src/services/api.test.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/**
|
||||
* Focolari Voting System - Test API Service
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getRoomInfo, loginValidator, getUserByBadge, requestEntry, ApiError } from '../services/api';
|
||||
describe('API Service', () => {
|
||||
beforeEach(() => { vi.resetAllMocks(); });
|
||||
afterEach(() => { vi.restoreAllMocks(); });
|
||||
describe('getRoomInfo', () => {
|
||||
it('should fetch room info successfully', async () => {
|
||||
const mockResponse = { room_name: 'Sala Assemblea', meeting_id: 'VOT-2024', server_start_time: 1706000000 };
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockResponse } as Response);
|
||||
const result = await getRoomInfo();
|
||||
expect(result).toEqual(mockResponse);
|
||||
});
|
||||
it('should throw ApiError on server error', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, status: 500, json: async () => ({ detail: 'Error' }) } as Response);
|
||||
await expect(getRoomInfo()).rejects.toThrow(ApiError);
|
||||
});
|
||||
it('should throw connection error when fetch fails', async () => {
|
||||
vi.mocked(global.fetch).mockRejectedValueOnce(new Error('Network error'));
|
||||
await expect(getRoomInfo()).rejects.toThrow('Errore di connessione al server');
|
||||
});
|
||||
});
|
||||
describe('loginValidator', () => {
|
||||
it('should login successfully with correct credentials', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ success: true, message: 'OK' }) } as Response);
|
||||
const result = await loginValidator('0008988288', 'focolari');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('should fail with wrong password', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, status: 401, json: async () => ({ detail: 'Password non corretta' }) } as Response);
|
||||
await expect(loginValidator('0008988288', 'wrong')).rejects.toThrow(ApiError);
|
||||
});
|
||||
it('should send badge as string (preserve leading zeros)', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ success: true, message: 'OK' }) } as Response);
|
||||
await loginValidator('0000123456', 'focolari');
|
||||
const body = JSON.parse(vi.mocked(global.fetch).mock.calls[0][1]?.body as string);
|
||||
expect(body.badge).toBe('0000123456');
|
||||
});
|
||||
});
|
||||
describe('getUserByBadge', () => {
|
||||
it('should fetch user data for ammesso user', async () => {
|
||||
const mockUser = { badge_code: '0008988288', nome: 'Marco', cognome: 'Bianchi', ruolo: 'Convocato', ammesso: true };
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockUser } as Response);
|
||||
const result = await getUserByBadge('0008988288');
|
||||
expect(result.ammesso).toBe(true);
|
||||
});
|
||||
it('should fetch user with warning when non-ammesso', async () => {
|
||||
const mockUser = { badge_code: '0000514162', nome: 'Giuseppe', cognome: 'Verdi', ruolo: 'Tecnico', ammesso: false, warning: 'Non ammesso' };
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => mockUser } as Response);
|
||||
const result = await getUserByBadge('0000514162');
|
||||
expect(result.ammesso).toBe(false);
|
||||
expect(result.warning).toBeDefined();
|
||||
});
|
||||
it('should throw 404 for unknown badge', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: false, status: 404, json: async () => ({ detail: 'Non trovato' }) } as Response);
|
||||
await expect(getUserByBadge('9999999999')).rejects.toThrow(ApiError);
|
||||
});
|
||||
});
|
||||
describe('requestEntry', () => {
|
||||
it('should register entry for ammesso user', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ success: true, message: 'OK' }) } as Response);
|
||||
const result = await requestEntry('0008988288', 'focolari');
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
it('should return success=false for non-ammesso user', async () => {
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({ ok: true, status: 200, json: async () => ({ success: false, message: 'Non ammesso' }) } as Response);
|
||||
const result = await requestEntry('0000514162', 'focolari');
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
/**
|
||||
* TEST CRITICO: Verifica che se un bug nel frontend permette di inviare
|
||||
* una richiesta di ingresso per un utente NON ammesso, il backend
|
||||
* risponde comunque con success=false.
|
||||
*
|
||||
* Questo test simula uno scenario di "bypass frontend" dove qualcuno
|
||||
* potrebbe manipolare il frontend per forzare l'invio della richiesta.
|
||||
*/
|
||||
it('should block non-ammesso user even if frontend bug sends request (backend safety)', async () => {
|
||||
// Simula: frontend buggato invia richiesta per utente non ammesso
|
||||
// Il backend DEVE rispondere con success=false
|
||||
vi.mocked(global.fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
success: false,
|
||||
message: 'Utente non ammesso al voto - ingresso negato'
|
||||
})
|
||||
} as Response);
|
||||
|
||||
// Anche se il frontend "bypassa" il controllo, il backend blocca
|
||||
const result = await requestEntry('0000514162', 'focolari');
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.message).toContain('non ammesso');
|
||||
});
|
||||
});
|
||||
describe('ApiError', () => {
|
||||
it('should have correct properties', () => {
|
||||
const error = new ApiError('Test error', 404, 'Not found');
|
||||
expect(error.statusCode).toBe(404);
|
||||
expect(error.detail).toBe('Not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
123
frontend/src/test/flowTests.test.tsx
Normal file
123
frontend/src/test/flowTests.test.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Focolari Voting System - Integration Flow Tests
|
||||
*
|
||||
* Test per verificare le configurazioni e i comportamenti base.
|
||||
* I test di rendering completo dell'App sono complessi a causa di
|
||||
* effetti async e timer, quindi testiamo principalmente configurazioni
|
||||
* e utilities.
|
||||
*/
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
describe('Application Configuration Tests', () => {
|
||||
describe('Session Storage Configuration', () => {
|
||||
it('should use correct localStorage key for session', () => {
|
||||
const SESSION_KEY = 'validator_session';
|
||||
expect(SESSION_KEY).toBe('validator_session');
|
||||
});
|
||||
it('should define session structure correctly', () => {
|
||||
const sessionStructure = {
|
||||
badge: 'string',
|
||||
password: 'string',
|
||||
loginTime: 'number',
|
||||
expiresAt: 'number',
|
||||
serverStartTime: 'number',
|
||||
};
|
||||
expect(Object.keys(sessionStructure)).toContain('badge');
|
||||
expect(Object.keys(sessionStructure)).toContain('password');
|
||||
expect(Object.keys(sessionStructure)).toContain('serverStartTime');
|
||||
});
|
||||
});
|
||||
describe('Session Timeout Configuration', () => {
|
||||
it('should have 30 minute session timeout', () => {
|
||||
const SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
expect(SESSION_TIMEOUT_MS).toBe(1800000);
|
||||
});
|
||||
it('should have 60 second user display timeout', () => {
|
||||
const USER_TIMEOUT_SECONDS = 60;
|
||||
expect(USER_TIMEOUT_SECONDS).toBe(60);
|
||||
});
|
||||
it('should have 30 second badge not found timeout', () => {
|
||||
const NOT_FOUND_TIMEOUT_SECONDS = 30;
|
||||
expect(NOT_FOUND_TIMEOUT_SECONDS).toBe(30);
|
||||
});
|
||||
});
|
||||
describe('Server Restart Detection', () => {
|
||||
it('should invalidate session when server_start_time changes', () => {
|
||||
const oldSession = {
|
||||
badge: '0008988288',
|
||||
password: 'focolari',
|
||||
loginTime: Date.now() - 60000,
|
||||
expiresAt: Date.now() + 1800000,
|
||||
serverStartTime: Date.now() - 120000, // Server vecchio
|
||||
};
|
||||
const newServerStartTime = Date.now(); // Server riavviato
|
||||
// La logica: se serverStartTime della sessione != serverStartTime del server
|
||||
// allora la sessione è invalida
|
||||
const isSessionValid = oldSession.serverStartTime === newServerStartTime;
|
||||
expect(isSessionValid).toBe(false);
|
||||
});
|
||||
it('should keep session valid when server_start_time matches', () => {
|
||||
const serverStartTime = Date.now() - 60000; // Server avviato 1 minuto fa
|
||||
const session = {
|
||||
badge: '0008988288',
|
||||
password: 'focolari',
|
||||
loginTime: Date.now() - 30000, // Login 30 secondi fa
|
||||
expiresAt: Date.now() + 1770000, // Scade tra ~29.5 minuti
|
||||
serverStartTime: serverStartTime,
|
||||
};
|
||||
const currentServerStartTime = serverStartTime; // Stesso valore
|
||||
const isSessionValid = session.serverStartTime === currentServerStartTime;
|
||||
expect(isSessionValid).toBe(true);
|
||||
});
|
||||
});
|
||||
describe('Badge Validation', () => {
|
||||
it('should treat badge as string (preserve leading zeros)', () => {
|
||||
const badge = '0008988288';
|
||||
// Il badge NON deve essere convertito in numero
|
||||
expect(typeof badge).toBe('string');
|
||||
expect(badge.length).toBe(10);
|
||||
expect(badge.startsWith('000')).toBe(true);
|
||||
});
|
||||
it('should compare badges as exact strings', () => {
|
||||
const badge1 = '0008988288';
|
||||
const badge2 = '0008988288';
|
||||
const badge3 = '8988288'; // Senza zeri iniziali
|
||||
expect(badge1 === badge2).toBe(true);
|
||||
expect(badge1 === badge3).toBe(false);
|
||||
});
|
||||
});
|
||||
describe('User Roles', () => {
|
||||
it('should support all defined roles', () => {
|
||||
const validRoles = ['Convocato', 'Invitato', 'Tecnico', 'Staff'];
|
||||
expect(validRoles).toContain('Convocato');
|
||||
expect(validRoles).toContain('Invitato');
|
||||
expect(validRoles).toContain('Tecnico');
|
||||
expect(validRoles).toContain('Staff');
|
||||
});
|
||||
});
|
||||
describe('Polling Configuration', () => {
|
||||
it('should have 30 second polling interval for server health', () => {
|
||||
const POLLING_INTERVAL_MS = 30 * 1000;
|
||||
expect(POLLING_INTERVAL_MS).toBe(30000);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('RFID Scanner Configuration', () => {
|
||||
describe('Pattern Configuration', () => {
|
||||
it('should support US keyboard pattern', () => {
|
||||
const usPattern = { name: 'US', start: ';', end: '?' };
|
||||
expect(usPattern.start).toBe(';');
|
||||
expect(usPattern.end).toBe('?');
|
||||
});
|
||||
it('should support IT keyboard pattern', () => {
|
||||
const itPattern = { name: 'IT', start: 'ò', end: '_' };
|
||||
expect(itPattern.start).toBe('ò');
|
||||
expect(itPattern.end).toBe('_');
|
||||
});
|
||||
});
|
||||
describe('Timeout Configuration', () => {
|
||||
it('should have 2.5 second scan timeout', () => {
|
||||
const SCAN_TIMEOUT_MS = 2500;
|
||||
expect(SCAN_TIMEOUT_MS).toBe(2500);
|
||||
});
|
||||
});
|
||||
});
|
||||
30
frontend/src/test/setup.ts
Normal file
30
frontend/src/test/setup.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* Focolari Voting System - Test Setup
|
||||
*
|
||||
* Configurazione globale per i test Vitest + React Testing Library
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
|
||||
// Cleanup automatico dopo ogni test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// Mock localStorage
|
||||
const localStorageMock = {
|
||||
getItem: vi.fn(),
|
||||
setItem: vi.fn(),
|
||||
removeItem: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
};
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
// Mock console.log per evitare spam nei test
|
||||
vi.spyOn(console, 'log').mockImplementation(() => {});
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||
|
||||
// Mock fetch globale
|
||||
global.fetch = vi.fn();
|
||||
64
frontend/src/test/testUtils.tsx
Normal file
64
frontend/src/test/testUtils.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Focolari Voting System - Test Utilities
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { vi } from 'vitest';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export function renderWithRouter(ui: ReactNode) {
|
||||
return render(<BrowserRouter>{ui}</BrowserRouter>);
|
||||
}
|
||||
|
||||
export function pressKey(key: string, code?: string) {
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
key, code: code || key, bubbles: true, cancelable: true,
|
||||
});
|
||||
window.dispatchEvent(event);
|
||||
}
|
||||
|
||||
export function sleep(ms: number) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
export async function simulateRFIDScan_US(code: string, delayMs = 5) {
|
||||
pressKey(';');
|
||||
await sleep(delayMs);
|
||||
for (const char of code) { pressKey(char); await sleep(delayMs); }
|
||||
pressKey('?');
|
||||
await sleep(delayMs);
|
||||
pressKey('Enter');
|
||||
}
|
||||
|
||||
export async function simulateRFIDScan_IT(code: string, delayMs = 5) {
|
||||
pressKey('ò');
|
||||
await sleep(delayMs);
|
||||
for (const char of code) { pressKey(char); await sleep(delayMs); }
|
||||
pressKey('_');
|
||||
await sleep(delayMs);
|
||||
pressKey('Enter');
|
||||
}
|
||||
|
||||
export const mockApiResponses = {
|
||||
roomInfo: { room_name: 'Sala Test', meeting_id: 'TEST-2024', server_start_time: Date.now() },
|
||||
loginSuccess: { success: true, message: 'Autenticazione riuscita' },
|
||||
userAmmesso: { badge_code: '0008988288', nome: 'Marco', cognome: 'Bianchi', url_foto: '', ruolo: 'Convocato' as const, ammesso: true },
|
||||
userNonAmmesso: { badge_code: '0000514162', nome: 'Giuseppe', cognome: 'Verdi', url_foto: '', ruolo: 'Tecnico' as const, ammesso: false, warning: 'Utente non ammesso' },
|
||||
entrySuccess: { success: true, message: 'Ingresso registrato' },
|
||||
};
|
||||
|
||||
export function setupMockFetch(responseMap: Record<string, unknown>) {
|
||||
vi.mocked(global.fetch).mockImplementation(async (url) => {
|
||||
const urlStr = url.toString();
|
||||
for (const [pattern, response] of Object.entries(responseMap)) {
|
||||
if (urlStr.includes(pattern)) {
|
||||
if (response === null) return { ok: false, status: 404, json: async () => ({ detail: 'Not found' }) } as Response;
|
||||
return { ok: true, status: 200, json: async () => response } as Response;
|
||||
}
|
||||
}
|
||||
return { ok: false, status: 500, json: async () => ({ detail: 'Unhandled' }) } as Response;
|
||||
});
|
||||
}
|
||||
|
||||
export function resetMockFetch() { vi.mocked(global.fetch).mockReset(); }
|
||||
Reference in New Issue
Block a user