Add scouthub-attivit-be
This commit is contained in:
@@ -0,0 +1,192 @@
|
||||
import request from 'supertest';
|
||||
import nock from 'nock';
|
||||
import { app } from '../../src/app';
|
||||
import { prisma } from '../../src/db/prisma';
|
||||
import { resetDatabase } from '../setup/resetDatabase';
|
||||
import { bearer, mockKeycloakJwks } from '../setup/authTestHelper';
|
||||
|
||||
const AUTORE = { sub: 'user-movio', email: 'movio@example.com', name: 'Movio' };
|
||||
const ALTRO_AUTORE = { sub: 'user-altro', email: 'altro@example.com', name: 'Altro Utente' };
|
||||
const AUTH_HEADER = bearer(AUTORE);
|
||||
const ALTRO_AUTH_HEADER = bearer(ALTRO_AUTORE);
|
||||
|
||||
function attivitaPayload(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
nome: 'Caccia al tesoro',
|
||||
stato: { id: 'BO', nome: 'Bozza' },
|
||||
brancaList: [{ nome: 'E/G' }],
|
||||
categoriaList: [{ nome: 'grande gioco' }],
|
||||
materialeList: [],
|
||||
periodoAnnoList: [],
|
||||
paragrafoList: [{ corpo: 'Introduzione al gioco', tipo: { id: 'PARAGRAFO' }, ordine: 1 }],
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
mockKeycloakJwks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
describe('attivita endpoints', () => {
|
||||
test('GET /health risponde 200 con { status: "ok" }', async () => {
|
||||
const res = await request(app).get('/health');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual({ status: 'ok' });
|
||||
});
|
||||
|
||||
test('GET /public/attivita/get/lista/home su DB vuoto restituisce []', async () => {
|
||||
const res = await request(app).get('/public/attivita/get/lista/home');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
test('GET /public/attivita/get/one/:id con id non numerico restituisce 400', async () => {
|
||||
const res = await request(app).get('/public/attivita/get/one/abc');
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
|
||||
test('GET /public/attivita/get/one/:id con id inesistente restituisce 404', async () => {
|
||||
const res = await request(app).get('/public/attivita/get/one/999999');
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test('endpoint privati senza token restituiscono 401', async () => {
|
||||
const saveRes = await request(app).post('/private/attivita/save').send(attivitaPayload());
|
||||
expect(saveRes.status).toBe(401);
|
||||
|
||||
const myRes = await request(app).get('/private/attivita/get/lista/my');
|
||||
expect(myRes.status).toBe(401);
|
||||
|
||||
const changeRes = await request(app).get('/private/attivita/change/stato/1/PU');
|
||||
expect(changeRes.status).toBe(401);
|
||||
});
|
||||
|
||||
test('POST /private/attivita/save senza nome restituisce 400 con informazioni di validazione', async () => {
|
||||
const { nome, ...payloadSenzaNome } = attivitaPayload();
|
||||
|
||||
const res = await request(app)
|
||||
.post('/private/attivita/save')
|
||||
.set('Authorization', AUTH_HEADER)
|
||||
.send(payloadSenzaNome);
|
||||
|
||||
expect(res.status).toBe(400);
|
||||
expect(typeof res.body.message).toBe('string');
|
||||
expect(res.body.message.length).toBeGreaterThan(0);
|
||||
expect(res.body.message).toContain('nome');
|
||||
});
|
||||
|
||||
test("POST /private/attivita/save con payload valido crea l'attività, visibile poi via get/lista/my", async () => {
|
||||
const saveRes = await request(app)
|
||||
.post('/private/attivita/save')
|
||||
.set('Authorization', AUTH_HEADER)
|
||||
.send(attivitaPayload());
|
||||
expect([200, 204]).toContain(saveRes.status);
|
||||
|
||||
const myRes = await request(app)
|
||||
.get('/private/attivita/get/lista/my')
|
||||
.set('Authorization', AUTH_HEADER);
|
||||
expect(myRes.status).toBe(200);
|
||||
expect(myRes.body).toHaveLength(1);
|
||||
expect(myRes.body[0]).toMatchObject({ nome: 'Caccia al tesoro', autore: 'Movio' });
|
||||
expect(myRes.body[0].brancaList.map((b: { nome: string }) => b.nome)).toEqual(['E/G']);
|
||||
expect(myRes.body[0].categoriaList.map((c: { nome: string }) => c.nome)).toEqual([
|
||||
'grande gioco',
|
||||
]);
|
||||
});
|
||||
|
||||
test('get/lista/my non restituisce le attività di un altro autore', async () => {
|
||||
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||
|
||||
const myRes = await request(app)
|
||||
.get('/private/attivita/get/lista/my')
|
||||
.set('Authorization', ALTRO_AUTH_HEADER);
|
||||
|
||||
expect(myRes.status).toBe(200);
|
||||
expect(myRes.body).toEqual([]);
|
||||
});
|
||||
|
||||
test("POST /private/attivita/save su un'attività di un altro autore restituisce 403", async () => {
|
||||
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||
const created = await prisma.attivita.findFirstOrThrow();
|
||||
|
||||
const res = await request(app)
|
||||
.post('/private/attivita/save')
|
||||
.set('Authorization', ALTRO_AUTH_HEADER)
|
||||
.send(attivitaPayload({ id: created.id, nome: 'Modificata da altri' }));
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
test("change/stato aggiorna lo stato e rende visibile l'attività in home", async () => {
|
||||
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||
const created = await prisma.attivita.findFirstOrThrow();
|
||||
|
||||
const changeRes = await request(app)
|
||||
.get(`/private/attivita/change/stato/${created.id}/PU`)
|
||||
.set('Authorization', AUTH_HEADER);
|
||||
expect(changeRes.status).toBe(200);
|
||||
expect(changeRes.body).toEqual({ id: 'PU', nome: 'Pubblicato' });
|
||||
|
||||
const homeRes = await request(app).get('/public/attivita/get/lista/home');
|
||||
expect(homeRes.status).toBe(200);
|
||||
expect(homeRes.body.map((a: { id: number }) => a.id)).toContain(created.id);
|
||||
});
|
||||
|
||||
test('change/stato su id inesistente restituisce 404', async () => {
|
||||
const res = await request(app)
|
||||
.get('/private/attivita/change/stato/999999/PU')
|
||||
.set('Authorization', AUTH_HEADER);
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
test("change/stato su un'attività di un altro autore restituisce 403", async () => {
|
||||
await request(app).post('/private/attivita/save').set('Authorization', AUTH_HEADER).send(attivitaPayload());
|
||||
const created = await prisma.attivita.findFirstOrThrow();
|
||||
|
||||
const res = await request(app)
|
||||
.get(`/private/attivita/change/stato/${created.id}/PU`)
|
||||
.set('Authorization', ALTRO_AUTH_HEADER);
|
||||
|
||||
expect(res.status).toBe(403);
|
||||
});
|
||||
|
||||
test('CORS: risponde con Access-Control-Allow-Origin per un Origin autorizzato', async () => {
|
||||
const res = await request(app).get('/health').set('Origin', 'http://localhost:4200');
|
||||
|
||||
expect(res.headers['access-control-allow-origin']).toBe('http://localhost:4200');
|
||||
});
|
||||
|
||||
test('POST /public/attivita/get/lista/search con [] restituisce tutte le attività presenti', async () => {
|
||||
await request(app)
|
||||
.post('/private/attivita/save')
|
||||
.set('Authorization', AUTH_HEADER)
|
||||
.send(attivitaPayload({ nome: 'Prima' }));
|
||||
await request(app)
|
||||
.post('/private/attivita/save')
|
||||
.set('Authorization', AUTH_HEADER)
|
||||
.send(attivitaPayload({ nome: 'Seconda', stato: { id: 'PU', nome: 'Pubblicato' } }));
|
||||
|
||||
const res = await request(app).post('/public/attivita/get/lista/search').send([]);
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(2);
|
||||
expect(res.body.map((a: { nome: string }) => a.nome).sort()).toEqual(['Prima', 'Seconda']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
import request from 'supertest';
|
||||
import { app } from '../../src/app';
|
||||
import { prisma } from '../../src/db/prisma';
|
||||
|
||||
// L'unica tabella che questi endpoint (read-only sulle anagrafiche) potrebbero
|
||||
// trovare "sporca" e' `materiale` (il seed non ne inserisce nessuno): la
|
||||
// puliamo prima di ogni test per non dipendere dall'ordine di esecuzione
|
||||
// rispetto alle altre suite.
|
||||
beforeEach(async () => {
|
||||
await prisma.materiale.deleteMany();
|
||||
});
|
||||
|
||||
describe('autocomplete endpoints', () => {
|
||||
test('POST /public/autocomplete/get/search senza body contiene il gruppo Testo', async () => {
|
||||
const res = await request(app).post('/public/autocomplete/get/search');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.some((g: { label: string }) => g.label === 'Testo')).toBe(true);
|
||||
});
|
||||
|
||||
test('POST /public/autocomplete/get/search con "gioco" contiene Categoria con le sotto-categorie', async () => {
|
||||
const res = await request(app)
|
||||
.post('/public/autocomplete/get/search')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('"gioco"');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
const categoria = res.body.find((g: { label: string }) => g.label === 'Categoria');
|
||||
expect(categoria).toBeDefined();
|
||||
expect(categoria.objectsList.map((o: { nome: string }) => o.nome).sort()).toEqual(
|
||||
["gioco", "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test('POST /public/autocomplete/get/branca con "e/g" restituisce un solo elemento E/G', async () => {
|
||||
const res = await request(app)
|
||||
.post('/public/autocomplete/get/branca')
|
||||
.set('Content-Type', 'application/json')
|
||||
.send('"e/g"');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toHaveLength(1);
|
||||
expect(res.body[0]).toMatchObject({ nome: 'E/G', gruppo: 'branca' });
|
||||
});
|
||||
|
||||
test('POST /public/autocomplete/get/materiale senza body restituisce array vuoto', async () => {
|
||||
const res = await request(app).post('/public/autocomplete/get/materiale');
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { generateKeyPairSync } from 'crypto';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import nock from 'nock';
|
||||
import { env } from '../../src/config/env';
|
||||
|
||||
const KID = 'test-kid';
|
||||
const CERTS_PATH = `/realms/${env.keycloak.realm}/protocol/openid-connect/certs`;
|
||||
|
||||
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;
|
||||
|
||||
const { privateKey: rogueKey } = generateKeyPairSync('rsa', { modulusLength: 2048 });
|
||||
export const rogueKeyPem = rogueKey.export({ type: 'pkcs1', format: 'pem' }) as string;
|
||||
|
||||
export function mockKeycloakJwks(): void {
|
||||
nock(env.keycloak.baseUrl)
|
||||
.persist()
|
||||
.get(CERTS_PATH)
|
||||
.reply(200, { keys: [{ ...jwk, kid: KID, alg: 'RS256', use: 'sig' }] });
|
||||
}
|
||||
|
||||
interface TokenPayload {
|
||||
sub: string;
|
||||
email?: string;
|
||||
name?: string;
|
||||
realm_access?: { roles?: string[] };
|
||||
}
|
||||
|
||||
export function signValidToken(payload: TokenPayload, signOptions: jwt.SignOptions = {}): string {
|
||||
return jwt.sign(payload, privateKeyPem, {
|
||||
algorithm: 'RS256',
|
||||
keyid: KID,
|
||||
expiresIn: '5m',
|
||||
...signOptions,
|
||||
});
|
||||
}
|
||||
|
||||
export function bearer(payload: TokenPayload, signOptions: jwt.SignOptions = {}): string {
|
||||
return `Bearer ${signValidToken(payload, signOptions)}`;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { execSync } from 'child_process';
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
export default async function globalSetup(): Promise<void> {
|
||||
dotenv.config();
|
||||
|
||||
const databaseUrlTest = process.env.DATABASE_URL_TEST;
|
||||
if (!databaseUrlTest) {
|
||||
throw new Error("Variabile d'ambiente mancante: DATABASE_URL_TEST");
|
||||
}
|
||||
|
||||
// Fa puntare tutto il codice applicativo (incluso src/db/prisma.ts) al DB di test,
|
||||
// senza bisogno di modifiche altrove: dotenv.config() non sovrascrive DATABASE_URL
|
||||
// gia' impostata qui.
|
||||
process.env.DATABASE_URL = databaseUrlTest;
|
||||
|
||||
execSync('npx prisma migrate deploy', {
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
execSync('npx prisma db seed', {
|
||||
env: process.env,
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { prisma } from '../../src/db/prisma';
|
||||
|
||||
export default async function globalTeardown(): Promise<void> {
|
||||
await prisma.$disconnect();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { prisma } from '../../src/db/prisma';
|
||||
|
||||
// Solo le tabelle "transazionali" popolate dai test: le anagrafiche di base
|
||||
// seedate una volta sola (stato_attivita, tipo_categoria, tipo_paragrafo,
|
||||
// branca, periodo_anno, categoria, materiale) restano intatte tra un test e l'altro.
|
||||
const TABLES_TO_RESET = [
|
||||
'paragrafo',
|
||||
'branca_attivita',
|
||||
'categoria_attivita',
|
||||
'materiale_attivita',
|
||||
'periodo_anno_attivita',
|
||||
'attivita',
|
||||
];
|
||||
|
||||
export async function resetDatabase(): Promise<void> {
|
||||
const tables = TABLES_TO_RESET.map((table) => `"${table}"`).join(', ');
|
||||
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${tables} RESTART IDENTITY CASCADE`);
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
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> = {}): 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());
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { prisma } from '../../src/db/prisma';
|
||||
import {
|
||||
getBranca,
|
||||
getCategoria,
|
||||
getMateriale,
|
||||
getPeriodoAnno,
|
||||
getSearch,
|
||||
} from '../../src/modules/autocomplete/autocomplete.service';
|
||||
|
||||
// Questo servizio legge solo le anagrafiche di base (branca, categoria,
|
||||
// periodo_anno, materiale) gia' popolate dal seed: niente resetDatabase()
|
||||
// completo qui, altrimenti perderemmo quei dati. L'unica tabella che questa
|
||||
// suite potrebbe "sporcare" e' `materiale` (il seed non ne inserisce
|
||||
// nessuno), quindi la puliamo prima di ogni test.
|
||||
beforeEach(async () => {
|
||||
await prisma.materiale.deleteMany();
|
||||
});
|
||||
|
||||
function nomi(list: { nome: string | null }[]): string[] {
|
||||
return list.map((item) => item.nome ?? '').sort();
|
||||
}
|
||||
|
||||
describe('autocomplete.service', () => {
|
||||
test('getBranca è case-insensitive e restituisce il gruppo "branca"', async () => {
|
||||
const risultati = await getBranca('e/g');
|
||||
|
||||
expect(risultati).toHaveLength(1);
|
||||
expect(risultati[0]).toMatchObject({ nome: 'E/G', gruppo: 'branca' });
|
||||
});
|
||||
|
||||
test('getCategoria("gioco") trova la categoria e le sue sotto-categorie che contengono "gioco"', async () => {
|
||||
const risultati = await getCategoria('gioco');
|
||||
|
||||
expect(nomi(risultati)).toEqual(
|
||||
['gioco', "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||
);
|
||||
expect(risultati.every((r) => r.gruppo === 'categoria')).toBe(true);
|
||||
expect(nomi(risultati)).not.toContain('torneo');
|
||||
expect(nomi(risultati)).not.toContain('olimpiadi');
|
||||
});
|
||||
|
||||
test('getPeriodoAnno("campo") trova i periodi "campo" ma non "promessa"', async () => {
|
||||
const risultati = await getPeriodoAnno('campo');
|
||||
|
||||
expect(nomi(risultati)).toEqual(['campo estivo', 'campo invernale'].sort());
|
||||
expect(risultati.every((r) => r.gruppo === 'periodoAnno')).toBe(true);
|
||||
});
|
||||
|
||||
test('getMateriale su un database senza materiali restituisce array vuoto', async () => {
|
||||
const risultati = await getMateriale('corda');
|
||||
|
||||
expect(risultati).toEqual([]);
|
||||
});
|
||||
|
||||
test('getSearch(null) contiene sempre il gruppo Testo, popola Branca/Categoria/Periodo anno con tutte le righe e omette Materiale (vuoto)', async () => {
|
||||
const gruppi = await getSearch(null);
|
||||
|
||||
expect(gruppi[0]).toEqual({
|
||||
label: 'Testo',
|
||||
objectsList: [{ id: null, nome: null, gruppo: 'testo' }],
|
||||
});
|
||||
|
||||
const branca = gruppi.find((g) => g.label === 'Branca');
|
||||
const categoria = gruppi.find((g) => g.label === 'Categoria');
|
||||
const periodoAnno = gruppi.find((g) => g.label === 'Periodo anno');
|
||||
const materiale = gruppi.find((g) => g.label === 'Materiale');
|
||||
|
||||
expect(branca?.objectsList).toHaveLength(await prisma.branca.count());
|
||||
expect(categoria?.objectsList).toHaveLength(await prisma.categoria.count());
|
||||
expect(periodoAnno?.objectsList).toHaveLength(await prisma.periodoAnno.count());
|
||||
expect(materiale).toBeUndefined();
|
||||
});
|
||||
|
||||
test('getSearch("gioco") restituisce Testo e Categoria ma non Branca, Periodo anno o Materiale', async () => {
|
||||
const gruppi = await getSearch('gioco');
|
||||
|
||||
const labels = gruppi.map((g) => g.label);
|
||||
expect(labels).toContain('Testo');
|
||||
expect(labels).toContain('Categoria');
|
||||
expect(labels).not.toContain('Branca');
|
||||
expect(labels).not.toContain('Periodo anno');
|
||||
expect(labels).not.toContain('Materiale');
|
||||
|
||||
const categoria = gruppi.find((g) => g.label === 'Categoria');
|
||||
expect(nomi(categoria!.objectsList)).toEqual(
|
||||
['gioco', "gioco d'acqua", 'gioco giungla', 'gioco notturno', 'grande gioco'].sort(),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import express from 'express';
|
||||
import request from 'supertest';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import nock from 'nock';
|
||||
import { authenticate } from '../../../src/middlewares/authenticate';
|
||||
import { bearer, mockKeycloakJwks, rogueKeyPem, signValidToken } from '../../setup/authTestHelper';
|
||||
|
||||
const CAPO_PAYLOAD = {
|
||||
sub: 'user-123',
|
||||
email: 'capo@example.com',
|
||||
name: 'Capo Unità',
|
||||
realm_access: { roles: ['capo-unita'] },
|
||||
};
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.get('/protetta', authenticate, (req, res) => {
|
||||
res.json({ auth: req.auth });
|
||||
});
|
||||
return app;
|
||||
}
|
||||
|
||||
beforeAll(() => {
|
||||
mockKeycloakJwks();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
nock.cleanAll();
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
test('restituisce 401 se manca il token', async () => {
|
||||
const app = buildApp();
|
||||
|
||||
const response = await request(app).get('/protetta');
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test('restituisce 401 se il token è scaduto', async () => {
|
||||
const app = buildApp();
|
||||
const token = signValidToken(CAPO_PAYLOAD, { expiresIn: '-10s' });
|
||||
|
||||
const response = await request(app).get('/protetta').set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test('restituisce 401 se il token ha una firma non valida', async () => {
|
||||
const app = buildApp();
|
||||
const token = jwt.sign(CAPO_PAYLOAD, rogueKeyPem, {
|
||||
algorithm: 'RS256',
|
||||
keyid: 'test-kid',
|
||||
expiresIn: '5m',
|
||||
});
|
||||
|
||||
const response = await request(app).get('/protetta').set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
});
|
||||
|
||||
test('restituisce 200 e popola req.auth se il token è valido', async () => {
|
||||
const app = buildApp();
|
||||
|
||||
const response = await request(app).get('/protetta').set('Authorization', bearer(CAPO_PAYLOAD));
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.auth).toEqual({
|
||||
userId: 'user-123',
|
||||
email: 'capo@example.com',
|
||||
name: 'Capo Unità',
|
||||
roles: ['capo-unita'],
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user