modulo 10 / lezione 53
Capstone: bug reale end-to-end
Capstone: fix, test e verifica
Applicare fix minimo, scrivere test di regressione, eseguire matrice di verifica e preparare commit e branch.
In parole semplici
Hai la causa. Ora il fix. Novanta minuti per: applicare le modifiche minime che eliminano il bug, scrivere test che lo catturino, eseguire la matrice di verifica, preparare una pull request pulita.
Cosa impari (rivedere applicare)
- Modulo 5.1: fix minimo vs cosmetico.
- Modulo 5.2: test che fallisce prima del fix.
- Modulo 5.3: matrice di verifica.
- Modulo 5.4: scelta fix-forward / branch / rollback.
- Modulo 5.5: commit message Conventional.
Sessione fix: scaletta 90 minuti
0–10 min: branch e setup
git checkout -b fix/capstone-checkout-duplicati-e-mancanti
Apri il taccuino: rileggi la causa radice e le direzioni di fix.
10–35 min: fix 1 (client) — disabilitazione pulsante + AbortController
Localizza il pulsante “Conferma” nel codice client.
Modifica minima:
function ConfermaOrdine({ carrello }) {
const [inCorso, setInCorso] = useState(false);
const controllerRef = useRef();
async function handleClick() {
if (inCorso) return;
setInCorso(true);
controllerRef.current?.abort();
controllerRef.current = new AbortController();
const key = crypto.randomUUID();
try {
const r = await fetch("/api/ordini", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Idempotency-Key": key,
},
body: JSON.stringify({ items: carrello }),
signal: controllerRef.current.signal,
});
if (!r.ok) throw new Error(`HTTP ${r.status}`);
mostraConferma();
} catch (err) {
if (err.name !== "AbortError") mostraErrore(err);
} finally {
setInCorso(false);
}
}
return (
<button onClick={handleClick} disabled={inCorso}>
{inCorso ? "Confermo..." : "Conferma ordine"}
</button>
);
}
Cambi minimi:
inCorsoblocca submit multipli.disabled={inCorso}impedisce ai click ulteriori.Idempotency-KeyUUID per richiesta.
35–55 min: fix 2 (server) — idempotency key
Localizza l’handler POST /api/ordini. Aggiungi check:
async function creaOrdine(req, res) {
const key = req.headers["idempotency-key"];
if (key) {
const esistente = await idempotencyStore.get(key);
if (esistente) {
return res.status(200).json(esistente); // ritorna risultato precedente
}
}
// logica creazione ordine (transazione)
const ordine = await db.transazione(async (tx) => {
return tx.ordini.crea(req.body);
});
if (key) {
await idempotencyStore.set(key, ordine, { ttl: 24 * 3600 });
}
res.status(201).json(ordine);
}
idempotencyStore può essere Redis o una tabella DB con (key UNIQUE, response JSONB, expires_at).
55–75 min: test di regressione
Test 1 (unit/integrazione lato server):
import { describe, it, expect } from "vitest";
import { creaOrdine } from "../server/routes/ordini.js";
describe("creaOrdine idempotency", () => {
it("ritorna lo stesso ordine se chiamato due volte con stessa idempotency-key", async () => {
const key = "test-key-1";
const body = { items: [{ id: 1, qta: 1 }] };
const r1 = await fetch("/api/ordini", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(body),
});
const ordine1 = await r1.json();
const r2 = await fetch("/api/ordini", {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(body),
});
const ordine2 = await r2.json();
expect(ordine1.id).toBe(ordine2.id);
// verifica DB: c'è un solo ordine, non due
const count = await db.ordini.count({ utente_id: ordine1.utente_id });
expect(count).toBe(1);
});
});
Test 2 (E2E con Playwright):
import { test, expect } from "@playwright/test";
test("doppio click su Conferma crea un solo ordine", async ({ page }) => {
await page.goto("/checkout");
// doppio click rapido
const btn = page.locator('button[data-testid="conferma"]');
await Promise.all([btn.click(), btn.click()]);
await page.waitForSelector("text=Ordine confermato");
await page.goto("/i-miei-ordini");
const count = await page.locator(".ordine-row").count();
expect(count).toBe(1);
});
Esegui prima i test senza fix: devono fallire. Poi applica fix. Devono passare.
75–85 min: matrice di verifica manuale
| # | Caso | Atteso |
|---|---|---|
| 1 | click singolo “Conferma” | 1 ordine creato |
| 2 | doppio click rapido (< 200ms) | 1 ordine (idempotency vince) |
| 3 | ”Conferma” su carrello vuoto | errore visibile, niente ordine |
| 4 | ”Conferma” con server giù | errore visibile, niente ordine |
| 5 | ”Conferma” + refresh subito | nessun ordine scomparso o duplicato |
| 6 | due tab simultanee, stesso utente | 2 ordini distinti |
| 7 | login → carrello → checkout (normale) | ordine ok, lista corretta |
Esegui ogni caso, annota osservato.
85–90 min: commit e PR
git add -p
git commit -m "fix(checkout): prevenire ordini duplicati e scomparsi" -m "
Il bug aveva tre cause concorrenti:
1. Il pulsante 'Conferma' non veniva disabilitato durante la richiesta,
permettendo doppi submit.
2. Il server non rispettava l'header Idempotency-Key, creando ordini
duplicati per richieste identiche.
3. (Da indagare in #C-002) race condition tra POST e GET ordini.
Fix applicati:
- Client: disable button + AbortController + Idempotency-Key UUID.
- Server: store idempotency-key in Redis (24h TTL).
Verificato:
- Test E2E playwright/checkout-no-duplicates.spec.ts.
- Test integrazione ordini-idempotency.test.ts.
- Matrice manuale: 7/7 casi OK.
Refs: #C-001
"
Apri PR con:
- Titolo:
fix(checkout): prevenire ordini duplicati e scomparsi - Descrizione: sintesi del bug, link al taccuino, screenshot prima/dopo.
- Checklist di review canonica (modulo 6.4).
Esito atteso
Alla fine dei 90 minuti:
- ✅ Fix applicati su client + server.
- ✅ Almeno un test E2E e uno unitario che falliscono pre-fix e passano post-fix.
- ✅ Matrice di verifica eseguita.
- ✅ Branch
fix/capstone-...con commit puliti. - ✅ Pull request pronta per review.
Errori frequenti del capstone
- Fix in più punti senza test in ognuno.
- Refactor opportunistico in commit di fix.
- Saltare la matrice manuale e fidarsi solo dei test automatici.
- Commit message vago che non aiuta chi rivede tra sei mesi.