import { generateKeyPairSync } from 'crypto'; import express from 'express'; import request from 'supertest'; import nock from 'nock'; import jwt from 'jsonwebtoken'; process.env.KEYCLOAK_BASE_URL = 'http://keycloak.test'; process.env.KEYCLOAK_REALM = 'scouthub'; process.env.KEYCLOAK_ORG_SERVICE_CLIENT_ID = 'test-client'; process.env.KEYCLOAK_ORG_SERVICE_CLIENT_SECRET = 'test-secret'; process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_home_test'; process.env.FRONTEND_BASE_URL = 'http://localhost:4200'; import { authenticate } from '../../../src/middleware/authenticate'; import { requireRole } from '../../../src/middleware/requireRole'; const KEYCLOAK_HOST = 'http://keycloak.test'; const CERTS_PATH = '/realms/scouthub/protocol/openid-connect/certs'; const KID = 'test-kid'; const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); const jwk = publicKey.export({ format: 'jwk' }) as Record; const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string; // Keypair "estranea": usata per firmare token che non devono validare contro // le chiavi pubblicate nel JWKS mockato (firma non corrispondente). const { privateKey: rogueKey } = generateKeyPairSync('rsa', { modulusLength: 2048 }); const rogueKeyPem = rogueKey.export({ type: 'pkcs1', format: 'pem' }) as string; function mockJwks(): void { nock(KEYCLOAK_HOST) .persist() .get(CERTS_PATH) .reply(200, { keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }] }); } function signValidToken(payload: object, signOptions: jwt.SignOptions = {}): string { return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m', ...signOptions, }); } const CAPO_GRUPPO_PAYLOAD = { sub: 'user-123', email: 'capo@example.com', realm_access: { roles: ['capo-gruppo', 'utente'] }, organization: { 'gruppo-alfa': { id: 'org-1', roles: ['owner'] }, }, }; function buildApp() { const app = express(); app.get('/capo', authenticate, requireRole('capo-gruppo'), (req, res) => { res.json({ auth: req.auth }); }); app.get('/solo-admin', authenticate, requireRole('super-admin'), (req, res) => { res.json({ auth: req.auth }); }); return app; } beforeAll(() => { mockJwks(); }); afterAll(() => { nock.cleanAll(); }); describe('authenticate + requireRole', () => { test('restituisce 401 se manca il token', async () => { const app = buildApp(); const response = await request(app).get('/capo'); expect(response.status).toBe(401); }); test('restituisce 401 se il token è scaduto', async () => { const app = buildApp(); const token = signValidToken(CAPO_GRUPPO_PAYLOAD, { expiresIn: '-10s' }); const response = await request(app).get('/capo').set('Authorization', `Bearer ${token}`); expect(response.status).toBe(401); }); test('restituisce 401 se il token ha una firma non valida', async () => { const app = buildApp(); const token = jwt.sign(CAPO_GRUPPO_PAYLOAD, rogueKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m', }); const response = await request(app).get('/capo').set('Authorization', `Bearer ${token}`); expect(response.status).toBe(401); }); test('restituisce 403 se il token è valido ma manca il ruolo richiesto', async () => { const app = buildApp(); const token = signValidToken(CAPO_GRUPPO_PAYLOAD); const response = await request(app).get('/solo-admin').set('Authorization', `Bearer ${token}`); expect(response.status).toBe(403); }); test('restituisce 200 e popola req.auth se il token è valido con il ruolo corretto', async () => { const app = buildApp(); const token = signValidToken(CAPO_GRUPPO_PAYLOAD); const response = await request(app).get('/capo').set('Authorization', `Bearer ${token}`); expect(response.status).toBe(200); expect(response.body.auth).toEqual({ userId: 'user-123', email: 'capo@example.com', organizationId: 'org-1', roles: ['capo-gruppo', 'utente', 'owner'], }); }); });