import { prisma } from '../../src/db/prisma'; import { resetDatabase } from '../setup/resetDatabase'; import { changeStato, getListaHome, getListaSearch, getListMy, getOne, save, } from '../../src/modules/attivita/attivita.service'; import { AuthContext } from '../../src/middlewares/auth.types'; import { AttivitaSaveInput } from '../../src/types/validation'; // Questi test girano contro un DB Postgres di test reale (vedi tests/setup), // non contro dei mock: il dominio ha query relazionali/JSON troppo specifiche // per essere simulate in modo affidabile. const AUTH: AuthContext = { userId: 'user-test', email: 'test@example.com', name: 'Autore Test', roles: [] }; const MARIO: AuthContext = { userId: 'user-mario', email: 'mario@example.com', name: 'Mario', roles: [] }; const LUIGI: AuthContext = { userId: 'user-luigi', email: 'luigi@example.com', name: 'Luigi', roles: [] }; function baseAttivita(overrides: Partial = {}): AttivitaSaveInput { return { nome: 'Attività di test', stato: { id: 'BO', nome: 'Bozza' }, brancaList: [], categoriaList: [], materialeList: [], periodoAnnoList: [], paragrafoList: [], ...overrides, }; } beforeEach(async () => { await resetDatabase(); }); afterEach(async () => { await resetDatabase(); }); describe('attivita.service', () => { test('save collega una branca esistente per nome senza duplicarla', async () => { await save(baseAttivita({ brancaList: [{ nome: 'E/G' }] }), AUTH); const brancheEG = await prisma.branca.findMany({ where: { nome: 'E/G' } }); expect(brancheEG).toHaveLength(1); const attivita = await prisma.attivita.findFirstOrThrow(); const link = await prisma.brancaAttivita.findUnique({ where: { attivitaId_brancaId: { attivitaId: attivita.id, brancaId: brancheEG[0].id } }, }); expect(link).not.toBeNull(); expect(link?.cancellato).toBe(false); const dto = await getOne(attivita.id); expect(dto?.brancaList.map((b) => b.nome)).toEqual(['E/G']); }); test('save crea branca/categoria/materiale/periodoAnno nuovi quando non esistono per nome', async () => { await save( baseAttivita({ materialeList: [{ nome: 'Corda 10m', proprieta: { lunghezza: 10 } }], }), AUTH, ); const materiale = await prisma.materiale.findFirst({ where: { nome: 'Corda 10m' } }); expect(materiale).not.toBeNull(); const attivita = await prisma.attivita.findFirstOrThrow(); const link = await prisma.materialeAttivita.findUnique({ where: { attivitaId_materialeId: { attivitaId: attivita.id, materialeId: materiale!.id } }, }); expect(link).not.toBeNull(); expect(link?.cancellato).toBe(false); expect(link?.proprieta).toEqual({ lunghezza: 10 }); }); test('save in aggiornamento marca come cancellato (soft-delete) il collegamento non più presente', async () => { const lc = await prisma.branca.findFirstOrThrow({ where: { nome: 'L/C' } }); const eg = await prisma.branca.findFirstOrThrow({ where: { nome: 'E/G' } }); await save(baseAttivita({ brancaList: [{ nome: 'L/C' }, { nome: 'E/G' }] }), AUTH); const created = await prisma.attivita.findFirstOrThrow(); await save(baseAttivita({ id: created.id, brancaList: [{ nome: 'E/G' }] }), AUTH); const linkLc = await prisma.brancaAttivita.findUniqueOrThrow({ where: { attivitaId_brancaId: { attivitaId: created.id, brancaId: lc.id } }, }); const linkEg = await prisma.brancaAttivita.findUniqueOrThrow({ where: { attivitaId_brancaId: { attivitaId: created.id, brancaId: eg.id } }, }); expect(linkLc.cancellato).toBe(true); expect(linkEg.cancellato).toBe(false); }); test('save sostituisce correttamente i paragrafi (modifica, rimozione, aggiunta)', async () => { await save( baseAttivita({ paragrafoList: [ { corpo: 'Paragrafo uno', tipo: { id: 'PARAGRAFO' }, ordine: 1 }, { corpo: 'Paragrafo due', tipo: { id: 'PARAGRAFO' }, ordine: 2 }, ], }), AUTH, ); const created = await prisma.attivita.findFirstOrThrow(); const paragrafi = await prisma.paragrafo.findMany({ where: { attivitaId: created.id }, orderBy: { ordine: 'asc' }, }); expect(paragrafi).toHaveLength(2); const [primo, secondo] = paragrafi; await save( baseAttivita({ id: created.id, paragrafoList: [ { id: primo.id, corpo: 'Paragrafo uno modificato', tipo: { id: 'PARAGRAFO' }, ordine: 1, }, { corpo: 'Paragrafo nuovo', tipo: { id: 'PARAGRAFO' }, ordine: 2 }, ], }), AUTH, ); const paragrafiFinali = await prisma.paragrafo.findMany({ where: { attivitaId: created.id }, orderBy: { ordine: 'asc' }, }); expect(paragrafiFinali).toHaveLength(2); expect(paragrafiFinali.map((p) => p.corpo)).toEqual(['Paragrafo uno modificato', 'Paragrafo nuovo']); expect(paragrafiFinali.find((p) => p.id === secondo.id)).toBeUndefined(); }); test('save su id inesistente lancia HttpError 404', async () => { await expect(save(baseAttivita({ id: 999999 }), AUTH)).rejects.toMatchObject({ statusCode: 404 }); }); test("save su un'attività di un altro autore lancia HttpError 403", async () => { await save(baseAttivita(), MARIO); const created = await prisma.attivita.findFirstOrThrow(); await expect(save(baseAttivita({ id: created.id }), LUIGI)).rejects.toMatchObject({ statusCode: 403 }); }); test('getListaHome restituisce solo attività pubblicate', async () => { await save(baseAttivita({ nome: 'Bozza', stato: { id: 'BO', nome: 'Bozza' } }), AUTH); await save(baseAttivita({ nome: 'Pubblicata', stato: { id: 'PU', nome: 'Pubblicato' } }), AUTH); await save(baseAttivita({ nome: 'Privata', stato: { id: 'PR', nome: 'Privato' } }), AUTH); const home = await getListaHome(); expect(home).toHaveLength(1); expect(home[0].nome).toBe('Pubblicata'); }); test('getListMy filtra per autore', async () => { await save(baseAttivita({ nome: 'Di Mario' }), MARIO); await save(baseAttivita({ nome: 'Di Luigi' }), LUIGI); const mie = await getListMy(MARIO.userId); expect(mie).toHaveLength(1); expect(mie[0].nome).toBe('Di Mario'); expect(mie[0].autore).toBe('Mario'); }); test('changeStato su id inesistente lancia HttpError 404', async () => { await expect(changeStato(999999, 'PU', AUTH)).rejects.toMatchObject({ statusCode: 404 }); }); test("changeStato su un'attività di un altro autore lancia HttpError 403", async () => { await save(baseAttivita(), MARIO); const created = await prisma.attivita.findFirstOrThrow(); await expect(changeStato(created.id, 'PU', LUIGI)).rejects.toMatchObject({ statusCode: 403 }); }); test('changeStato aggiorna lo stato e lo restituisce come TipologicaDto', async () => { await save(baseAttivita(), AUTH); const created = await prisma.attivita.findFirstOrThrow(); const stato = await changeStato(created.id, 'PU', AUTH); expect(stato).toEqual({ id: 'PU', nome: 'Pubblicato' }); const aggiornata = await prisma.attivita.findUniqueOrThrow({ where: { id: created.id } }); expect(aggiornata.statoId).toBe('PU'); }); test('getListaSearch combina filtri di gruppi diversi in AND e valori dello stesso gruppo in OR', async () => { const lc = await prisma.branca.findFirstOrThrow({ where: { nome: 'L/C' } }); const gioco = await prisma.categoria.findFirstOrThrow({ where: { nome: 'gioco' } }); await save( baseAttivita({ nome: 'L/C gioco', brancaList: [{ nome: 'L/C' }], categoriaList: [{ nome: 'gioco' }], }), AUTH, ); await save( baseAttivita({ nome: 'E/G gioco', brancaList: [{ nome: 'E/G' }], categoriaList: [{ nome: 'gioco' }], }), AUTH, ); await save( baseAttivita({ nome: 'L/C danza', brancaList: [{ nome: 'L/C' }], categoriaList: [{ nome: 'danza' }], }), AUTH, ); const risultati = await getListaSearch([ { gruppo: 'branca', id: lc.id, nome: null }, { gruppo: 'categoria', id: gioco.id, nome: null }, ]); expect(risultati).toHaveLength(1); expect(risultati[0].nome).toBe('L/C gioco'); }); test('getListaSearch con filtro testo trova sia nel nome sia nei paragrafi', async () => { await save(baseAttivita({ nome: 'Caccia al tesoro' }), AUTH); await save( baseAttivita({ nome: 'Attività generica', paragrafoList: [ { corpo: "C'è un tesoro nascosto nel bosco", tipo: { id: 'PARAGRAFO' }, ordine: 1, }, ], }), AUTH, ); const risultati = await getListaSearch([{ gruppo: 'testo', nome: 'tesoro', id: null }]); expect(risultati.map((r) => r.nome).sort()).toEqual(['Attività generica', 'Caccia al tesoro'].sort()); }); });