> ## Documentation Index
> Fetch the complete documentation index at: https://docs-staging-automated-sdk-version-update.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Tester unitairement une action qui redirige l'utilisateur

> Tests unitaires pour une action Password Reset Post Challenge qui redirige les utilisateurs à risque élevé vers une vérification d'identité et qui valide le jeton retourné avant de poursuivre, avec Jest, Mocha et le Node.js Test Runner, en JavaScript et en TypeScript.

<h2 id="project">
  Projet
</h2>

Chaque exemple sépare le code source de l'action, placé dans `src/`, de ses tests unitaires, placés dans `test/`, en suivant la même structure pour les projets JavaScript et TypeScript.

<Tabs>
  <Tab title="JavaScript">
    <Tree>
      <Tree.Folder name="/" defaultOpen>
        <Tree.Folder name="src" />

        <Tree.Folder name="test" />
      </Tree.Folder>
    </Tree>
  </Tab>

  <Tab title="TypeScript">
    <Tree>
      <Tree.Folder name="/" defaultOpen>
        <Tree.Folder name="src" />

        <Tree.Folder name="test">
          <Tree.Folder name="test-utils" />
        </Tree.Folder>
      </Tree.Folder>
    </Tree>
  </Tab>
</Tabs>

<h2 id="action">
  Action
</h2>

L'action Password Reset Post Challenge suivante redirige l'utilisateur vers une page externe de vérification d'identité lorsque son pointage de risque dépasse un seuil donné, puis valide le jeton de session renvoyé ainsi que le résultat de la vérification avant de laisser la réinitialisation du mot de passe se poursuivre.

<Tabs>
  <Tab title="JavaScript">
    ```js title="mock-redirects.js" theme={null}
    /** @import {Event, PasswordResetPostChallengeAPI} from "@auth0/actions/password-reset-post-challenge/v1" */

    const VERIFICATION_URL = 'https://verify.example.com/identity';
    const RISK_SCORE_THRESHOLD = 50;

    /**
    * Gestionnaire appelé pendant l’exécution d’un flux PasswordResetPostChallenge.
    * Redirige l’utilisateur vers une page externe de vérification d’identité lorsque le score
    * d’évaluation complémentaire des risques dépasse le seuil configuré.
    *
    * @param {Event} event - Détails sur l’utilisateur et la transaction de réinitialisation du mot de passe.
    * @param {PasswordResetPostChallengeAPI} api - Interface dont les méthodes permettent de modifier le comportement de la réinitialisation du mot de passe.
    */
    exports.onExecutePostChallenge = async (event, api) => {
      const riskScore = event.authentication.riskAssessment?.supplemental?.akamai?.akamaiUserRisk?.score ?? 0;

      if (riskScore < RISK_SCORE_THRESHOLD) {
        return;
      }

      const sessionToken = api.redirect.encodeToken({
        secret: event.secrets.REDIRECT_SECRET,
        payload: { userId: event.user.user_id },
      });

      api.redirect.sendUserTo(VERIFICATION_URL, {
        query: { session_token: sessionToken },
      });
    };

    /**
    * Gestionnaire appelé lorsque l’utilisateur revient de la redirection effectuée par
    * onExecutePostChallenge. Valide le jeton de session avant de permettre au flux de
    * réinitialisation du mot de passe de se poursuivre.
    *
    * @param {Event} event - Détails sur l’utilisateur et la transaction de réinitialisation du mot de passe.
    * @param {PasswordResetPostChallengeAPI} api - Interface dont les méthodes permettent de modifier le comportement de la réinitialisation du mot de passe.
    */
    exports.onContinuePostChallenge = async (event, api) => {
      let payload;

      try {
        payload = api.redirect.validateToken({ secret: event.secrets.REDIRECT_SECRET });
      } catch (err) {
        api.access.deny(`Unable to verify redirect response: ${err.message}`);
        return;
      }

      if (payload.userId !== event.user.user_id) {
        api.access.deny('Verification response does not match the current user.');
        return;
      }

      if (event.request.query.verified !== 'true') {
        api.access.deny('Identity verification was not completed.');
      }
    };

    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts title="mock-redirects.ts" theme={null}
    import type { Event, PasswordResetPostChallengeAPI } from '@auth0/actions/password-reset-post-challenge/v1';

    const VERIFICATION_URL = 'https://verify.example.com/identity';
    const RISK_SCORE_THRESHOLD = 50;

    /**
    * Gestionnaire qui sera appelé lors de l'exécution d'un flux PasswordResetPostChallenge.
    * Redirige l'utilisateur vers une page externe de vérification d'identité lorsque le pointage
    * d'évaluation du risque supplémentaire dépasse le seuil configuré.
    *
    * @param {Event} event - Détails sur l'utilisateur et sur la transaction de réinitialisation du mot de passe.
    * @param {PasswordResetPostChallengeAPI} api - Interface dont les méthodes permettent de modifier le comportement de la réinitialisation du mot de passe.
    */
    exports.onExecutePostChallenge = async (event: Event, api: PasswordResetPostChallengeAPI) => {
      const riskScore = event.authentication.riskAssessment?.supplemental?.akamai?.akamaiUserRisk?.score ?? 0;

      if (riskScore < RISK_SCORE_THRESHOLD) {
        return;
      }

      const sessionToken = api.redirect.encodeToken({
        secret: event.secrets.REDIRECT_SECRET,
        payload: { userId: event.user.user_id },
      });

      api.redirect.sendUserTo(VERIFICATION_URL, {
        query: { session_token: sessionToken },
      });
    };

    /**
    * Gestionnaire qui sera appelé lorsque l'utilisateur revient de la redirection émise par
    * onExecutePostChallenge. Valide le jeton de session avant de laisser le flux de
    * réinitialisation du mot de passe se poursuivre.
    *
    * @param {Event} event - Détails sur l'utilisateur et sur la transaction de réinitialisation du mot de passe.
    * @param {PasswordResetPostChallengeAPI} api - Interface dont les méthodes permettent de modifier le comportement de la réinitialisation du mot de passe.
    */
    exports.onContinuePostChallenge = async (event: Event, api: PasswordResetPostChallengeAPI) => {
      let payload;

      try {
        payload = api.redirect.validateToken({ secret: event.secrets.REDIRECT_SECRET });
      } catch (err) {
        api.access.deny(`Unable to verify redirect response: ${(err as Error).message}`);
        return;
      }

      if (payload.userId !== event.user.user_id) {
        api.access.deny('Verification response does not match the current user.');
        return;
      }

      if (event.request.query.verified !== 'true') {
        api.access.deny('Identity verification was not completed.');
      }
    };

    ```
  </Tab>
</Tabs>

<h2 id="unit-test">
  Unit Test
</h2>

Les tests unitaires simulent les objets `event` et `api`, ainsi que l'encodage et la validation du jeton de redirection, afin de vérifier que la redirection ne se déclenche qu'au-delà du seuil de risque et que l'accès est refusé dans le cas de jetons invalides, d'utilisateurs non concordants ou d'une vérification incomplète.

<AccordionGroup>
  <Accordion title="Jest">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-redirects.spec.js" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            jest.resetAllMocks();
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            jest.spyOn(api.redirect, 'encodeToken').mockReturnValue('signed.jwt.token');
            jest.spyOn(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            expect(api.redirect.encodeToken).not.toHaveBeenCalled();
            expect(api.redirect.sendUserTo).not.toHaveBeenCalled();
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            expect(api.redirect.encodeToken).toHaveBeenCalledWith({
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            });
            expect(api.redirect.sendUserTo).toHaveBeenCalledWith(VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            });
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            jest.resetAllMocks();
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            jest.spyOn(api.access, 'deny');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockImplementation(() => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.redirect.validateToken).toHaveBeenCalledWith({ secret: 'test-secret' });
            expect(api.access.deny).toHaveBeenCalledWith('Unable to verify redirect response: invalid signature');
          });

          it('denies access when the token belongs to a different user', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: 'someone-else' });

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Verification response does not match the current user.');
          });

          it('denies access when the verification was not completed', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Identity verification was not completed.');
          });

          it('allows the flow to continue when verification succeeded', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).not.toHaveBeenCalled();
          });
        });

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-jest",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Jest",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "jest"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "jest": "^30.4.2"
          },
          "jest": {
            "testEnvironment": "node"
          }
        }

        ```

        Configurez `jsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-redirects.test.ts" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
        const path = require('path');
        const { compileActionModules } = require('./test-utils/load-compiled-action');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.ts');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            jest.resetAllMocks();
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            jest.spyOn(api.redirect, 'encodeToken').mockReturnValue('signed.jwt.token');
            jest.spyOn(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            expect(api.redirect.encodeToken).not.toHaveBeenCalled();
            expect(api.redirect.sendUserTo).not.toHaveBeenCalled();
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            expect(api.redirect.encodeToken).toHaveBeenCalledWith({
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            });
            expect(api.redirect.sendUserTo).toHaveBeenCalledWith(VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            });
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            jest.resetAllMocks();
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            jest.spyOn(api.access, 'deny');
          });

          afterEach(() => {
            jest.resetAllMocks();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockImplementation(() => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.redirect.validateToken).toHaveBeenCalledWith({ secret: 'test-secret' });
            expect(api.access.deny).toHaveBeenCalledWith('Unable to verify redirect response: invalid signature');
          });

          it('denies access when the token belongs to a different user', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: 'someone-else' });

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Verification response does not match the current user.');
          });

          it('denies access when the verification was not completed', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).toHaveBeenCalledWith('Identity verification was not completed.');
          });

          it('allows the flow to continue when verification succeeded', async () => {
            jest.spyOn(api.redirect, 'validateToken').mockReturnValue({ userId: event.user.user_id });
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            expect(api.access.deny).not.toHaveBeenCalled();
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        import * as fs from 'fs';
        import * as os from 'os';
        import * as path from 'path';
        import * as ts from 'typescript';

        export interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute au moyen de vm.compileFunction; il ne passe donc jamais par la
         * transformation TS de ts-node/Vitest. Les sources d'actions (et tout module
         * enregistré avec actions:) doivent d'abord être transpilées en JS ordinaire sur le disque.
         */
        export function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        }

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-jest",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Jest and TypeScript",
          "main": "example.ts",
          "scripts": {
            "test": "jest"
          },
          "author": "Auth0",
          "license": "MIT",
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/jest": "^29.5.12",
            "@types/node": "22.14.0",
            "jest": "^29.7.0",
            "ts-jest": "^29.1.2",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez Jest pour qu'il compile le TypeScript à l'aide de `jest.config.js` :

        ```js title="jest.config.js" theme={null}
        module.exports = {
          preset: 'ts-jest',
          testEnvironment: 'node',
        };
        ```

        Configurez `tsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Mocha">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-redirects.spec.js" theme={null}
        const { expect } = require('chai');
        const sinon = require('sinon');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            sinon.stub(api.redirect, 'encodeToken').returns('signed.jwt.token');
            sinon.spy(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            sinon.assert.notCalled(api.redirect.encodeToken);
            sinon.assert.notCalled(api.redirect.sendUserTo);
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            sinon.assert.calledWith(api.redirect.encodeToken, {
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            });
            sinon.assert.calledWith(api.redirect.sendUserTo, VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            });
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            sinon.spy(api.access, 'deny');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            sinon.stub(api.redirect, 'validateToken').callsFake(() => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.redirect.validateToken, { secret: 'test-secret' });
            sinon.assert.calledWith(api.access.deny, 'Unable to verify redirect response: invalid signature');
          });

          it('denies access when the token belongs to a different user', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: 'someone-else' });

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.access.deny, 'Verification response does not match the current user.');
          });

          it('denies access when the verification was not completed', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.access.deny, 'Identity verification was not completed.');
          });

          it('allows the flow to continue when verification succeeded', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.notCalled(api.access.deny);
          });
        });

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-mocha",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Mocha",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "mocha"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "chai": "^4.5.0",
            "mocha": "^11.0.0",
            "sinon": "^19.0.0"
          }
        }

        ```

        Configurez Mocha avec `.mocharc.json` :

        ```json title=".mocharc.json" theme={null}
        {
          "spec": "src/**/*.spec.js"
        }

        ```

        Configurez `jsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-redirects.test.ts" theme={null}
        import * as path from 'path';
        import sinon from 'sinon';
        import { compileActionModules } from './test-utils/load-compiled-action';

        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.ts');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            sinon.stub(api.redirect, 'encodeToken').returns('signed.jwt.token');
            sinon.spy(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            sinon.assert.notCalled(api.redirect.encodeToken);
            sinon.assert.notCalled(api.redirect.sendUserTo);
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            sinon.assert.calledWith(api.redirect.encodeToken, {
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            });
            sinon.assert.calledWith(api.redirect.sendUserTo, VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            });
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader: any;
          let event: any;
          let api: any;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            sinon.spy(api.access, 'deny');
          });

          afterEach(() => {
            sinon.restore();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            sinon.stub(api.redirect, 'validateToken').callsFake(() => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.redirect.validateToken, { secret: 'test-secret' });
            sinon.assert.calledWith(api.access.deny, 'Unable to verify redirect response: invalid signature');
          });

          it('denies access when the token belongs to a different user', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: 'someone-else' });

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.access.deny, 'Verification response does not match the current user.');
          });

          it('denies access when the verification was not completed', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.calledWith(api.access.deny, 'Identity verification was not completed.');
          });

          it('allows the flow to continue when verification succeeded', async () => {
            sinon.stub(api.redirect, 'validateToken').returns({ userId: event.user.user_id });
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            sinon.assert.notCalled(api.access.deny);
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        import * as fs from 'fs';
        import * as os from 'os';
        import * as path from 'path';
        import * as ts from 'typescript';

        export interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute au moyen de vm.compileFunction : il ne passe donc jamais par la transformation
         * TS propre à ts-node/Vitest. Les sources des actions (et tout module enregistré avec
         * actions:) doivent d'abord être transpilées en JS pur sur le disque.
         */
        export function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        }

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-mocha",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using Mocha and TypeScript",
          "license": "MIT",
          "author": "Auth0",
          "scripts": {
            "test": "NODE_OPTIONS=--no-experimental-strip-types mocha"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/chai": "^4.3.16",
            "@types/mocha": "^10.0.6",
            "@types/node": "22.14.0",
            "@types/sinon": "^17.0.3",
            "chai": "^4.5.0",
            "mocha": "^11.0.0",
            "sinon": "^19.0.0",
            "ts-node": "^10.9.2",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez Mocha avec `.mocharc.json` :

        ```json title=".mocharc.json" theme={null}
        {
          "require": "ts-node/register",
          "extension": ["ts"],
          "spec": "src/**/*.test.ts"
        }

        ```

        Configurez `tsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>

  <Accordion title="Node.js Test Runner">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="mock-redirects.spec.js" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.js');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            mock.method(api.redirect, 'encodeToken', () => 'signed.jwt.token');
            mock.method(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            mock.reset();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            assert.strictEqual(api.redirect.encodeToken.mock.calls.length, 0);
            assert.strictEqual(api.redirect.sendUserTo.mock.calls.length, 0);
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            assert.deepEqual(api.redirect.encodeToken.mock.calls[0].arguments, [{
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            }]);
            assert.deepEqual(api.redirect.sendUserTo.mock.calls[0].arguments, [VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            }]);
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            loader = await loadAction(ACTION_PATH);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            mock.method(api.access, 'deny');
          });

          afterEach(() => {
            mock.reset();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            mock.method(api.redirect, 'validateToken', () => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.redirect.validateToken.mock.calls[0].arguments, [{ secret: 'test-secret' }]);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Unable to verify redirect response: invalid signature']);
          });

          it('denies access when the token belongs to a different user', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: 'someone-else' }));

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Verification response does not match the current user.']);
          });

          it('denies access when the verification was not completed', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Identity verification was not completed.']);
          });

          it('allows the flow to continue when verification succeeded', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });
        });

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-js-node-test",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using the Node.js built-in test runner",
          "license": "MIT",
          "author": "Auth0",
          "type": "commonjs",
          "main": "module-usage.js",
          "scripts": {
            "test": "node --test src/*.spec.js"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0"
          }
        }

        ```

        Configurez `jsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="jsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "commonjs",
            "checkJs": false,
            "baseUrl": ".",
            "paths": {
              "actions:*": [
                "src/*"
              ]
            }
          },
          "include": [
            "src/**/*.js"
          ]
        }

        ```
      </Tab>

      <Tab title="TypeScript">
        ```ts title="mock-redirects.test.ts" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/password-reset-post-challenge/v1/test');
        const path = require('path');
        const { compileActionModules } = require('./test-utils/load-compiled-action.ts');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/mock-redirects.ts');
        const VERIFICATION_URL = 'https://verify.example.com/identity';

        describe('onExecutePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            mock.method(api.redirect, 'encodeToken', () => 'signed.jwt.token');
            mock.method(api.redirect, 'sendUserTo');
          });

          afterEach(() => {
            mock.reset();
          });

          it('does not redirect when the risk score is below the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 10;

            await loader.execute('onExecutePostChallenge', event, api);

            assert.strictEqual(api.redirect.encodeToken.mock.calls.length, 0);
            assert.strictEqual(api.redirect.sendUserTo.mock.calls.length, 0);
          });

          it('redirects to the identity verification page when the risk score exceeds the threshold', async () => {
            event.authentication.riskAssessment.supplemental.akamai.akamaiUserRisk.score = 80;

            await loader.execute('onExecutePostChallenge', event, api);

            assert.deepEqual(api.redirect.encodeToken.mock.calls[0].arguments, [{
              secret: 'test-secret',
              payload: { userId: event.user.user_id },
            }]);
            assert.deepEqual(api.redirect.sendUserTo.mock.calls[0].arguments, [VERIFICATION_URL, {
              query: { session_token: 'signed.jwt.token' },
            }]);
          });
        });

        describe('onContinuePostChallenge', () => {
          let loader;
          let event;
          let api;

          beforeEach(async () => {
            const { compiledActionPath } = compileActionModules(ACTION_PATH);
            loader = await loadAction(compiledActionPath);
            [event, api] = getDefaultArguments();
            event.secrets.REDIRECT_SECRET = 'test-secret';
            mock.method(api.access, 'deny');
          });

          afterEach(() => {
            mock.reset();
          });

          it('denies access when the redirect token cannot be validated', async () => {
            mock.method(api.redirect, 'validateToken', () => {
              throw new Error('invalid signature');
            });

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.redirect.validateToken.mock.calls[0].arguments, [{ secret: 'test-secret' }]);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Unable to verify redirect response: invalid signature']);
          });

          it('denies access when the token belongs to a different user', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: 'someone-else' }));

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Verification response does not match the current user.']);
          });

          it('denies access when the verification was not completed', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
            event.request.query.verified = 'false';

            await loader.execute('onContinuePostChallenge', event, api);

            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Identity verification was not completed.']);
          });

          it('allows the flow to continue when verification succeeded', async () => {
            mock.method(api.redirect, 'validateToken', () => ({ userId: event.user.user_id }));
            event.request.query.verified = 'true';

            await loader.execute('onContinuePostChallenge', event, api);

            assert.strictEqual(api.access.deny.mock.calls.length, 0);
          });
        });

        ```

        Cet utilitaire de test compile l'action TypeScript dans un fichier temporaire afin qu'elle puisse être chargée à l'exécution :

        ```ts title="load-compiled-action.ts" theme={null}
        const fs = require('fs');
        const os = require('os');
        const path = require('path');
        const ts = require('typescript');

        interface ModuleToCompile {
          name: string;
          filename: string;
        }

        function transpileToTemp(sourcePath: string): string {
          const source = fs.readFileSync(sourcePath, 'utf8');
          const { outputText } = ts.transpileModule(source, {
            compilerOptions: {
              module: ts.ModuleKind.CommonJS,
              target: ts.ScriptTarget.ES2020,
              esModuleInterop: true,
            },
          });

          const tempPath = path.join(
            os.tmpdir(),
            `${path.basename(sourcePath, path.extname(sourcePath))}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.js`,
          );
          fs.writeFileSync(tempPath, outputText);
          return tempPath;
        }

        /**
         * loadAction() (de @auth0/actions/*\/test) lit son fichier cible sur le disque et
         * l'exécute avec vm.compileFunction; il ne passe donc jamais par le retrait
         * natif des types TS de node. Les sources d'actions (et tout module enregistré
         * avec actions:) doivent d'abord être transpilées en JS pur sur le disque.
         */
        exports.compileActionModules = function compileActionModules(actionPath: string, modules: ModuleToCompile[] = []) {
          const compiledActionPath = transpileToTemp(actionPath);
          const compiledModules = modules.map((m: ModuleToCompile) => ({
            name: m.name,
            filename: transpileToTemp(m.filename),
          }));

          return { compiledActionPath, compiledModules };
        };

        ```

        Ajoutez les dépendances de test à `package.json` :

        ```json title="package.json" theme={null}
        {
          "name": "actions-npm-example-ts-node-test",
          "version": "1.0.0",
          "description": "Auth0 Actions unit testing example using the Node.js built-in test runner and TypeScript",
          "license": "MIT",
          "author": "Auth0",
          "scripts": {
            "test": "node --test src/*.test.ts"
          },
          "devDependencies": {
            "@auth0/actions": "^0.33.0",
            "@types/node": "22.14.0",
            "typescript": "^5.9.2"
          }
        }

        ```

        Configurez `tsconfig.json` de sorte que l'alias d'importation `actions:` pointe vers `src/` :

        ```json title="tsconfig.json" theme={null}
        {
          "compilerOptions": {
            "target": "ES2020",
            "module": "NodeNext",
            "moduleResolution": "nodenext",
            "esModuleInterop": true,
            "allowSyntheticDefaultImports": true,
            "strict": true,
            "outDir": "dist",
            "declaration": true,
            "sourceMap": true,
            "allowJs": true,
            "checkJs": false,
            "resolveJsonModule": true,
            "skipLibCheck": true,
            "forceConsistentCasingInFileNames": true,
            "isolatedModules": true,
            "noEmit": true,
            "paths": {
              "actions:*": [
                "./src/*"
              ]
            }
          },
          "exclude": [
            "node_modules",
            "dist"
          ],
          "include": [
            "**/*.ts"
          ],
          "ts-node": {
            "transpileOnly": true
          }
        }

        ```
      </Tab>
    </Tabs>
  </Accordion>
</AccordionGroup>
