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.

90 min Debugging

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:

  • inCorso blocca submit multipli.
  • disabled={inCorso} impedisce ai click ulteriori.
  • Idempotency-Key UUID 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

#CasoAtteso
1click singolo “Conferma”1 ordine creato
2doppio click rapido (< 200ms)1 ordine (idempotency vince)
3”Conferma” su carrello vuotoerrore visibile, niente ordine
4”Conferma” con server giùerrore visibile, niente ordine
5”Conferma” + refresh subitonessun ordine scomparso o duplicato
6due tab simultanee, stesso utente2 ordini distinti
7login → 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.