Fix scouthub-home-be
This commit is contained in:
@@ -0,0 +1,330 @@
|
||||
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<string, unknown>;
|
||||
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<Record<string, unknown>> = {}) {
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user