Playwright · 2FA
Le code arrive par email, ou il se calcule
Un parcours à double authentification bloque une suite de tests à deux endroits : le code envoyé par email, que le test ne peut pas lire, et celui de l’application d’authentification, qu’il ne peut pas générer. Les deux se règlent, et pas de la même façon.
Le code reçu par email
Même mécanique que pour une inscription : la requête part avant le clic et répond à l’arrivée du message.
import { expect, test } from '@playwright/test';
const KEY = process.env.FACTEUR_API_KEY!;
const INBOX = 'j3k9x2mq';
/** hasOtp vise le code extrait, pas un texte qui le cite. */
const attendreLeCode = (sentTo: string) =>
fetch('https://api.facteur.eu/v1/messages/search', {
method: 'POST',
headers: {
authorization: `Bearer ${KEY}`,
'content-type': 'application/json',
},
body: JSON.stringify({ inbox: INBOX, sentTo, hasOtp: true, wait: 30_000 }),
}).then((response) => response.json());
test('la connexion demande le code reçu par email', async ({ page }) => {
const tag = `login-${Date.now()}`;
const arrive = attendreLeCode(tag);
await page.goto('/connexion');
await page.getByLabel('Email').fill(`${tag}@${INBOX}.inbox.facteur.eu`);
await page.getByRole('button', { name: 'Continuer' }).click();
const { messages } = await arrive;
// Six chiffres, extraits à la réception.
await page.getByLabel('Code').fill(messages[0].otp);
await expect(page.getByText('Bienvenue')).toBeVisible();
});Un critère qui vise le code, pas le texte
Demander « le message qui porte un code » évite de retomber sur un email qui cite un ancien code dans son historique.
Le code vit dans son propre champ
Six chiffres extraits à la réception. Votre test compare une valeur, il ne cherche pas un motif dans du HTML.
Le code d’une application d’authentification
Celui-là, personne ne peut vous l’envoyer : il se calcule à partir du secret que votre écran d’activation affiche. Vingt lignes de Node, aucune dépendance, et le téléphone reste dans le tiroir.
import { createHmac } from 'node:crypto';
const BASE32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
/** Le secret que votre écran d'activation affiche, en base32. */
function decode(secret: string): Buffer {
let bits = 0;
let value = 0;
const out: number[] = [];
for (const c of secret.replace(/=+$/, '').toUpperCase()) {
value = (value << 5) | BASE32.indexOf(c);
bits += 5;
if (bits >= 8) {
out.push((value >>> (bits - 8)) & 0xff);
bits -= 8;
}
}
return Buffer.from(out);
}
/** RFC 6238 : SHA-1, six chiffres, pas de trente secondes. */
export function totp(secret: string, at = Date.now()): string {
const counter = Math.floor(at / 1000 / 30);
const message = Buffer.alloc(8);
message.writeUInt32BE(Math.floor(counter / 2 ** 32), 0);
message.writeUInt32BE(counter % 2 ** 32, 4);
const digest = createHmac('sha1', decode(secret)).update(message).digest();
const offset = digest[digest.length - 1] & 0x0f;
const binary =
((digest[offset] & 0x7f) << 24) |
(digest[offset + 1] << 16) |
(digest[offset + 2] << 8) |
digest[offset + 3];
return String(binary % 1_000_000).padStart(6, '0');
}Ce que ça change pour une suite de tests
Le parcours complet est testable
Activation de la double authentification, connexion, code de secours : rien ne dépend plus d’un appareil physique ni d’une boîte mail personnelle.
Le test reste lisible
Deux appels et une assertion. Le code n’est pas extrait par vous, et l’attente n’est pas une boucle que vous maintenez.
Les exécutions parallèles ne se marchent pas dessus
Chaque test reçoit à son adresse, donc le code lu est celui de son propre parcours.
Et un greffon Playwright ?
Prévu, pas publié. Le test ci-dessus n’attend rien de lui : il appelle l’API directement, et continuera de fonctionner ensuite.
Débloquez vos tests 2FA
Une boîte de test, une clé en lecture seule, et le parcours entier redevient testable. Le plan gratuit ne demande pas de carte bancaire.