Add scouthub-eventi-be
This commit is contained in:
@@ -0,0 +1,471 @@
|
||||
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_EVENTI_CLIENT_ID = 'test-client';
|
||||
process.env.KEYCLOAK_EVENTI_CLIENT_SECRET = 'test-secret';
|
||||
process.env.KEYCLOAK_AUTHORIZED_SERVICE_CLIENTS = 'test-service-client';
|
||||
process.env.DATABASE_URL = 'postgresql://user:pass@localhost:5432/scouthub_eventi_test';
|
||||
|
||||
const eventoFindFirst = jest.fn();
|
||||
const eventoFindMany = jest.fn();
|
||||
const eventoFindUnique = jest.fn();
|
||||
const eventoCreate = jest.fn();
|
||||
const eventoUpdate = jest.fn();
|
||||
const eventoDelete = jest.fn();
|
||||
const risorsaCollegataCreate = jest.fn();
|
||||
|
||||
jest.mock('../../src/db/prisma', () => ({
|
||||
prisma: {
|
||||
evento: {
|
||||
findFirst: (...args: unknown[]) => eventoFindFirst(...args),
|
||||
findMany: (...args: unknown[]) => eventoFindMany(...args),
|
||||
findUnique: (...args: unknown[]) => eventoFindUnique(...args),
|
||||
create: (...args: unknown[]) => eventoCreate(...args),
|
||||
update: (...args: unknown[]) => eventoUpdate(...args),
|
||||
delete: (...args: unknown[]) => eventoDelete(...args),
|
||||
},
|
||||
risorsaCollegata: {
|
||||
create: (...args: unknown[]) => risorsaCollegataCreate(...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 tokenFor(orgId: string, branche: string[], roles: string[] = []): string {
|
||||
return signToken({
|
||||
sub: 'user-1',
|
||||
realm_access: { roles },
|
||||
organization: { gruppo: { id: orgId, roles: [] } },
|
||||
groups: branche.map((b) => `/${b}`),
|
||||
});
|
||||
}
|
||||
|
||||
// Token client-credentials (client di servizio): niente organization/groups, solo
|
||||
// "azp" (client_id del chiamante), come emesso da Keycloak per un service account.
|
||||
function serviceTokenFor(azp: string): string {
|
||||
return signToken({ sub: `service-account-${azp}`, azp });
|
||||
}
|
||||
|
||||
function evento(overrides: Partial<Record<string, unknown>> = {}) {
|
||||
return {
|
||||
id: 'ev-1',
|
||||
orgId: 'org-a',
|
||||
brancaId: 'Lupetti',
|
||||
parentId: null,
|
||||
titolo: 'Uscita',
|
||||
descrizione: null,
|
||||
dataInizio: new Date('2026-08-01'),
|
||||
dataFine: new Date('2026-08-10'),
|
||||
tipo: 'campo',
|
||||
location: null,
|
||||
creatoDa: 'user-1',
|
||||
creatoIl: new Date('2026-01-01'),
|
||||
children: [] as unknown[],
|
||||
risorseCollegate: [] as unknown[],
|
||||
...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 /eventi — profondità massima (max 2 livelli)', () => {
|
||||
test('rifiuta la creazione di un nipote (3° livello) con 400', async () => {
|
||||
// Il "parent" indicato in request ha a sua volta un parentId: è già un
|
||||
// figlio di 2° livello, quindi crearvi sotto un evento sarebbe un 3° livello.
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ id: 'ev-figlio', parentId: 'ev-nonno' }));
|
||||
|
||||
const response = await request(app)
|
||||
.post('/eventi')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({
|
||||
titolo: 'Nipote',
|
||||
dataInizio: '2026-08-02',
|
||||
dataFine: '2026-08-03',
|
||||
tipo: 'uscita',
|
||||
parentId: 'ev-figlio',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(response.body.message).toMatch(/profondità massima superata/);
|
||||
expect(eventoCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /eventi — contenimento date nel parent', () => {
|
||||
test('rifiuta un figlio con date fuori dall\'intervallo del parent', async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(
|
||||
evento({ id: 'ev-parent', parentId: null, dataInizio: new Date('2026-08-01'), dataFine: new Date('2026-08-10') }),
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/eventi')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({
|
||||
titolo: 'Figlio fuori data',
|
||||
dataInizio: '2026-07-30',
|
||||
dataFine: '2026-08-05',
|
||||
tipo: 'uscita',
|
||||
parentId: 'ev-parent',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('accetta un figlio con date contenute nell\'intervallo del parent', async () => {
|
||||
const parent = evento({ id: 'ev-parent', parentId: null, dataInizio: new Date('2026-08-01'), dataFine: new Date('2026-08-10') });
|
||||
eventoFindFirst.mockResolvedValueOnce(parent);
|
||||
eventoCreate.mockResolvedValueOnce(
|
||||
evento({ id: 'ev-figlio', parentId: 'ev-parent', dataInizio: new Date('2026-08-02'), dataFine: new Date('2026-08-03') }),
|
||||
);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/eventi')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({
|
||||
titolo: 'Figlio ok',
|
||||
dataInizio: '2026-08-02',
|
||||
dataFine: '2026-08-03',
|
||||
tipo: 'uscita',
|
||||
parentId: 'ev-parent',
|
||||
});
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(eventoCreate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
data: expect.objectContaining({ orgId: 'org-a', brancaId: 'Lupetti', parentId: 'ev-parent' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /eventi/:id — isolamento branca', () => {
|
||||
test("un capo-unità di un'altra branca non può modificare l'evento (403)", async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ brancaId: 'Esploratori' }));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'], ['capo-unita'])}`)
|
||||
.send({ titolo: 'Nuovo titolo' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(eventoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('capo-gruppo può modificare un evento di qualunque branca della propria org', async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ brancaId: 'Esploratori' }));
|
||||
eventoUpdate.mockResolvedValueOnce(evento({ brancaId: 'Esploratori', titolo: 'Nuovo titolo' }));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', [], ['capo-gruppo'])}`)
|
||||
.send({ titolo: 'Nuovo titolo' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(eventoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('un membro della stessa branca può modificare l\'evento', async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ brancaId: 'Lupetti' }));
|
||||
eventoUpdate.mockResolvedValueOnce(evento({ brancaId: 'Lupetti', titolo: 'Nuovo titolo' }));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'], ['capo-unita'])}`)
|
||||
.send({ titolo: 'Nuovo titolo' });
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(eventoUpdate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PUT /eventi/:id — contenimento date su modifica', () => {
|
||||
test('rifiuta la modifica delle date del parent se non contengono più un figlio esistente', async () => {
|
||||
const figlio = evento({ id: 'ev-figlio', parentId: 'ev-1', dataInizio: new Date('2026-08-05'), dataFine: new Date('2026-08-06') });
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ id: 'ev-1', children: [figlio] }));
|
||||
|
||||
const response = await request(app)
|
||||
.put('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({ dataInizio: '2026-08-01', dataFine: '2026-08-05' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rifiuta la modifica delle date di un figlio se escono dall\'intervallo del parent', async () => {
|
||||
const figlio = evento({ id: 'ev-figlio', parentId: 'ev-parent', dataInizio: new Date('2026-08-02'), dataFine: new Date('2026-08-03') });
|
||||
eventoFindFirst
|
||||
.mockResolvedValueOnce(figlio) // fetch dell'evento da modificare
|
||||
.mockResolvedValueOnce(evento({ id: 'ev-parent', dataInizio: new Date('2026-08-01'), dataFine: new Date('2026-08-10') })); // fetch del parent
|
||||
|
||||
const response = await request(app)
|
||||
.put('/eventi/ev-figlio')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({ dataFine: '2026-08-15' });
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoUpdate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /eventi/:id', () => {
|
||||
test('elimina un evento a cui l\'utente ha accesso', async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(evento());
|
||||
eventoDelete.mockResolvedValueOnce(evento());
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(204);
|
||||
expect(eventoDelete).toHaveBeenCalledWith({ where: { id: 'ev-1' } });
|
||||
});
|
||||
|
||||
test("rifiuta l'eliminazione se l'utente non ha accesso alla branca (403)", async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ brancaId: 'Esploratori' }));
|
||||
|
||||
const response = await request(app)
|
||||
.delete('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'], ['capo-unita'])}`);
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(eventoDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('risponde 401 senza token', async () => {
|
||||
const response = await request(app).delete('/eventi/ev-1');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(eventoDelete).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /eventi/:id', () => {
|
||||
test('restituisce il dettaglio completo con figli e risorse collegate', async () => {
|
||||
const figlio = evento({ id: 'ev-figlio', parentId: 'ev-1' });
|
||||
const risorsa = {
|
||||
id: 'r-1',
|
||||
eventoId: 'ev-1',
|
||||
tipoRisorsa: 'attivita',
|
||||
risorsaId: 'att-1',
|
||||
servizioOrigine: 'scouthub-attivita-be',
|
||||
metadata: null,
|
||||
creatoIl: new Date('2026-01-02'),
|
||||
};
|
||||
eventoFindFirst.mockResolvedValueOnce(evento({ children: [figlio], risorseCollegate: [risorsa] }));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.figli).toHaveLength(1);
|
||||
expect(response.body.figli[0].id).toBe('ev-figlio');
|
||||
expect(response.body.risorseCollegate).toHaveLength(1);
|
||||
expect(response.body.risorseCollegate[0]).toMatchObject({ id: 'r-1', tipoRisorsa: 'attivita' });
|
||||
});
|
||||
|
||||
test("un'altra org non può leggere l'evento (404)", async () => {
|
||||
eventoFindFirst.mockResolvedValueOnce(null);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/eventi/ev-1')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-b', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /eventi?vista=anno', () => {
|
||||
test('aggrega per giorno solo gli eventi radice, senza descrizione/risorse', async () => {
|
||||
eventoFindMany.mockResolvedValueOnce([
|
||||
evento({ id: 'ev-1', titolo: 'Campo estivo', dataInizio: new Date('2026-08-01'), dataFine: new Date('2026-08-02') }),
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/eventi?vista=anno&anno=2026')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(eventoFindMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: expect.objectContaining({ orgId: 'org-a', parentId: null }) }),
|
||||
);
|
||||
expect(response.body).toEqual([
|
||||
{ data: '2026-08-01', conteggio: 1, eventi: [{ titolo: 'Campo estivo', tipo: 'campo', brancaId: 'Lupetti' }] },
|
||||
{ data: '2026-08-02', conteggio: 1, eventi: [{ titolo: 'Campo estivo', tipo: 'campo', brancaId: 'Lupetti' }] },
|
||||
]);
|
||||
});
|
||||
|
||||
test("risponde 400 se manca l'anno", async () => {
|
||||
const response = await request(app)
|
||||
.get('/eventi?vista=anno')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoFindMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /eventi?vista=mese — intersezione date', () => {
|
||||
test('un evento che inizia il mese prima e finisce dentro il mese richiesto compare nel risultato', async () => {
|
||||
// Evento a cavallo tra agosto e settembre: dataInizio nel mese precedente,
|
||||
// dataFine dentro il mese richiesto (settembre 2026).
|
||||
eventoFindMany.mockResolvedValueOnce([
|
||||
evento({
|
||||
id: 'ev-a-cavallo',
|
||||
titolo: 'Campo a cavallo',
|
||||
dataInizio: new Date('2026-08-28'),
|
||||
dataFine: new Date('2026-09-03'),
|
||||
}),
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/eventi?vista=mese&mese=2026-09')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(eventoFindMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
orgId: 'org-a',
|
||||
dataInizio: { lte: new Date(Date.UTC(2026, 8, 30, 23, 59, 59, 999)) },
|
||||
dataFine: { gte: new Date(Date.UTC(2026, 8, 1)) },
|
||||
},
|
||||
orderBy: { dataInizio: 'asc' },
|
||||
});
|
||||
expect(response.body).toHaveLength(1);
|
||||
expect(response.body[0].id).toBe('ev-a-cavallo');
|
||||
});
|
||||
|
||||
test('applica il filtro brancaId quando presente', async () => {
|
||||
eventoFindMany.mockResolvedValueOnce([]);
|
||||
|
||||
await request(app)
|
||||
.get('/eventi?vista=mese&mese=2026-09&brancaId=Esploratori')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(eventoFindMany).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ where: expect.objectContaining({ brancaId: 'Esploratori' }) }),
|
||||
);
|
||||
});
|
||||
|
||||
test("risponde 400 se il formato di 'mese' non è YYYY-MM", async () => {
|
||||
const response = await request(app)
|
||||
.get('/eventi?vista=mese&mese=settembre-2026')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoFindMany).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("risponde 400 se 'vista' non è valorizzato con anno o mese", async () => {
|
||||
const response = await request(app)
|
||||
.get('/eventi?vista=settimana')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect(eventoFindMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /eventi/:id/risorse — machine-to-machine', () => {
|
||||
test('crea la risorsa collegata con credenziali di servizio autorizzate', async () => {
|
||||
eventoFindUnique.mockResolvedValueOnce(evento());
|
||||
risorsaCollegataCreate.mockResolvedValueOnce({
|
||||
id: 'r-1',
|
||||
eventoId: 'ev-1',
|
||||
tipoRisorsa: 'attivita',
|
||||
risorsaId: 'att-1',
|
||||
servizioOrigine: 'scouthub-attivita-be',
|
||||
metadata: null,
|
||||
creatoIl: new Date('2026-01-01'),
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.post('/eventi/ev-1/risorse')
|
||||
.set('Authorization', `Bearer ${serviceTokenFor('test-service-client')}`)
|
||||
.send({ tipoRisorsa: 'attivita', risorsaId: 'att-1', servizioOrigine: 'scouthub-attivita-be' });
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(risorsaCollegataCreate).toHaveBeenCalledWith({
|
||||
data: expect.objectContaining({
|
||||
eventoId: 'ev-1',
|
||||
tipoRisorsa: 'attivita',
|
||||
risorsaId: 'att-1',
|
||||
servizioOrigine: 'scouthub-attivita-be',
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
test("risponde 404 se l'evento non esiste", async () => {
|
||||
eventoFindUnique.mockResolvedValueOnce(null);
|
||||
|
||||
const response = await request(app)
|
||||
.post('/eventi/ev-inesistente/risorse')
|
||||
.set('Authorization', `Bearer ${serviceTokenFor('test-service-client')}`)
|
||||
.send({ tipoRisorsa: 'attivita', risorsaId: 'att-1', servizioOrigine: 'scouthub-attivita-be' });
|
||||
|
||||
expect(response.status).toBe(404);
|
||||
expect(risorsaCollegataCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('risponde 401 senza alcun token', async () => {
|
||||
const response = await request(app)
|
||||
.post('/eventi/ev-1/risorse')
|
||||
.send({ tipoRisorsa: 'attivita', risorsaId: 'att-1', servizioOrigine: 'scouthub-attivita-be' });
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
expect(eventoFindUnique).not.toHaveBeenCalled();
|
||||
expect(risorsaCollegataCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("risponde 403 se il client (azp) non è nella whitelist KEYCLOAK_AUTHORIZED_SERVICE_CLIENTS", async () => {
|
||||
const response = await request(app)
|
||||
.post('/eventi/ev-1/risorse')
|
||||
.set('Authorization', `Bearer ${serviceTokenFor('client-non-autorizzato')}`)
|
||||
.send({ tipoRisorsa: 'attivita', risorsaId: 'att-1', servizioOrigine: 'scouthub-attivita-be' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(eventoFindUnique).not.toHaveBeenCalled();
|
||||
expect(risorsaCollegataCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('un token utente valido (senza azp di un client di servizio) viene comunque respinto', async () => {
|
||||
const response = await request(app)
|
||||
.post('/eventi/ev-1/risorse')
|
||||
.set('Authorization', `Bearer ${tokenFor('org-a', ['Lupetti'])}`)
|
||||
.send({ tipoRisorsa: 'attivita', risorsaId: 'att-1', servizioOrigine: 'scouthub-attivita-be' });
|
||||
|
||||
expect(response.status).toBe(403);
|
||||
expect(risorsaCollegataCreate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
// Test di integrazione reale (Prisma NON mockato): verifica il vincolo ON DELETE
|
||||
// CASCADE definito nella migration fra evento.parent_id -> evento(id) e fra
|
||||
// risorsa_collegata.evento_id -> evento(id). Un mock di Prisma non potrebbe
|
||||
// verificare un comportamento che vive nel database, quindi qui si usa una
|
||||
// connessione reale. Richiede un Postgres raggiungibile con lo schema di
|
||||
// scouthub-eventi-be già migrato (`npx prisma migrate deploy`), es. il database
|
||||
// locale di sviluppo scouthub_eventi.
|
||||
process.env.DATABASE_URL =
|
||||
process.env.DATABASE_URL ?? 'postgresql://postgres:postgres@localhost:5432/scouthub_eventi?schema=public';
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
afterAll(async () => {
|
||||
await prisma.$disconnect();
|
||||
});
|
||||
|
||||
describe('cascade delete evento -> figli e risorse collegate (vincolo DB)', () => {
|
||||
test('eliminare il parent elimina anche i figli e le risorse collegate', async () => {
|
||||
const parent = await prisma.evento.create({
|
||||
data: {
|
||||
orgId: 'org-cascade-test',
|
||||
brancaId: 'Lupetti',
|
||||
titolo: 'Campo estivo',
|
||||
dataInizio: new Date('2026-08-01'),
|
||||
dataFine: new Date('2026-08-10'),
|
||||
tipo: 'campo',
|
||||
creatoDa: 'user-test',
|
||||
},
|
||||
});
|
||||
|
||||
const figlio = await prisma.evento.create({
|
||||
data: {
|
||||
orgId: parent.orgId,
|
||||
brancaId: parent.brancaId,
|
||||
parentId: parent.id,
|
||||
titolo: 'Uscita di un giorno',
|
||||
dataInizio: new Date('2026-08-02'),
|
||||
dataFine: new Date('2026-08-02'),
|
||||
tipo: 'uscita',
|
||||
creatoDa: 'user-test',
|
||||
},
|
||||
});
|
||||
|
||||
const risorsa = await prisma.risorsaCollegata.create({
|
||||
data: {
|
||||
eventoId: parent.id,
|
||||
tipoRisorsa: 'attivita',
|
||||
risorsaId: 'att-1',
|
||||
servizioOrigine: 'scouthub-attivita-be',
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.evento.delete({ where: { id: parent.id } });
|
||||
|
||||
const figlioRimasto = await prisma.evento.findUnique({ where: { id: figlio.id } });
|
||||
const risorsaRimasta = await prisma.risorsaCollegata.findUnique({ where: { id: risorsa.id } });
|
||||
|
||||
expect(figlioRimasto).toBeNull();
|
||||
expect(risorsaRimasta).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user