fix: corriger le flux de réinitialisation par e-mail
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 2m22s
This commit is contained in:
51
server/__tests__/passwordResetEmailTemplate.test.ts
Normal file
51
server/__tests__/passwordResetEmailTemplate.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { replaceEmailVariables } from "../emailTemplateUtils";
|
||||
|
||||
const getEmailTemplateByType = vi.fn();
|
||||
|
||||
vi.mock("../db", () => ({ getEmailTemplateByType }));
|
||||
|
||||
const databaseHelpers = readFileSync(new URL("../db.ts", import.meta.url), "utf8");
|
||||
const emailService = readFileSync(new URL("../emailService.ts", import.meta.url), "utf8");
|
||||
|
||||
describe("modèle d’e-mail de réinitialisation", () => {
|
||||
it("conserve un lien unique sous forme de variable dans le modèle par défaut", () => {
|
||||
expect(databaseHelpers).toContain('type: "reset_password"');
|
||||
expect(databaseHelpers).toContain("{{lienReinitialisation}}");
|
||||
expect(emailService).toContain("lienReinitialisation: params.resetLink");
|
||||
});
|
||||
|
||||
it("substitue le lien de réinitialisation dans le contenu du modèle", () => {
|
||||
const link = "https://formations.recette.santinova-soft.org/reset-password?token=abc123";
|
||||
const result = replaceEmailVariables(
|
||||
'<a href="{{lienReinitialisation}}">Réinitialiser</a>',
|
||||
{ lienReinitialisation: link },
|
||||
);
|
||||
|
||||
expect(result).toContain(`href="${link}"`);
|
||||
expect(result).not.toContain("{{lienReinitialisation}}");
|
||||
});
|
||||
|
||||
it("ajoute le contenu de secours lorsque le modèle existant ne contient pas de lien", async () => {
|
||||
getEmailTemplateByType.mockResolvedValue({
|
||||
type: "reset_password",
|
||||
titre: "Réinitialisation",
|
||||
bodyContent: "<p>Réinitialisation mot de passe</p>",
|
||||
couleurPrincipale: "#283581",
|
||||
couleurSecondaire: "#0578BE",
|
||||
piedDePage: null,
|
||||
});
|
||||
|
||||
const { generateEmailFromTemplate } = await import("../emailTemplateGenerator");
|
||||
const resetLink = "https://formations.recette.santinova-soft.org/reset-password?token=abc123";
|
||||
const html = await generateEmailFromTemplate(
|
||||
"reset_password",
|
||||
`<a href="${resetLink}">Réinitialiser</a>`,
|
||||
{ lienReinitialisation: resetLink },
|
||||
);
|
||||
|
||||
expect(html).toContain("Réinitialisation mot de passe");
|
||||
expect(html).toContain(`href="${resetLink}"`);
|
||||
});
|
||||
});
|
||||
66
server/__tests__/passwordResetTokenContract.test.ts
Normal file
66
server/__tests__/passwordResetTokenContract.test.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getDb, createPasswordResetToken, getPasswordResetToken, markTokenAsUsed } from "../db";
|
||||
import { passwordResetTokens, users } from "../../drizzle/schema";
|
||||
|
||||
const schema = readFileSync(
|
||||
new URL("../../drizzle/schema.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const databaseHelpers = readFileSync(
|
||||
new URL("../db.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const router = readFileSync(
|
||||
new URL("../routers.ts", import.meta.url),
|
||||
"utf8",
|
||||
);
|
||||
const passwordResetTokenSchema = schema.match(
|
||||
/export const passwordResetTokens = mysqlTable\([\s\S]*?\n\}\);/,
|
||||
)?.[0];
|
||||
|
||||
describe("contrat des tokens de réinitialisation", () => {
|
||||
it("référence un utilisateur et non une colonne email absente de la table déployée", () => {
|
||||
expect(passwordResetTokenSchema).toContain('userId: int("userId").notNull()');
|
||||
expect(passwordResetTokenSchema).not.toContain('email: varchar("email", { length: 320 }).notNull()');
|
||||
expect(databaseHelpers).toContain(
|
||||
"createPasswordResetToken(userId: number, token: string)",
|
||||
);
|
||||
expect(databaseHelpers).toContain("userId,");
|
||||
});
|
||||
|
||||
it("utilise l’identifiant utilisateur dans les parcours public et administrateur", () => {
|
||||
expect(router).toContain("db.createPasswordResetToken(utilisateur.id, token)");
|
||||
expect(router).toContain("db.createPasswordResetToken(user.id, token)");
|
||||
expect(router).toContain("db.getUserById(resetToken.userId)");
|
||||
});
|
||||
|
||||
it("ne génère plus de lien de réinitialisation sur un domaine fictif", () => {
|
||||
expect(router).not.toContain("https://votre-domaine.com/reset-password");
|
||||
});
|
||||
|
||||
it("insère et consomme un token lié à un utilisateur sans dépendre d’un e-mail", async () => {
|
||||
const db = await getDb();
|
||||
if (!db) throw new Error("Base de test indisponible");
|
||||
|
||||
const [user] = await db.select({ id: users.id }).from(users).limit(1);
|
||||
if (!user) throw new Error("Utilisateur de test introuvable");
|
||||
|
||||
const token = `test-${randomUUID()}`;
|
||||
try {
|
||||
await createPasswordResetToken(user.id, token);
|
||||
|
||||
const created = await getPasswordResetToken(token);
|
||||
expect(created?.userId).toBe(user.id);
|
||||
expect(created?.used).toBe(false);
|
||||
|
||||
await markTokenAsUsed(created!.id);
|
||||
const consumed = await getPasswordResetToken(token);
|
||||
expect(consumed?.used).toBe(true);
|
||||
} finally {
|
||||
await db.delete(passwordResetTokens).where(eq(passwordResetTokens.token, token));
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user