263 lines
9.8 KiB
TypeScript
263 lines
9.8 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();
|
|
const gruppoScoutFindMany = 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),
|
|
findMany: (...args: unknown[]) => gruppoScoutFindMany(...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']);
|
|
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', 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();
|
|
});
|
|
});
|
|
|
|
describe('GET /gruppi', () => {
|
|
test('restituisce 200 con la lista dei gruppi scout per admin', async () => {
|
|
gruppoScoutFindMany.mockResolvedValueOnce([
|
|
{ orgId: 'org-1', nome: 'Gruppo Alfa', regione: 'Lombardia' },
|
|
{ orgId: 'org-2', nome: 'Gruppo Beta', regione: null },
|
|
]);
|
|
|
|
const response = await request(app).get('/gruppi').set('Authorization', `Bearer ${adminToken()}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([
|
|
{ orgId: 'org-1', nome: 'Gruppo Alfa', regione: 'Lombardia' },
|
|
{ orgId: 'org-2', nome: 'Gruppo Beta', regione: null },
|
|
]);
|
|
expect(gruppoScoutFindMany).toHaveBeenCalledWith({
|
|
select: { orgId: true, nome: true, regione: true },
|
|
orderBy: { nome: 'asc' },
|
|
});
|
|
});
|
|
|
|
test('risponde 403 se il ruolo non è admin', async () => {
|
|
const response = await request(app).get('/gruppi').set('Authorization', `Bearer ${nonAdminToken()}`);
|
|
|
|
expect(response.status).toBe(403);
|
|
expect(gruppoScoutFindMany).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('risponde 401 senza token', async () => {
|
|
const response = await request(app).get('/gruppi');
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(gruppoScoutFindMany).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('GET /gruppi/elenco-pubblico', () => {
|
|
test('restituisce 200 con solo orgId/nome, accessibile a un utente non admin', async () => {
|
|
gruppoScoutFindMany.mockResolvedValueOnce([
|
|
{ orgId: 'org-1', nome: 'Gruppo Alfa' },
|
|
{ orgId: 'org-2', nome: 'Gruppo Beta' },
|
|
]);
|
|
|
|
const response = await request(app)
|
|
.get('/gruppi/elenco-pubblico')
|
|
.set('Authorization', `Bearer ${nonAdminToken()}`);
|
|
|
|
expect(response.status).toBe(200);
|
|
expect(response.body).toEqual([
|
|
{ orgId: 'org-1', nome: 'Gruppo Alfa' },
|
|
{ orgId: 'org-2', nome: 'Gruppo Beta' },
|
|
]);
|
|
expect(gruppoScoutFindMany).toHaveBeenCalledWith({
|
|
select: { orgId: true, nome: true },
|
|
orderBy: { nome: 'asc' },
|
|
});
|
|
});
|
|
|
|
test('risponde 401 senza token', async () => {
|
|
const response = await request(app).get('/gruppi/elenco-pubblico');
|
|
|
|
expect(response.status).toBe(401);
|
|
expect(gruppoScoutFindMany).not.toHaveBeenCalled();
|
|
});
|
|
});
|