Files
scouthub/scouthub-home-be/tests/integration/gruppi.endpoint.test.ts
T
2026-07-23 19:36:55 +02:00

196 lines
7.5 KiB
TypeScript

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 gruppoScoutCreate = jest.fn();
jest.mock('../../src/keycloak-admin', () => ({
createOrganization: (...args: unknown[]) => createOrganization(...args),
createOrganizationGroup: (...args: unknown[]) => createOrganizationGroup(...args),
addMemberToOrganization: (...args: unknown[]) => addMemberToOrganization(...args),
}));
jest.mock('../../src/db/prisma', () => ({
prisma: {
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<string, unknown>;
const privateKeyPem = privateKey.export({ type: 'pkcs1', format: 'pem' }) as string;
function signToken(roles: string[]): string {
return jwt.sign(
{ sub: 'user-1', realm_access: { roles } },
privateKeyPem,
{ algorithm: 'RS256', keyid: KID, expiresIn: '5m' },
);
}
const adminToken = () => signToken(['admin-centrale']);
const nonAdminToken = () => signToken(['capo-gruppo']);
function conflictError(): Error {
return Object.assign(new Error('Conflict'), { isAxiosError: true, response: { status: 409 } });
}
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 /gruppi', () => {
test('crea organizzazione, gruppi ruolo, membership e riga locale, rispondendo 201', async () => {
createOrganization.mockResolvedValueOnce({ orgId: 'org-1' });
createOrganizationGroup.mockResolvedValue({ groupId: 'irrelevant' });
addMemberToOrganization.mockResolvedValueOnce(undefined);
gruppoScoutCreate.mockResolvedValueOnce({});
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ nome: 'Gruppo Alfa', regione: 'Lombardia', ruoliDefault: ['Capi', 'Rover'] });
expect(response.status).toBe(201);
expect(response.body).toEqual({ orgId: 'org-1', gruppiCreati: ['Capi', 'Rover'] });
expect(createOrganization).toHaveBeenCalledWith('Gruppo Alfa');
expect(createOrganizationGroup).toHaveBeenCalledTimes(2);
expect(createOrganizationGroup).toHaveBeenNthCalledWith(1, 'org-1', 'Capi');
expect(createOrganizationGroup).toHaveBeenNthCalledWith(2, 'org-1', 'Rover');
expect(addMemberToOrganization).toHaveBeenCalledWith('org-1', 'user-1');
expect(gruppoScoutCreate).toHaveBeenCalledWith({
data: { orgId: 'org-1', nome: 'Gruppo Alfa', regione: 'Lombardia' },
});
});
test('usa i ruoli di default quando ruoliDefault non è specificato', async () => {
createOrganization.mockResolvedValueOnce({ orgId: 'org-2' });
createOrganizationGroup.mockResolvedValue({ groupId: 'irrelevant' });
addMemberToOrganization.mockResolvedValueOnce(undefined);
gruppoScoutCreate.mockResolvedValueOnce({});
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ nome: 'Gruppo Beta' });
expect(response.status).toBe(201);
expect(response.body.gruppiCreati).toEqual(['Capi', 'Aiuto capi', 'Censiti']);
expect(createOrganizationGroup).toHaveBeenCalledTimes(3);
expect(addMemberToOrganization).toHaveBeenCalledWith('org-2', 'user-1');
});
test('risponde 409 se Keycloak segnala un nome duplicato, senza creare la riga locale', async () => {
createOrganization.mockRejectedValueOnce(conflictError());
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ nome: 'Gruppo Già Esistente' });
expect(response.status).toBe(409);
expect(response.body.message).toMatch(/Gruppo Già Esistente/);
expect(createOrganizationGroup).not.toHaveBeenCalled();
expect(gruppoScoutCreate).not.toHaveBeenCalled();
});
test('se la creazione di un gruppo ruolo fallisce a metà, non salva la riga locale e logga il punto di arresto', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
createOrganization.mockResolvedValueOnce({ orgId: 'org-3' });
createOrganizationGroup
.mockResolvedValueOnce({ groupId: 'g-capi' })
.mockRejectedValueOnce(new Error('Keycloak non raggiungibile'));
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ nome: 'Gruppo Gamma', ruoliDefault: ['Capi', 'Aiuto capi'] });
expect(response.status).toBe(502);
expect(gruppoScoutCreate).not.toHaveBeenCalled();
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('STEP 2'), expect.anything());
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('org-3'), expect.anything());
consoleErrorSpy.mockRestore();
});
test("se l'aggiunta del creatore all'organizzazione fallisce, non salva la riga locale e logga il punto di arresto", async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
createOrganization.mockResolvedValueOnce({ orgId: 'org-4' });
createOrganizationGroup.mockResolvedValue({ groupId: 'irrelevant' });
addMemberToOrganization.mockRejectedValueOnce(new Error('Keycloak non raggiungibile'));
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ nome: 'Gruppo Delta due' });
expect(response.status).toBe(502);
expect(gruppoScoutCreate).not.toHaveBeenCalled();
expect(addMemberToOrganization).toHaveBeenCalledWith('org-4', 'user-1');
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('STEP 3'), expect.anything());
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('org-4'), expect.anything());
consoleErrorSpy.mockRestore();
});
test('risponde 403 se il ruolo non è admin-centrale', async () => {
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${nonAdminToken()}`)
.send({ nome: 'Gruppo Delta' });
expect(response.status).toBe(403);
expect(createOrganization).not.toHaveBeenCalled();
});
test('risponde 401 senza token', async () => {
const response = await request(app).post('/gruppi').send({ nome: 'Gruppo Epsilon' });
expect(response.status).toBe(401);
expect(createOrganization).not.toHaveBeenCalled();
});
test("risponde 400 se manca il campo 'nome'", async () => {
const response = await request(app)
.post('/gruppi')
.set('Authorization', `Bearer ${adminToken()}`)
.send({ regione: 'Piemonte' });
expect(response.status).toBe(400);
expect(createOrganization).not.toHaveBeenCalled();
});
});