> ## 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 utilise un module

> Tests unitaires pour une action Post Login qui récupère les rôles d'un utilisateur auprès d'un service externe au moyen d'un module personnalisé et les ajoute comme demande personnalisée du jeton d'accès, en refusant l'accès en cas d'échec, avec Jest, Mocha et Node.js Test Runner en JavaScript et en TypeScript.

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

Chaque exemple sépare le code source de l'action, situé dans `src/`, de ses tests unitaires, situé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 Post Login suivante importe un module personnalisé pour récupérer les rôles de l'utilisateur auprès d'un service externe et les ajoute au jeton d'accès sous forme de demande personnalisée, en refusant l'accès si l'appel au module échoue.

<Tabs>
  <Tab title="JavaScript">
    ```js title="test-an-action-module.js" theme={null}
    /** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */

    const { getRoles } = require('actions:my-module');

    const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

    /**
    * Gestionnaire appelé pendant l’exécution d’un flux PostLogin.
    *
    * @param {Event} event - Renseignements sur l’utilisateur et le contexte dans lequel il se connecte.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onExecutePostLogin = async (event, api) => {
      try {
        const { roles } = await getRoles();

        api.accessToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
      } catch (err) {
        api.access.deny(err.message);
      }
    }
    ```

    L'action nécessite un Action Module nommé `my-module`, qui récupère les rôles auprès d'un service externe :

    ```js title="my-module.js" theme={null}
    module.exports = {
      /**
       * Retourne les rôles de l'utilisateur.
       *
       * @returns {{ roles: string[] }} La charge utile des rôles.
       */
      getRoles: async () => {
        const response = await fetch(actions.secrets.EVENT_SINK_URL, {
          method: 'GET',
          headers: {
            'Content-Type': 'application/json',
            'X-API-Key': actions.secrets.EVENT_SINK_API_KEY,
          }
        });

        if (!response.ok) {
          throw new Error(`External service responded with status ${response.status}`);
        }

        return response.json();
      }
    };
    ```
  </Tab>

  <Tab title="TypeScript">
    ```ts title="test-an-action-module.ts" theme={null}
    import type { Event, PostLoginAPI } from '@auth0/actions/post-login/v3';

    const { getRoles } = require('actions:my-module');

    const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

    /**
    * Gestionnaire appelé pendant l’exécution d’un flux PostLogin.
    *
    * @param {Event} event - Détails sur l’utilisateur et le contexte de sa connexion.
    * @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
    */
    exports.onExecutePostLogin = async (event: Event, api: PostLoginAPI) => {
      try {
        const { roles } = await getRoles();

        api.accessToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
      } catch (err) {
        api.access.deny((err as Error).message);
      }
    };

    ```

    L'action nécessite un Action Module nommé `my-module`, qui récupère les rôles auprès d'un service externe :

    ```ts title="my-module.ts" theme={null}
    /**
     * Renvoie les rôles de l’utilisateur.
     *
     * @returns La charge utile des rôles.
     */
    exports.getRoles = async (): Promise<{ roles: string[] }> => {
      const response = await fetch(actions.secrets.EVENT_SINK_URL, {
        method: 'GET',
        headers: {
          'Content-Type': 'application/json',
          'X-API-Key': actions.secrets.EVENT_SINK_API_KEY,
        },
      });

      if (!response.ok) {
        throw new Error(`External service responded with status ${response.status}`);
      }

      return response.json();
    };

    ```
  </Tab>
</Tabs>

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

Les tests unitaires chargent l'action avec le module personnalisé et simulent `fetch` afin de vérifier que les rôles sont bien attribués en cas de succès et que l'accès est refusé lorsque le service externe renvoie un statut d'erreur ou que la requête échoue en raison d'une erreur réseau.

<AccordionGroup>
  <Accordion title="Jest">
    <Tabs>
      <Tab title="JavaScript">
        ```js title="test-an-action-module.spec.js" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');

        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            jest.spyOn(api.accessToken, 'setCustomClaim');

            jest.spyOn(global, 'fetch').mockResolvedValueOnce({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            });

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.accessToken.setCustomClaim).toHaveBeenCalledWith(`${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
          });

          it('denies access when the external service responds with an error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            jest.spyOn(api.access, 'deny');

            jest.spyOn(global, 'fetch').mockResolvedValueOnce({
              ok: false,
              status: 500,
            });

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.access.deny).toHaveBeenCalledWith('External service responded with status 500');
          });

          it('denies access when fetching roles fails with a network error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            jest.spyOn(api.access, 'deny');

            jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.access.deny).toHaveBeenCalledWith('Network error');
          });
        });

        ```

        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` pour 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="test-an-action-module.test.ts" theme={null}
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');
        const { compileActionModules } = require('./test-utils/load-compiled-action');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.ts');
        const MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');

        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            jest.spyOn(api.accessToken, 'setCustomClaim');

            jest.spyOn(global, 'fetch').mockResolvedValueOnce({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            } as any);

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.accessToken.setCustomClaim).toHaveBeenCalledWith(`${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
          });

          it('denies access when the external service responds with an error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            jest.spyOn(api.access, 'deny');

            jest.spyOn(global, 'fetch').mockResolvedValueOnce({
              ok: false,
              status: 500,
            } as any);

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.access.deny).toHaveBeenCalledWith('External service responded with status 500');
          });

          it('denies access when fetching roles fails with a network error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            jest.spyOn(api.access, 'deny');

            jest.spyOn(global, 'fetch').mockRejectedValueOnce(new Error('Network error'));

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

            expect(global.fetch).toHaveBeenCalled();
            expect(api.access.deny).toHaveBeenCalledWith('Network error');
          });
        });

        ```

        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 compiler le TypeScript avec `jest.config.js` :

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

        Configurez `tsconfig.json` pour 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="test-an-action-module.spec.js" theme={null}
        const sinon = require('sinon');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');

        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            sinon.spy(api.accessToken, 'setCustomClaim');

            sinon.stub(global, 'fetch').resolves({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            });

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

            sinon.assert.called(global.fetch);
            sinon.assert.calledWith(api.accessToken.setCustomClaim, `${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
          });

          it('denies access when the external service responds with an error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            sinon.spy(api.access, 'deny');

            sinon.stub(global, 'fetch').resolves({
              ok: false,
              status: 500,
            });

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

            sinon.assert.called(global.fetch);
            sinon.assert.calledWith(api.access.deny, 'External service responded with status 500');
          });

          it('denies access when fetching roles fails with a network error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            sinon.spy(api.access, 'deny');

            sinon.stub(global, 'fetch').rejects(new Error('Network error'));

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

            sinon.assert.called(global.fetch);
            sinon.assert.calledWith(api.access.deny, 'Network error');
          });
        });

        ```

        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` pour 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="test-an-action-module.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/post-login/v3/test');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.ts');
        const MY_MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');
        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            sinon.spy(api.accessToken, 'setCustomClaim');

            sinon.stub(global, 'fetch').resolves({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            } as any);

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

            sinon.assert.called(global.fetch as any);
            sinon.assert.calledWith(api.accessToken.setCustomClaim, `${CUSTOM_CLAIM_NAMESPACE}/roles`, ['admin']);
          });

          it('denies access when the external service responds with an error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            sinon.spy(api.access, 'deny');

            sinon.stub(global, 'fetch').resolves({
              ok: false,
              status: 500,
            } as any);

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

            sinon.assert.called(global.fetch as any);
            sinon.assert.calledWith(api.access.deny, 'External service responded with status 500');
          });

          it('denies access when fetching roles fails with a network error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            sinon.spy(api.access, 'deny');

            sinon.stub(global, 'fetch').rejects(new Error('Network error'));

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

            sinon.assert.called(global.fetch as any);
            sinon.assert.calledWith(api.access.deny, 'Network error');
          });
        });

        ```

        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` pour 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="test-an-action-module.spec.js" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/test');
        const path = require('path');

        const DIRNAME = path.dirname('../');
        const ACTION_PATH = path.resolve(DIRNAME, './src/test-an-action-module.js');

        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            mock.method(api.accessToken, 'setCustomClaim');

            mock.method(global, 'fetch', async () => ({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            }));

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.accessToken.setCustomClaim.mock.calls[0].arguments, [
              `${CUSTOM_CLAIM_NAMESPACE}/roles`,
              ['admin'],
            ]);
          });

          it('denies access when the external service responds with an error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            mock.method(api.access, 'deny');

            mock.method(global, 'fetch', async () => ({
              ok: false,
              status: 500,
            }));

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['External service responded with status 500']);
          });

          it('denies access when fetching roles fails with a network error', async () => {
            loader = await loadAction(ACTION_PATH, [
              { name: 'my-module', filename: path.resolve(DIRNAME, './src/my-module.js') }],
            );

            mock.method(api.access, 'deny');

            mock.method(global, 'fetch', async () => {
              throw new Error('Network error');
            });

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Network error']);
          });
        });

        ```

        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` pour 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="test-an-action-module.test.ts" theme={null}
        const assert = require('node:assert');
        const { describe, it, beforeEach, afterEach, mock } = require('node:test');
        const { getDefaultArguments, loadAction } = require('@auth0/actions/post-login/v3/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/test-an-action-module.ts');
        const MY_MODULE_PATH = path.resolve(DIRNAME, './src/my-module.ts');

        const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';

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

          beforeEach(async () => {
            [event, api] = getDefaultArguments();
          });

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

          it('uses a module to get roles and set them on the access token', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            mock.method(api.accessToken, 'setCustomClaim');

            mock.method(global, 'fetch', async () => ({
              ok: true,
              status: 200,
              json: async () => ({ roles: ['admin'] }),
            }));

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.accessToken.setCustomClaim.mock.calls[0].arguments, [
              `${CUSTOM_CLAIM_NAMESPACE}/roles`,
              ['admin'],
            ]);
          });

          it('denies access when the external service responds with an error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            mock.method(api.access, 'deny');

            mock.method(global, 'fetch', async () => ({
              ok: false,
              status: 500,
            }));

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['External service responded with status 500']);
          });

          it('denies access when fetching roles fails with a network error', async () => {
            const { compiledActionPath, compiledModules } = compileActionModules(ACTION_PATH, [
              { name: 'my-module', filename: MY_MODULE_PATH },
            ]);
            loader = await loadAction(compiledActionPath, compiledModules);

            mock.method(api.access, 'deny');

            mock.method(global, 'fetch', async () => {
              throw new Error('Network error');
            });

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

            assert.strictEqual(global.fetch.mock.calls.length, 1);
            assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Network error']);
          });
        });

        ```

        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` pour 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>
