import { generateKeyPairSync } from 'crypto'; 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'; const createOrganization = jest.fn(); const createOrganizationGroup = jest.fn(); const addMemberToOrganization = jest.fn(); const assignUserToGroup = jest.fn(); const assignRealmRoleToUser = jest.fn(); jest.mock('../../src/keycloak-admin', () => ({ createOrganization: (...args: unknown[]) => createOrganization(...args), createOrganizationGroup: (...args: unknown[]) => createOrganizationGroup(...args), addMemberToOrganization: (...args: unknown[]) => addMemberToOrganization(...args), assignUserToGroup: (...args: unknown[]) => assignUserToGroup(...args), assignRealmRoleToUser: (...args: unknown[]) => assignRealmRoleToUser(...args), })); const richiestaCreazioneGruppoFindFirst = jest.fn(); const richiestaCreazioneGruppoCreate = jest.fn(); const richiestaCreazioneGruppoFindMany = jest.fn(); const richiestaCreazioneGruppoFindUnique = jest.fn(); const richiestaCreazioneGruppoUpdate = jest.fn(); const gruppoScoutCreate = jest.fn(); jest.mock('../../src/db/prisma', () => ({ prisma: { richiestaCreazioneGruppo: { findFirst: (...args: unknown[]) => richiestaCreazioneGruppoFindFirst(...args), create: (...args: unknown[]) => richiestaCreazioneGruppoCreate(...args), findMany: (...args: unknown[]) => richiestaCreazioneGruppoFindMany(...args), findUnique: (...args: unknown[]) => richiestaCreazioneGruppoFindUnique(...args), update: (...args: unknown[]) => richiestaCreazioneGruppoUpdate(...args), }, gruppoScout: { create: (...args: unknown[]) => gruppoScoutCreate(...args), }, }, })); import { app } from '../../src/app'; 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; function signToken(payload: object): string { return jwt.sign(payload, privateKeyPem, { algorithm: 'RS256', keyid: KID, expiresIn: '5m' }); } function utenteToken(userId: string, email: string): string { return signToken({ sub: userId, email, realm_access: { roles: [] } }); } function adminCentraleToken(): string { return signToken({ sub: 'user-admin', email: 'admin@example.com', realm_access: { roles: ['admin'] }, }); } function capoGruppoToken(orgId: string): string { return signToken({ sub: 'user-capo', email: 'capo@example.com', realm_access: { roles: ['capo-gruppo'] }, organization: { alfa: { id: orgId, roles: [] } }, }); } const ORA = new Date(); function richiestaFixture(overrides: Partial> = {}) { return { id: 'richiesta-1', userId: 'user-nuovo', email: 'nuovo@example.com', nomeProposto: 'Gruppo Alfa', regione: 'Lombardia', stato: 'PENDING', createdAt: ORA, updatedAt: ORA, ...overrides, }; } beforeAll(() => { nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, { keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }], }); }); afterAll(() => { nock.cleanAll(); }); beforeEach(() => { jest.clearAllMocks(); }); describe('POST /richieste-creazione-gruppo', () => { test('crea una richiesta PENDING e risponde 201', async () => { richiestaCreazioneGruppoFindFirst.mockResolvedValueOnce(null); richiestaCreazioneGruppoCreate.mockResolvedValueOnce({ id: 'richiesta-1' }); const response = await request(app) .post('/richieste-creazione-gruppo') .set('Authorization', `Bearer ${utenteToken('user-nuovo', 'nuovo@example.com')}`) .send({ nomeProposto: 'Gruppo Alfa', regione: 'Lombardia' }); expect(response.status).toBe(201); expect(response.body).toEqual({ id: 'richiesta-1' }); expect(richiestaCreazioneGruppoCreate).toHaveBeenCalledWith({ data: { userId: 'user-nuovo', email: 'nuovo@example.com', nomeProposto: 'Gruppo Alfa', regione: 'Lombardia', stato: 'PENDING', }, }); }); test("risponde 409 se l'utente ha già una richiesta PENDING", async () => { richiestaCreazioneGruppoFindFirst.mockResolvedValueOnce(richiestaFixture()); const response = await request(app) .post('/richieste-creazione-gruppo') .set('Authorization', `Bearer ${utenteToken('user-nuovo', 'nuovo@example.com')}`) .send({ nomeProposto: 'Gruppo Beta' }); expect(response.status).toBe(409); expect(richiestaCreazioneGruppoCreate).not.toHaveBeenCalled(); }); test("risponde 400 se manca il campo 'nomeProposto'", async () => { const response = await request(app) .post('/richieste-creazione-gruppo') .set('Authorization', `Bearer ${utenteToken('user-nuovo', 'nuovo@example.com')}`) .send({ regione: 'Lombardia' }); expect(response.status).toBe(400); expect(richiestaCreazioneGruppoCreate).not.toHaveBeenCalled(); }); test('risponde 401 senza token', async () => { const response = await request(app).post('/richieste-creazione-gruppo').send({ nomeProposto: 'Gruppo Alfa' }); expect(response.status).toBe(401); expect(richiestaCreazioneGruppoCreate).not.toHaveBeenCalled(); }); }); describe('GET /richieste-creazione-gruppo', () => { test('restituisce le richieste PENDING per admin', async () => { richiestaCreazioneGruppoFindMany.mockResolvedValueOnce([richiestaFixture()]); const response = await request(app) .get('/richieste-creazione-gruppo') .set('Authorization', `Bearer ${adminCentraleToken()}`); expect(response.status).toBe(200); expect(response.body).toEqual([ { id: 'richiesta-1', email: 'nuovo@example.com', nomeProposto: 'Gruppo Alfa', regione: 'Lombardia', createdAt: ORA.toISOString(), }, ]); expect(richiestaCreazioneGruppoFindMany).toHaveBeenCalledWith({ where: { stato: 'PENDING' }, orderBy: { createdAt: 'asc' }, }); }); test('risponde 403 se il ruolo non è admin', async () => { const response = await request(app) .get('/richieste-creazione-gruppo') .set('Authorization', `Bearer ${capoGruppoToken('org-1')}`); expect(response.status).toBe(403); expect(richiestaCreazioneGruppoFindMany).not.toHaveBeenCalled(); }); test('risponde 401 senza token', async () => { const response = await request(app).get('/richieste-creazione-gruppo'); expect(response.status).toBe(401); expect(richiestaCreazioneGruppoFindMany).not.toHaveBeenCalled(); }); }); describe('PUT /richieste-creazione-gruppo/:id', () => { test('approva la richiesta: crea il gruppo, rende il richiedente capo-gruppo e aggiorna stato=APPROVATA', async () => { richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(richiestaFixture()); createOrganization.mockResolvedValueOnce({ orgId: 'org-nuovo' }); createOrganizationGroup.mockResolvedValue({ groupId: 'irrelevant' }); addMemberToOrganization.mockResolvedValueOnce(undefined); gruppoScoutCreate.mockResolvedValueOnce({}); assignUserToGroup.mockResolvedValueOnce(undefined); assignRealmRoleToUser.mockResolvedValueOnce(undefined); richiestaCreazioneGruppoUpdate.mockResolvedValueOnce(richiestaFixture({ stato: 'APPROVATA' })); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'approvata' }); expect(response.status).toBe(200); expect(response.body).toEqual({ id: 'richiesta-1', stato: 'APPROVATA' }); expect(createOrganization).toHaveBeenCalledWith('Gruppo Alfa'); expect(gruppoScoutCreate).toHaveBeenCalledWith({ data: { orgId: 'org-nuovo', nome: 'Gruppo Alfa', regione: 'Lombardia' }, }); expect(addMemberToOrganization).toHaveBeenCalledWith('org-nuovo', 'user-nuovo'); expect(assignUserToGroup).toHaveBeenCalledWith('user-nuovo', 'capo-gruppo'); expect(assignRealmRoleToUser).toHaveBeenCalledWith('user-nuovo', 'capo-gruppo'); expect(richiestaCreazioneGruppoUpdate).toHaveBeenCalledWith({ where: { id: 'richiesta-1' }, data: { stato: 'APPROVATA' }, }); }); test("risponde 502 e NON marca la richiesta come approvata se l'assegnazione del ruolo capo-gruppo fallisce", async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(richiestaFixture()); createOrganization.mockResolvedValueOnce({ orgId: 'org-nuovo' }); createOrganizationGroup.mockResolvedValue({ groupId: 'irrelevant' }); addMemberToOrganization.mockResolvedValueOnce(undefined); gruppoScoutCreate.mockResolvedValueOnce({}); assignUserToGroup.mockRejectedValueOnce(new Error('Keycloak non raggiungibile')); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'approvata' }); expect(response.status).toBe(502); expect(richiestaCreazioneGruppoUpdate).not.toHaveBeenCalled(); expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('STEP 5'), expect.anything()); expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('richiesta-1'), expect.anything()); consoleErrorSpy.mockRestore(); }); test('risponde 502 e NON marca la richiesta come approvata se la creazione del gruppo fallisce', async () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined); richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(richiestaFixture()); createOrganization.mockRejectedValueOnce(new Error('Keycloak non raggiungibile')); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'approvata' }); expect(response.status).toBe(502); expect(richiestaCreazioneGruppoUpdate).not.toHaveBeenCalled(); expect(assignUserToGroup).not.toHaveBeenCalled(); consoleErrorSpy.mockRestore(); }); test('rifiuta la richiesta senza creare alcun gruppo e aggiorna stato=RIFIUTATA', async () => { richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(richiestaFixture()); richiestaCreazioneGruppoUpdate.mockResolvedValueOnce(richiestaFixture({ stato: 'RIFIUTATA' })); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'rifiutata' }); expect(response.status).toBe(200); expect(response.body).toEqual({ id: 'richiesta-1', stato: 'RIFIUTATA' }); expect(createOrganization).not.toHaveBeenCalled(); expect(richiestaCreazioneGruppoUpdate).toHaveBeenCalledWith({ where: { id: 'richiesta-1' }, data: { stato: 'RIFIUTATA' }, }); }); test('risponde 409 se la richiesta non è più PENDING', async () => { richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(richiestaFixture({ stato: 'APPROVATA' })); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'rifiutata' }); expect(response.status).toBe(409); expect(richiestaCreazioneGruppoUpdate).not.toHaveBeenCalled(); }); test('risponde 404 se la richiesta non esiste', async () => { richiestaCreazioneGruppoFindUnique.mockResolvedValueOnce(null); const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-inesistente') .set('Authorization', `Bearer ${adminCentraleToken()}`) .send({ esito: 'rifiutata' }); expect(response.status).toBe(404); }); test('risponde 403 se il ruolo non è admin', async () => { const response = await request(app) .put('/richieste-creazione-gruppo/richiesta-1') .set('Authorization', `Bearer ${capoGruppoToken('org-1')}`) .send({ esito: 'approvata' }); expect(response.status).toBe(403); expect(richiestaCreazioneGruppoFindUnique).not.toHaveBeenCalled(); }); });