Les proves de les dues lliçons anteriors són verdes: el domini calcula bé, la matriu de permisos és correcta cel·la per cel·la, el conversor de divises degrada amb elegància. I tot i així, l'API d'Escena Viva podria estar completament trencada.
Considera aquest escenari, que passa de veritat. Algú reordena els middlewares a crearAplicacio i col·loca la ruta de comandes abans que express.json(). Resultat: req.body arriba undefined, l'esquema zod ho rebutja tot i cada compra retorna 400. Quantes proves unitàries ho detecten? Cap. El controlador funciona perfectament quan li passes un req fabricat a mà, l'esquema valida bé el que se li dona, el repositori guarda correctament. Totes les peces estan bé; el cablatge està malament.
Això és el que proven les proves d'integració: que les peces encaixen quan s'executen juntes de veritat.
Contingut
- Què integra una prova d'integració
- supertest i el disseny del mòdul 6
- La primera prova d'extrem a extrem
- La base de dades a les proves
- Aïllament entre proves
- Autenticar a les proves
- Provar els camins que importen
- Provar la concurrència contra la sobrevenda
- Contracte del format d'error i serveis externs
Què integra una prova d'integració
Una prova d'integració executa diverses capes reals juntes. Una petició a POST /api/comandes recorre això:
graph LR
P["Peticio HTTP"] --> M1["helmet · cors · json<br/>id-peticio · registre"]
M1 --> M2["limitCompra"] --> M3["autenticar"] --> M4["validar (zod)"]
M4 --> C["controlador de comandes"] --> R["repositori + transaccio"]
R --> BD[("escena_viva")]
C --> E["gestorDErrors"] --> RES["Resposta JSON"]
Cada fletxa és un punt de fallada invisible per a una prova unitària: middleware mal ordenat o no muntat; una ruta amb el verb o el prefix incorrectes; el gestorDErrors col·locat abans que les rutes i per tant no assolit mai; un esquema zod que valida un camp que el controlador espera amb un altre nom; el repositori retornant un document de Mongoose on s'espera un objecte del domini; la transacció del mòdul 7 que a la pràctica no aïlla el que creiem; i l'estat HTTP real de cada error de la jerarquia.
supertest i el disseny del mòdul 6
Què fa supertest, exactament: pren la teva aplicació d'Express, l'arrenca en un port efímer que li assigna el sistema operatiu, fa una petició HTTP real contra aquell port i tanca el servidor en acabar. És una petició de veritat, no una simulació: passa per tots els middlewares, l'enrutador, l'anàlisi del cos i el gestor d'errors. I com que el port és efímer, mai no xocaràs amb un EADDRINUSE encara que tinguis el servidor de desenvolupament al 3000.
Aquí es cobra una decisió del mòdul 6 que aleshores va poder semblar un caprici: src/app.js exporta una factoria que no crida mai listen, i és src/servidor.js qui connecta la base de dades, escolta i gestiona l'aturada ordenada. Gràcies a aquesta separació, la prova és una línia:
const app = crearAplicacio({ servirEstatics: false, registrar: false, limitar: false });
await request(app).get('/api/esdeveniments').expect(200);Si app.js cridés listen en carregar-se —l'error més comú en projectes d'Express—, cada fitxer de proves obriria un servidor al 3000, el segon fallaria i el procés no acabaria mai. Aquesta és la raó real de la factoria.
| Opció | Valor a les proves | Per què |
|---|---|---|
servirEstatics / registrar |
false |
Els estàtics toquen el disc a cada petició i morgan embruta la sortida de Mocha |
limitar |
false |
Crític: amb express-rate-limit actiu, la prova de concurrència rebria 429 en comptes de provar l'aforament |
L'última té un matís: desactivar els límits està bé, però aleshores cal provar-los en un fitxer a part amb limitar: true, o ningú no comprovarà mai que funcionen.
La primera prova d'extrem a extrem
// test/integracio/esdeveniments.test.js
'use strict';
const request = require('supertest');
const { expect } = require('chai');
const { crearAplicacio } = require('../../src/app.js');
describe('GET /api/esdeveniments', () => {
const app = crearAplicacio({ servirEstatics: false, registrar: false, limitar: false });
it('retorna 200 amb el cataleg en JSON', async () => {
const r = await request(app).get('/api/esdeveniments')
.expect(200).expect('Content-Type', /application\/json/);
expect(r.body.dades).to.be.an('array').with.lengthOf(3);
});
it('cada esdeveniment inclou titol, sala i sessions', async () => {
const { body } = await request(app).get('/api/esdeveniments').expect(200);
const concert = body.dades.find((e) => e.id === 'evt-001');
expect(concert).to.deep.include({ titol: 'Concierto de Otono', salaId: 'org-almendra' });
expect(concert.sessions).to.have.lengthOf(2);
});
it('no exposa camps interns del model', async () => {
const { body } = await request(app).get('/api/esdeveniments').expect(200);
expect(body.dades[0]).to.not.have.any.keys('_id', '__v');
});
});La tercera és de les més rendibles que existeixen: comprova que la serialització no filtra els camps interns de Mongoose. És una fallada que apareix tan bon punt algú retorna el document cru en lloc de l'objecte del domini, i que ningú no mira fins que un client pregunta què és __v.
El repertori s'encadena: .post(ruta), .set(capcalera, valor), .send(cosJSON), .query({ divisa: 'GBP' }), .expect(estat), .expect('Content-Type', /json/) i .expect((res) => ...) per a una asserció a mida sobre la resposta.
Consell pràctic: fes servir .expect() per a l'estat i les capçaleres, i Chai per al cos, perquè els missatges de fallada de Chai sobre objectes són molt més informatius. I si escrius .expect(200) i el servidor retorna 500, supertest imprimeix els estats però no el cos de l'error: mentre depures, desa la resposta i imprimeix resposta.body.
La base de dades a les proves
Aquesta és la decisió de disseny més important de la lliçó.
| Opció | Avantatges | Inconvenients |
|---|---|---|
Base real dedicada (escena_viva_test) |
Fidelitat total: índexs, transaccions, tipus reals | Requereix el servei instal·lat; s'ha de netejar; més lenta |
mongodb-memory-server |
Sense instal·lar res; aïllada; ràpida | Descarrega binaris; pot divergir de la versió de producció |
| SQLite en memòria (part Sequelize) | Instantània, zero configuració | Dialecte diferent de PostgreSQL: no prova blocatges ni tipus reals |
| Contenidor efímer (Testcontainers) | Màxima fidelitat i aïllament | Necessita Docker; arrencada lenta |
| Repositori fals en memòria | Rapidíssim | No prova la integració, que és l'objectiu |
La nostra elecció: una base real dedicada, escena_viva_test. La prova estrella d'aquesta lliçó és la de concurrència contra la sobrevenda, que depèn del comportament transaccional real del motor: amb SQLite o amb un repositori fals passaria sempre sense demostrar res. A més ja tenim la llavor idempotent del mòdul 7, src/config/index.js és l'únic punt que llegeix process.env —canviar de base és canviar una variable— i a CI s'aixeca un servei amb dues línies.
Creem .env.test amb NODE_ENV=test, MONGODB_URI=mongodb://localhost:27017/escena_viva_test, DATABASE_URL=postgres://escena:escena@localhost:5432/escena_viva_test, un JWT_SECRET de proves, BCRYPT_COST=4, NIVELL_REGISTRE=silent i TZ=UTC. I el carreguem abans que res des del fitxer d'arrencada de Mocha:
// test/ajudes/preparacio.js
'use strict';
const path = require('node:path');
const sinon = require('sinon');
// Carrega .env.test ABANS que src/config/index.js llegeixi process.env
require('dotenv').config({ path: path.join(__dirname, '..', '..', '.env.test') });
const { connectar, desconnectar } = require('../../src/db/connexio.js');
exports.mochaHooks = {
async beforeAll() { await connectar(); },
afterEach() { sinon.restore(); },
async afterAll() { await desconnectar(); }, // sense aixo el proces no acaba
};L'ordre importa moltíssim. src/config/index.js llegeix process.env en carregar-se i congela el resultat; si dotenv s'executa després del primer require d'un mòdul que importi la configuració, la teva prova es connectarà alegrement a la base de desenvolupament i la buidarà. Carregar-lo des del require de .mocharc.json, abans que qualsevol altra cosa, és la garantia. I com a salvaguarda barata, una funció comprovarBaseDeProves() que llança si configuracio.mongodbUri no inclou _test, invocada abans de qualsevol esborrat.
Aïllament entre proves
Les proves no poden dependre de l'ordre en què s'executen. Si la prova B només passa després de l'A, tens una bomba de rellotgeria: el dia que algú afegeixi .only, executi un sol fitxer o activi el paral·lelisme, B fallarà sense motiu aparent.
// test/ajudes/base-dades.js
async function netejar() {
comprovarBaseDeProves();
const colleccions = await mongoose.connection.db.collections();
await Promise.all(colleccions.map((c) => c.deleteMany({}))); // sense destruir index
}
async function sembrarDadesDeProva() {
await netejar();
await sembrar({ silencios: true }); // 7 sessions, aforament 3000, venudes 1811
}El seu ús a cada fitxer és beforeEach(sembrarDadesDeProva) i afterEach(netejar). Per què beforeEach i no before: perquè la primera prova que compri entrades modificarà l'aforament i la segona esperaria un número diferent. Sembrar a cada prova costa uns mil·lisegons i compra determinisme.
Compartir estat entre proves és la font número u de proves intermitents, i apareix de tres maneres: un objecte de mòdul mutat per una prova i llegit per una altra, dades a la base que sobreviuen a la prova que les va crear, i una memòria cau en memòria (com la de canvi-divises.js) que no es buida.
Autenticar a les proves
Aquí hi ha la promesa del mòdul 8: gairebé totes les rutes interessants exigeixen un usuari autenticat. Com aconsegueix la prova un token vàlid?
La mala idea és fer l'entrada des de la prova amb credencials escrites al codi: fica credencials al repositori (un hàbit que acaba amb una contrasenya real en un commit), és lent perquè cada entrada executa bcrypt a propòsit, acobla tota la bateria al punt d'entrada de sessió —si es trenca fallen cinquanta proves en comptes d'una— i és fràgil davant de qualsevol canvi de la política de contrasenyes.
La bona idea és una ajuda que crea l'usuari que la prova necessita i signa un token amb el mateix servei que fa servir l'aplicació:
// test/ajudes/autenticacio.js
'use strict';
const { Usuari } = require('../../src/models/usuari.js');
const { signarAcces } = require('../../src/serveis/tokens.js');
let comptador = 0;
/** Crea un usuari amb el rol demanat i retorna un token d'acces valid. */
async function crearUsuariAutenticat({ rol = 'assistent', salaId = null } = {}) {
comptador += 1;
const usuari = await Usuari.create({
nom: `Usuari de prova ${comptador}`,
correu: `prova-${rol}-${comptador}@escenaviva.test`,
hashContrasenya: '$2b$12$000000000000000uGf3nFEQhZ1lPCpNvBrjLzcVX2CqOMy',
rol, salaId, verificat: true, // hash irrellevant: no hi entrarem mai amb ell
});
const token = signarAcces({ id: usuari.id, rol: usuari.rol, salaId: usuari.salaId });
return { usuari, token, capcalera: ['Authorization', `Bearer ${token}`] };
}
const comAssistent = () => crearUsuariAutenticat({ rol: 'assistent' });
const comOrganitzador = (s = 'org-almendra') => crearUsuariAutenticat({ rol: 'organitzador', salaId: s });
const comAdministrador = () => crearUsuariAutenticat({ rol: 'administrador' });
module.exports = { comAssistent, comOrganitzador, comAdministrador };Es fa servir desplegant l'array a .set(...): const lucia = await comAssistent() i després request(app).post('/api/comandes').set(...lucia.capcalera), o comOrganitzador('org-almendra') per editar un esdeveniment del Teatro Almendra. En veuràs desenes d'exemples a la resta de la lliçó.
Per què això és correcte i no una trampa: fem servir el mateix signarAcces que fa servir producció, així que el token passa exactament per la mateixa verificació que qualsevol token real —signatura HS256 amb el mateix secret, caducitat, format de la càrrega—. No ens saltem l'autenticació; ens saltem l'entrada de sessió, que és una operació diferent i que es prova a part, una sola vegada, a test/integracio/autenticacio.test.js: credencials vàlides, 401 amb la mateixa latència si el correu no existeix (el hash esquer de bcrypt), 401 amb contrasenya incorrecta i no revelar quina de les dues ha fallat.
Provar els camins que importen
El flux complet de compra
describe('flux de compra d entrades', () => {
beforeEach(async () => { await sembrarDadesDeProva(); });
afterEach(async () => { await netejar(); });
it('crea la comanda, retorna 201 amb Location i permet recuperar-la', async () => {
const lucia = await comAssistent();
const creacio = await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 2 })
.expect(201).expect('Content-Type', /json/);
expect(creacio.body.totalCentims).to.equal(5000);
expect(creacio.body.estat).to.equal('pendent');
expect(creacio.body.entrades).to.have.lengthOf(2);
expect(creacio.body.entrades[0].codi).to.match(/^EV-\d{4}-\d{6}$/);
expect(creacio.headers.location).to.equal(`/api/comandes/${creacio.body.id}`);
// El recurs creat es recuperable a la ubicacio anunciada
const consulta = await request(app).get(creacio.headers.location)
.set(...lucia.capcalera).expect(200);
expect(consulta.body.id).to.equal(creacio.body.id);
});
it('descompta l aforament de la sessio despres de la compra', async () => {
const lucia = await comAssistent();
const abans = await request(app).get('/api/sessions/ses-001-1').expect(200);
await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 3 }).expect(201);
const despres = await request(app).get('/api/sessions/ses-001-1').expect(200);
expect(despres.body.lliures).to.equal(abans.body.lliures - 3);
});
});La segona prova lliga el món HTTP amb el món de les dades: comprova que la compra ha tingut efecte real i persistent, no només que ha retornat 201.
Els rebuigs
Cada estat d'error de la jerarquia del mòdul 6 mereix la seva prova:
it('retorna 401 si no s envia token', async () => {
const { body } = await request(app).post('/api/comandes')
.send({ sessioId: 'ses-001-1', quantitat: 2 }).expect(401);
expect(body.error.codi).to.equal('NO_AUTENTICAT');
});
it('retorna 403 si un organitzador edita un esdeveniment d una altra sala', async () => {
const bruno = await comOrganitzador('org-boveda');
await request(app).patch('/api/esdeveniments/evt-001') // pertany a org-almendra
.set(...bruno.capcalera).send({ descripcio: 'Sala equivocada' }).expect(403);
});
it('retorna 404 si la sessio no existeix', async () => {
const lucia = await comAssistent();
const { body } = await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-999-9', quantitat: 2 }).expect(404);
expect(body.error.codi).to.equal('SESSIO_NO_TROBADA');
});
it('retorna 409 si l aforament es insuficient', async () => {
const lucia = await comAssistent();
await ajustarAforament('ses-001-1', { lliures: 1 });
const { body } = await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 4 }).expect(409);
expect(body.error.detalls).to.deep.include({ lliures: 1, sollicitades: 4 }); // per que
});
it('retorna 422 si es demanen mes de 6 entrades', async () => {
const lucia = await comAssistent();
await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 7 }).expect(422);
});Hi falten tres germanes evidents, del mateix motlle: el 400 amb un cos que no compleix l'esquema ({ quantitat: 'dos' }, sense sessioId, el error.detalls del qual ha de ser un array amb les fallades de zod), el 401 amb un token manipulat (Bearer no.es.un.token) i el 403 d'un assistent que intenta editar un esdeveniment.
La referència directa insegura
De totes les proves del mòdul, aquesta és la que més val per línia escrita. És la fallada de seguretat més comuna de les APIs REST: un identificador a l'URL que el servidor serveix sense comprovar de qui és.
it('impedeix que un assistent vegi la comanda d un altre assistent', async () => {
const lucia = await comAssistent();
const marc = await comAssistent();
const comandaDeLucia = await request(app).post('/api/comandes').set(...lucia.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 2 }).expect(201);
// El Marc coneix l'id de la comanda de la Lucia i la demana directament
const { body } = await request(app).get(`/api/comandes/${comandaDeLucia.body.id}`)
.set(...marc.capcalera)
.expect(404); // 404, no 403: no confirmem que el recurs existeix
expect(body.error.codi).to.equal('COMANDA_NO_TROBADA');
});El 404 en comptes de 403 és deliberat i el vam decidir al mòdul 8: respondre 403 confirmaria al Marc que aquella comanda existeix, informació que no li correspon. La prova fixa aquesta decisió de seguretat perquè ningú no la desfaci creient que millora els missatges d'error. El seu complement és la prova que un administrador sí que pot veure la comanda de qualsevol assistent.
Provar la concurrència contra la sobrevenda
Aquesta prova valida tota la feina transaccional del mòdul 7, i no hi ha manera d'escriure-la que no sigui d'integració.
it('no sobreven quan arriben compres simultanies per les ultimes entrades', async () => {
await ajustarAforament('ses-001-1', { lliures: 5 });
// Deu assistents diferents demanen 2 entrades cadascun: 20 sollicitades, 5 disponibles
const compradors = await Promise.all(Array.from({ length: 10 }, () => comAssistent()));
const resultats = await Promise.all(compradors.map((c) =>
request(app).post('/api/comandes').set(...c.capcalera)
.send({ sessioId: 'ses-001-1', quantitat: 2 })));
const exits = resultats.filter((r) => r.status === 201);
const conflictes = resultats.filter((r) => r.status === 409);
expect(exits).to.have.lengthOf(2); // nomes hi caben 2 compres de 2 en 5 localitats
expect(conflictes).to.have.lengthOf(8);
conflictes.forEach((r) => expect(r.body.error.codi).to.equal('AFORAMENT_INSUFICIENT'));
const sessio = await request(app).get('/api/sessions/ses-001-1').expect(200);
expect(sessio.body.lliures).to.equal(1); // l'estat final es coherent
expect(sessio.body.venudes).to.be.at.most(sessio.body.aforament);
});L'asserció decisiva és l'última: venudes no supera mai aforament. Si el mòdul 7 hagués implementat la compra amb un llegir → comprovar → escriure sense transacció ni actualització condicional, aquesta prova posaria en evidència la condició de cursa immediatament: les deu compres llegirien «5 lliures», les deu passarien la comprovació i vendríem 20 entrades per a 5 seients.
Dos advertiments honestos: la prova és no determinista en el seu detall —el nombre exacte d'èxits pot variar si la lògica permet compres parcials, així que afirma la invariant i no un repartiment concret— i és lenta, uns quants centenars de mil·lisegons amb base real. Val la pena: és la prova que impedeix vendre una butaca dues vegades.
Contracte del format d'error i serveis externs
El format { error: { codi, missatge, estat, detalls } } és un contracte amb qui consumeixi l'API, i val la pena fixar-lo amb una taula de casos:
const CASOS = [
['401 sense token', () => request(app).post('/api/comandes').send({}), 401],
['404 ruta inexistent', () => request(app).get('/api/no-existeix'), 404],
['400 cos invalid', () => request(app).post('/api/autenticacio/entrada').send({}), 400],
];
CASOS.forEach(([nom, peticio, estat]) => {
it(`respecta el format d error a ${nom}`, async () => {
const { body } = await peticio().expect(estat);
expect(body.error).to.have.all.keys('codi', 'missatge', 'estat', 'detalls');
expect(body.error.estat).to.equal(estat);
expect(body.error.codi).to.be.a('string').and.match(/^[A-Z_]+$/);
});
});Val la pena afegir-hi una prova més, que comprova que la resposta d'error no filtra la pila de crides: expect(body.error).to.not.have.property('stack') i expect(JSON.stringify(body)).to.not.include('node_modules'). Filtrar una traça al client revela rutes del servidor, versions de biblioteques i de vegades fragments de consultes, i és una fallada silenciosa que apareix tan bon punt algú «millora» el gestor d'errors per depurar i s'oblida de treure-ho.
En integració provem el nostre sistema complet, però no els serveis de tercers: cridar el proveïdor real de taxes a cada execució produeix proves lentes, intermitents i dependents d'una quota. La tècnica és simular a la vora: mantenir tota la integració real i substituir únicament la crida sortint amb Sinon.
it('continua retornant el cataleg si el conversor esta caigut', async () => {
sinon.stub(global, 'fetch').rejects(new Error('ECONNREFUSED'));
const { body } = await request(app).get('/api/esdeveniments?divisa=GBP').expect(200);
expect(body.dades).to.have.lengthOf(3);
});Aquesta prova té un valor enorme: garanteix que una caiguda d'un proveïdor extern no tomba el catàleg, un requisit de negoci real que només una prova d'integració amb la vora simulada pot demostrar.
Nota sobre extrem a extrem amb navegador. Per sobre hi ha el nivell real: un navegador automatitzat que obre el web, busca el Festival de Jazz de Primavera, tria butaques, paga i comprova que l'entrada apareix. Playwright és avui la referència a Node, amb Cypress com a alternativa. És fora de l'abast d'aquest curs, que és de servidor, i hi ha una raó de fons a més del temari: són les proves més cares i fràgils de la piràmide. La recomanació professional és tenir-ne molt poques, només sobre els fluxos que enfonsen el negoci, i resoldre la resta als nivells de sota, que és el que hem fet.
Errors Comuns i Consells
- Apuntar a la base de desenvolupament. Es descobreix quan desapareixen les dades. Carrega
.env.testdes delrequirede Mocha i comprova_testabans de qualsevol esborrat. - Oblidar l'
awaita supertest.request(app).get('/api/esdeveniments').expect(200)senseawaitretorna una promesa: la prova passa sempre. És el parany de 09-02 disfressat. - Deixar els límits de peticions actius. La prova de concurrència rep 429 en lloc de 409 i en culpes la transacció.
- Sembrar a
beforeen comptes debeforeEach. Cada compra altera l'aforament i la segona prova espera un número que ja no és el seu. I no facis l'entrada de sessió a cada prova: bcrypt és lent a propòsit, per a això hi ha l'ajuda d'autenticació. - No tancar la connexió en acabar. Amb
"exit": falseel procés es queda penjat: aquest és l'afterAllambdesconnectar(). I no depenguis de l'ordre d'un array: Mongo no el garanteix sensesort, així que cerca per id ambdades.find(...). - Consell: quan una prova d'integració falli, imprimeix
resposta.bodyabans que res; el cos de l'error sol dir exactament què ha passat. I executa la bateria dues vegades seguides sense netejar a mà: si la segona falla, la teva neteja és incompleta.
Exercicis
Exercici 1: els límits de peticions
Crea test/integracio/limits.test.js amb una aplicació construïda amb limitar: true. Escriu una prova que repeteixi POST /api/autenticacio/entrada fins a superar limitEntrada i comprovi que l'última retorna 429 amb el seu codi i la capçalera Retry-After. Pensa com evitar que aquest fitxer contamini els altres.
Exercici 2: el cicle de vida del refresc
Escriu les proves del refresc rotatori del mòdul 8: que POST /api/autenticacio/refrescar amb una galeta vàlida retorna 200 i una galeta nova diferent; que reutilitzar la galeta antiga després de rotar-la retorna 401 i revoca la família sencera; i que després de la revocació ni tan sols la galeta nova funciona.
Exercici 3: l'organitzador i els seus informes
Escriu les proves de GET /api/esdeveniments/:id/informe, que només pot consultar l'organitzador propietari de la sala o un administrador. Cobreix 200 per a l'organitzador propietari, 403 per al d'una altra sala, 403 per a l'assistent, 401 sense token, 200 per a l'administrador i 404 per a un esdeveniment inexistent demanat per un administrador.
Solucions
Exercici 1
// Aplicacio PROPIA amb limits actius: no es comparteix amb els altres fitxers
const appLimitada = crearAplicacio({ servirEstatics: false, registrar: false, limitar: true });
it('retorna 429 en superar el limit d intents d entrada', async () => {
const credencials = { correu: '[email protected]', contrasenya: 'ElQueSigui1!' };
let ultima;
for (let i = 0; i < 12; i += 1) {
ultima = await request(appLimitada).post('/api/autenticacio/entrada').send(credencials);
if (ultima.status === 429) break;
}
expect(ultima.body.error.codi).to.equal('MASSA_PETICIONS');
expect(ultima.headers).to.have.property('retry-after'); // quan reintentar
});La contaminació s'evita fent servir una instància pròpia de l'aplicació (cada crearAplicacio crea els seus propis comptadors d'express-rate-limit) i no reutilitzant-la en altres fitxers. Si el magatzem de límits fos extern —Redis, mòdul 10—, caldria buidar-lo a afterEach.
Exercici 2
it('revoca la familia si es reutilitza una galeta ja rotada', async () => {
const inicial = (await iniciarSessio()).headers['set-cookie'];
const primera = await request(app).post('/api/autenticacio/refrescar')
.set('Cookie', inicial).expect(200);
const nova = primera.headers['set-cookie'];
expect(nova[0]).to.not.equal(inicial[0]);
const reus = await request(app).post('/api/autenticacio/refrescar')
.set('Cookie', inicial).expect(401); // reutilitzar l'antiga: senyal de robatori
expect(reus.body.error.codi).to.equal('REFRESC_REUTILITZAT');
// La familia sencera queda revocada: ni tan sols la nova val
await request(app).post('/api/autenticacio/refrescar').set('Cookie', nova).expect(401);
});És una de les proves més valuoses del projecte: la detecció de reutilització és lògica de seguretat subtil que ningú no recorda sis mesos després i que una refactorització ben intencionada pot desactivar sense que es noti.
Exercici 3
const RUTA = '/api/esdeveniments/evt-001/informe';
it('retorna 200 a l organitzador propietari de la sala', async () => {
const marta = await comOrganitzador('org-almendra');
const { body } = await request(app).get(RUTA).set(...marta.capcalera).expect(200);
expect(body).to.have.property('ingressosCentims');
});
it('retorna 404 si l esdeveniment no existeix, fins i tot per a l administrador', async () => {
const admin = await comAdministrador();
await request(app).get('/api/esdeveniments/evt-999/informe').set(...admin.capcalera).expect(404);
});Les altres quatre són del mateix motlle: 403 per a l'organitzador d'org-boveda, 403 per a l'assistent, 401 sense token i 200 per a l'administrador. Fixa't en l'ordre que revelen aquestes proves: primer autenticació (401), després autorització (403) i només aleshores existència (404). Si el 404 es comprovés abans que el 403, un assistent podria esbrinar quins identificadors d'esdeveniment existeixen sondejant l'API.
Conclusió
Hem pujat un nivell a la piràmide i hem guanyat el que les proves unitàries no podien donar: la certesa que les peces encaixen. Ara sabem que les rutes estan muntades, que els middlewares s'executen en l'ordre correcte, que el gestorDErrors tradueix cada error de la jerarquia al seu estat HTTP i al seu format de contracte, que l'aforament es descompta de veritat a la base de dades, que un assistent no pot veure la comanda d'un altre, i que deu compres simultànies per cinc butaques venen cinc butaques.
I hem resolt la promesa del mòdul 8 amb test/ajudes/autenticacio.js: una ajuda que crea l'usuari amb el rol necessari i signa un token vàlid amb el mateix servei que fa servir l'aplicació, sense ni una sola contrasenya escrita al codi i sense pagar el cost de bcrypt a cada prova.
Ens queda una pregunta incòmoda. Tenim moltes proves, però cobreixen el que importa? Hi ha branques del codi que cap it no ha executat mai? Quant triga la bateria i què passarà quan trigui el triple? Com s'assegura l'equip que ningú no puja codi amb les proves vermelles?
A la lliçó següent, Cobertura i Automatització de les Proves, mesurem què estem provant de veritat amb c8, fixem llindars que només poden pujar, organitzem els scripts d'npm, aprenem a caçar una prova intermitent i deixem el projecte a punt perquè un servidor d'integració contínua executi tot això sense intervenció humana.
Curs de Node.js: De Principiant a Avançat
Mòdul 1: Introducció a Node.js
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
