SPB Git forge

spb/toveli

Public
14commits 1branches 0releases
1.1 MBsize
maindefault branch
2 h agolast push
TypeScript 50.6% JavaScript 46.1% SQL 2.1% Shell 0.6% CSS 0.5%
4.4 KB · 50 lines typescript
Raw Blame History
1import { describe, it, expect, beforeAll, afterAll, beforeEach } from "vitest";2import { resetDb, closeDb } from "./setup";3import { requestCode, verifyCode } from "../../src/lib/auth/otp";4import { lastMockMail, setTransportForTests, mockTransport } from "../../src/lib/auth/email";5import { createSession } from "../../src/lib/auth/session";6import { db } from "../../src/db/client";7import { sessions, otpChallenges, users } from "../../src/db/schema";8import { eq } from "drizzle-orm";9import { normalizeEmail, generateOtp, otpDigest, sessionDigest } from "../../src/lib/auth/crypto";1011const codeFromMail = () => lastMockMail.set?.text.match(/\b(\d{6})\b/)?.[1] ?? "";12describe("connexion par code courriel (Resend simulé)", () => {13  beforeAll(() => setTransportForTests(mockTransport)); afterAll(closeDb); beforeEach(resetDb);14  it("normalise les adresses sans supprimer points ni +", () => { expect(normalizeEmail(" Marie.T+toveli@Exemple.CA ")).toBe("marie.t+toveli@exemple.ca"); expect(normalizeEmail("pas une adresse")).toBeNull(); });15  it("génère un code à 6 chiffres et n'en stocke que l'empreinte HMAC", async () => {16    const r = await requestCode("test1@exemple.ca", "1.2.3.4"); expect(r.ok).toBe(true); if (!r.ok) return;17    const code = codeFromMail(); expect(code).toMatch(/^\d{6}$/);18    const [ch] = await db().select().from(otpChallenges).where(eq(otpChallenges.id, r.challengeId)); expect(ch.secretDigest).not.toContain(code); expect(ch.secretDigest).toBe(otpDigest(code, ch.id));19    expect(generateOtp()).toMatch(/^\d{6}$/);20  });21  it("crée le compte au premier succès, ouvre une session, refuse la réutilisation du code", async () => {22    const r = await requestCode("test2@exemple.ca", "1.2.3.4"); if (!r.ok) throw new Error();23    const code = codeFromMail();24    const bad = await verifyCode(r.challengeId, code, "1.2.3.4", false); expect(bad.ok).toBe(false); if (!bad.ok) expect(bad.reason).toBe("terms_required"); // nouveau compte sans acceptation : code non consommé25    const ok = await verifyCode(r.challengeId, code, "1.2.3.4", true); expect(ok.ok).toBe(true); if (!ok.ok) return; expect(ok.created).toBe(true);26    const s = await createSession(ok.userId, { ip: "1.2.3.4", device: "test" }); const [row] = await db().select().from(sessions).where(eq(sessions.tokenDigest, sessionDigest(s.token))); expect(row).toBeTruthy(); expect(row.tokenDigest).not.toBe(s.token);27    const again = await verifyCode(r.challengeId, code, "1.2.3.4", true); expect(again.ok).toBe(false); // usage unique28  });29  it("refuse code faux (×5 → verrou), code expiré et renvoi avant 60 s", async () => {30    const r = await requestCode("test3@exemple.ca", "1.2.3.4"); if (!r.ok) throw new Error();31    for (let i = 0; i < 5; i++) { const v = await verifyCode(r.challengeId, "000000", "1.2.3.4", true); expect(v.ok).toBe(false); }32    const sixth = await verifyCode(r.challengeId, codeFromMail(), "1.2.3.4", true); expect(sixth.ok).toBe(false); if (!sixth.ok) expect(["too_many_attempts", "invalid"]).toContain(sixth.reason);33    const cd = await requestCode("test3@exemple.ca", "1.2.3.4"); expect(cd.ok).toBe(false); if (!cd.ok) expect(cd.reason).toBe("cooldown");34    const r2 = await requestCode("test4@exemple.ca", "1.2.3.4"); if (!r2.ok) throw new Error();35    await db().update(otpChallenges).set({ expiresAt: new Date(Date.now() - 1000) }).where(eq(otpChallenges.id, r2.challengeId));36    const exp = await verifyCode(r2.challengeId, codeFromMail(), "1.2.3.4", true); expect(exp.ok).toBe(false); if (!exp.ok) expect(exp.reason).toBe("expired");37  });38  it("double soumission concurrente : une seule réussit", async () => {39    const r = await requestCode("test5@exemple.ca", "1.2.3.4"); if (!r.ok) throw new Error(); const code = codeFromMail();40    const results = await Promise.all([verifyCode(r.challengeId, code, "1.2.3.4", true), verifyCode(r.challengeId, code, "1.2.3.4", true), verifyCode(r.challengeId, code, "1.2.3.4", true)]);41    expect(results.filter((x) => x.ok).length).toBe(1);42    expect((await db().select().from(users)).length).toBe(1);43  });44  it("limite les demandes par adresse (5/h) sans révéler l'existence du compte", async () => {45    let limited = 0;46    for (let i = 0; i < 7; i++) { await db().update(otpChallenges).set({ createdAt: new Date(Date.now() - 120000) }); const r = await requestCode("spam@exemple.ca", `9.9.9.${i}`); if (!r.ok && r.reason === "rate_limited") limited++; }47    expect(limited).toBeGreaterThan(0);48  });49});50