# Tester un code OTP avec Playwright · Facteur

> Tester une double authentification avec Playwright : lire le code reçu par e-mail, et calculer celui d'une application d'authentification en vingt lignes de Node.
> https://facteur.eu/fr/tester-un-code-otp-avec-playwright

/ Playwright · 2FA

#  Le code arrive par e-mail, ou il se calcule 

 Un parcours à double authentification bloque une suite de tests à deux endroits : le code envoyé par e-mail, 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 guide complet sur le portail ](https://developers.facteur.eu/fr/guides/playwright/) [ Voir l'état des paquets ](/fr/integrations) 

 

 Le code reçu par e-mail

## La requête part avant le clic

Même mécanique que pour une inscription : la requête part avant le clic et répond à l'arrivée du message.

 

       connexion-2fa.spec.ts TypeScript 

 

```
<span class="hl-keyword">import</span> { expect, test } <span class="hl-keyword">from</span> <span class="hl-value">'@playwright/test'</span>;

<span class="hl-keyword">const</span> INBOX = <span class="hl-value">'j3k9x2mq'</span>;

<span class="hl-comment">// hasOtp vise le code extrait, pas un texte qui le cite.</span>
<span class="hl-keyword">async</span> <span class="hl-keyword">function</span> <span class="hl-property">attendreLeCode</span>(<span class="hl-property">sentTo</span>: <span class="hl-type">string</span>) {
  <span class="hl-keyword">const</span> search = <span class="hl-keyword">new</span> <span class="hl-type">URLSearchParams</span>({ <span class="hl-property">inbox</span>: <span class="hl-type">INBOX</span>, sentTo, <span class="hl-property">hasOtp</span>: <span class="hl-value">'true'</span>, <span class="hl-property">wait</span>: <span class="hl-value">'30000'</span> });
  <span class="hl-keyword">const</span> response = <span class="hl-keyword">await</span> <span class="hl-property">fetch</span>(<span class="hl-value">'https://api.facteur.eu/v1/messages?'</span> + search, {
    <span class="hl-property">headers</span>: { <span class="hl-property">authorization</span>: <span class="hl-value">'Bearer '</span> + process.<span class="hl-property">env</span>.<span class="hl-property">FACTEUR_API_KEY</span> },
  });
  <span class="hl-keyword">const</span> { messages } = <span class="hl-keyword">await</span> response.<span class="hl-property">json</span>();
  <span class="hl-keyword">if</span> (!messages[0]) <span class="hl-keyword">throw</span> <span class="hl-keyword">new</span> <span class="hl-type">Error</span>(<span class="hl-value">'Aucun code pour '</span> + sentTo);
  <span class="hl-keyword">return</span> messages[0].<span class="hl-property">otp</span>;
}

<span class="hl-property">test</span>(<span class="hl-value">'la connexion demande le code reçu par e-mail'</span>, <span class="hl-keyword">async</span> ({ page }) => {
  <span class="hl-keyword">const</span> tag = <span class="hl-value">'login-'</span> + <span class="hl-type">Date</span>.<span class="hl-property">now</span>();
  <span class="hl-keyword">const</span> arrive = <span class="hl-property">attendreLeCode</span>(tag);

  <span class="hl-keyword">await</span> page.<span class="hl-property">goto</span>(<span class="hl-value">'/connexion'</span>);
  <span class="hl-keyword">await</span> page.<span class="hl-property">getByLabel</span>(<span class="hl-value">'Email'</span>).<span class="hl-property">fill</span>(tag + <span class="hl-value">'@'</span> + INBOX + <span class="hl-value">'.inbox.facteur.eu'</span>);
  <span class="hl-keyword">await</span> page.<span class="hl-property">getByRole</span>(<span class="hl-value">'button'</span>, { <span class="hl-property">name</span>: <span class="hl-value">'Continuer'</span> }).<span class="hl-property">click</span>();

  <span class="hl-comment">// Le code, extrait à la réception.</span>
  <span class="hl-keyword">await</span> page.<span class="hl-property">getByLabel</span>(<span class="hl-value">'Code'</span>).<span class="hl-property">fill</span>(<span class="hl-keyword">await</span> arrive);
  <span class="hl-keyword">await</span> <span class="hl-property">expect</span>(page.<span class="hl-property">getByText</span>(<span class="hl-value">'Bienvenue'</span>)).<span class="hl-property">toBeVisible</span>();
});
```

  

  Un critère qui vise le code, pas le texte Le critère hasOtp demande « le message qui porte un code », ce qui évite de retomber sur un e-mail qui cite un ancien code dans son historique. 

 Le code vit dans son propre champ Extrait à la réception. Votre test compare une valeur, il ne cherche pas un motif dans du HTML. 

  

 Le code d'une application d'authentification

## Vingt lignes de Node, aucune dépendance

Celui-là, personne ne peut vous l'envoyer : il se calcule à partir du secret que votre écran d'activation affiche. Facteur n'y joue aucun rôle, et le téléphone reste dans le tiroir.

 

       totp.ts TypeScript 

 

```
<span class="hl-keyword">import</span> { createHmac } <span class="hl-keyword">from</span> <span class="hl-value">'node:crypto'</span>;

<span class="hl-keyword">const</span> BASE32 = <span class="hl-value">'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'</span>;

<span class="hl-comment">/** Le secret que votre écran d'activation affiche, en base32. */</span>
<span class="hl-keyword">function</span> <span class="hl-property">decode</span>(<span class="hl-property">secret</span>: <span class="hl-type">string</span>): <span class="hl-type">Buffer</span> {
  <span class="hl-keyword">let</span> bits = 0;
  <span class="hl-keyword">let</span> value = 0;
  <span class="hl-keyword">const</span> <span class="hl-property">out</span>: <span class="hl-type">number</span>[] = [];
  <span class="hl-keyword">for</span> (<span class="hl-keyword">const</span> c <span class="hl-keyword">of</span> secret.<span class="hl-property">replace</span>(/=/g, <span class="hl-value">''</span>).<span class="hl-property">toUpperCase</span>()) {
    value = (value << 5) | <span class="hl-type">BASE32</span>.<span class="hl-property">indexOf</span>(c);
    bits += 5;
    <span class="hl-keyword">if</span> (bits >= 8) {
      out.<span class="hl-property">push</span>((value >>> (bits - 8)) & 0xff);
      bits -= 8;
    }
  }
  <span class="hl-keyword">return</span> <span class="hl-type">Buffer</span>.<span class="hl-property">from</span>(out);
}

<span class="hl-comment">/** RFC 6238 : SHA-1, six chiffres, pas de trente secondes. */</span>
<span class="hl-keyword">export</span> <span class="hl-keyword">function</span> <span class="hl-property">totp</span>(<span class="hl-property">secret</span>: <span class="hl-type">string</span>, at = <span class="hl-type">Date</span>.<span class="hl-property">now</span>()): <span class="hl-type">string</span> {
  <span class="hl-keyword">const</span> counter = <span class="hl-type">Math</span>.<span class="hl-property">floor</span>(at / 1000 / 30);
  <span class="hl-keyword">const</span> message = <span class="hl-type">Buffer</span>.<span class="hl-property">alloc</span>(8);
  message.<span class="hl-property">writeUInt32BE</span>(<span class="hl-type">Math</span>.<span class="hl-property">floor</span>(counter / 2 ** 32), 0);
  message.<span class="hl-property">writeUInt32BE</span>(counter % 2 ** 32, 4);

  <span class="hl-keyword">const</span> digest = <span class="hl-property">createHmac</span>(<span class="hl-value">'sha1'</span>, <span class="hl-property">decode</span>(secret)).<span class="hl-property">update</span>(message).<span class="hl-property">digest</span>();
  <span class="hl-keyword">const</span> offset = digest[digest.<span class="hl-property">length</span> - 1] & 0x0f;
  <span class="hl-keyword">const</span> binary =
    ((digest[offset] & 0x7f) << 24) |
    (digest[offset + 1] << 16) |
    (digest[offset + 2] << 8) |
    digest[offset + 3];

  <span class="hl-keyword">return</span> <span class="hl-property">String</span>(binary % 1_000_000).<span class="hl-property">padStart</span>(6, <span class="hl-value">'0'</span>);
}
```

  

 

 Pourquoi

## Ce que ça change pour une suite de tests

 

01

### 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 aux lettres personnelle.

 

02

### 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.

 

03

### 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.

 



 

 

 Le plugin Playwright

## Écrit, pas encore publié

playwright-facteur sait attendre un message qui porte un code. Il n'est pas encore publié sur npm. Le test ci-dessus n'attend rien de lui : il appelle l'API directement, et continuera de marcher tel quel.

 

 [ Voir l'état des paquets ](/fr/integrations) 

 

 

## Débloquez vos tests de double authentification

Une inbox, une clé en lecture seule, et le parcours entier redevient testable. L'offre gratuite ne demande pas de carte bancaire.

 [ Créer un compte gratuit ](/fr/inscription) [ Comment migrer ](/fr/migrer)
