Fix scouthub-home-be

This commit is contained in:
Lorenzo Sanesi
2026-07-25 12:09:12 +02:00
parent ac92ca43ce
commit 5b7c82d359
39 changed files with 2116 additions and 81 deletions
@@ -18,6 +18,8 @@ const getUserGroupsInOrganization = jest.fn();
const assignRealmRoleToUser = jest.fn();
const removeRealmRoleFromUser = jest.fn();
const getUserRealmRoles = jest.fn();
const addMemberToOrganization = jest.fn();
const findUserByEmail = jest.fn();
jest.mock('../../src/keycloak-admin', () => ({
listOrganizationMembers: (...args: unknown[]) => listOrganizationMembers(...args),
@@ -28,6 +30,8 @@ jest.mock('../../src/keycloak-admin', () => ({
assignRealmRoleToUser: (...args: unknown[]) => assignRealmRoleToUser(...args),
removeRealmRoleFromUser: (...args: unknown[]) => removeRealmRoleFromUser(...args),
getUserRealmRoles: (...args: unknown[]) => getUserRealmRoles(...args),
addMemberToOrganization: (...args: unknown[]) => addMemberToOrganization(...args),
findUserByEmail: (...args: unknown[]) => findUserByEmail(...args),
}));
import { app } from '../../src/app';
@@ -62,6 +66,14 @@ function nonCapoToken(orgId: string): string {
});
}
function adminCentraleToken(): string {
return signToken({
sub: 'user-admin',
email: 'admin@example.com',
realm_access: { roles: ['admin'] },
});
}
beforeAll(() => {
nock(KEYCLOAK_HOST).persist().get(CERTS_PATH).reply(200, {
keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }],
@@ -76,6 +88,68 @@ beforeEach(() => {
jest.clearAllMocks();
});
describe('POST /gruppi/:orgId/membri', () => {
test('aggiunge un utente già registrato con successo e risponde 201', async () => {
findUserByEmail.mockResolvedValueOnce({ id: 'user-1' });
addMemberToOrganization.mockResolvedValueOnce(undefined);
assignUserToGroup.mockResolvedValueOnce(undefined);
assignRealmRoleToUser.mockResolvedValueOnce(undefined);
const response = await request(app)
.post('/gruppi/org-1/membri')
.set('Authorization', `Bearer ${adminCentraleToken()}`)
.send({ email: 'gia-registrato@example.com', ruolo: 'Capi' });
expect(response.status).toBe(201);
expect(response.body).toEqual({ userId: 'user-1', email: 'gia-registrato@example.com', ruolo: 'Capi' });
expect(findUserByEmail).toHaveBeenCalledWith('gia-registrato@example.com');
expect(addMemberToOrganization).toHaveBeenCalledWith('org-1', 'user-1');
expect(assignUserToGroup).toHaveBeenCalledWith('user-1', 'Capi');
expect(assignRealmRoleToUser).toHaveBeenCalledWith('user-1', 'Capi');
});
test("risponde 404 se non esiste un utente registrato con quell'email", async () => {
findUserByEmail.mockResolvedValueOnce(null);
const response = await request(app)
.post('/gruppi/org-1/membri')
.set('Authorization', `Bearer ${adminCentraleToken()}`)
.send({ email: 'sconosciuto@example.com', ruolo: 'Capi' });
expect(response.status).toBe(404);
expect(addMemberToOrganization).not.toHaveBeenCalled();
});
test('risponde 502 se una chiamata Keycloak fallisce a metà sequenza', async () => {
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
findUserByEmail.mockResolvedValueOnce({ id: 'user-1' });
addMemberToOrganization.mockResolvedValueOnce(undefined);
assignUserToGroup.mockRejectedValueOnce(new Error('Keycloak non raggiungibile'));
const response = await request(app)
.post('/gruppi/org-1/membri')
.set('Authorization', `Bearer ${adminCentraleToken()}`)
.send({ email: 'gia-registrato@example.com', ruolo: 'Capi' });
expect(response.status).toBe(502);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('STEP 2'), expect.anything());
expect(assignRealmRoleToUser).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore();
});
test('risponde 403 se chiamato da capo-gruppo, anche sulla propria organizzazione', async () => {
const response = await request(app)
.post('/gruppi/org-1/membri')
.set('Authorization', `Bearer ${capoGruppoToken('org-1')}`)
.send({ email: 'gia-registrato@example.com', ruolo: 'Capi' });
expect(response.status).toBe(403);
expect(findUserByEmail).not.toHaveBeenCalled();
});
});
describe('GET /gruppi/:orgId/membri', () => {
test('restituisce la lista membri con ruolo e gruppo interno', async () => {
listOrganizationMembers.mockResolvedValueOnce([
@@ -116,6 +190,19 @@ describe('GET /gruppi/:orgId/membri', () => {
expect(response.status).toBe(403);
expect(listOrganizationMembers).not.toHaveBeenCalled();
});
test("admin accede con successo anche a un'organizzazione che non è la propria", async () => {
listOrganizationMembers.mockResolvedValueOnce([{ userId: 'user-1', email: 'uno@example.com' }]);
getUserGroupsInOrganization.mockResolvedValueOnce([]);
getUserRealmRoles.mockResolvedValueOnce([]);
const response = await request(app)
.get('/gruppi/org-altrui/membri')
.set('Authorization', `Bearer ${adminCentraleToken()}`);
expect(response.status).toBe(200);
expect(listOrganizationMembers).toHaveBeenCalledWith('org-altrui');
});
});
describe('PUT /gruppi/:orgId/membri/:userId/ruolo', () => {
@@ -173,6 +260,19 @@ describe('PUT /gruppi/:orgId/membri/:userId/ruolo', () => {
consoleErrorSpy.mockRestore();
});
test("admin può cambiare ruolo anche in un'organizzazione che non è la propria", async () => {
getUserGroupsInOrganization.mockResolvedValueOnce([]);
getUserRealmRoles.mockResolvedValueOnce([]);
const response = await request(app)
.put('/gruppi/org-altrui/membri/user-1/ruolo')
.set('Authorization', `Bearer ${adminCentraleToken()}`)
.send({ ruolo: 'Aiuto capi' });
expect(response.status).toBe(200);
expect(assignUserToGroup).toHaveBeenCalledWith('user-1', 'Aiuto capi');
});
});
describe('DELETE /gruppi/:orgId/membri/:userId', () => {
@@ -209,4 +309,15 @@ describe('DELETE /gruppi/:orgId/membri/:userId', () => {
consoleErrorSpy.mockRestore();
});
test("admin può rimuovere un membro anche in un'organizzazione che non è la propria", async () => {
removeMemberFromOrganization.mockResolvedValueOnce(undefined);
const response = await request(app)
.delete('/gruppi/org-altrui/membri/user-1')
.set('Authorization', `Bearer ${adminCentraleToken()}`);
expect(response.status).toBe(204);
expect(removeMemberFromOrganization).toHaveBeenCalledWith('org-altrui', 'user-1');
});
});