> ## Documentation Index
> Fetch the complete documentation index at: https://auth0.com/llms.txt
> Use this file to discover all available pages before exploring further.

# モジュールを使用するアクションのユニットテスト

> カスタムモジュール経由で外部サービスからユーザーのロールを取得し、アクセストークンのclaimとして設定して、失敗時にはアクセスを拒否するPost Loginアクションのユニットテストを、JavaScriptとTypeScriptでJest、Mocha、Node.js テストランナーを使って作成します。

## プロジェクト

各サンプルでは、アクションのソースコードを `src/` 配下に、ユニットテストを `test/` 配下に分けて配置しており、JavaScript プロジェクトと TypeScript プロジェクトのいずれも同じ構造になっています。

> [tab] JavaScript
>
> **/**
>
> **src**
>
> **test**

> [tab] TypeScript
>
> **/**
>
> **src**
>
> **test**
>
> **test-utils**

## アクション

次の Post Login アクションは、外部サービスからユーザーのロールを取得するカスタムモジュールをインポートし、取得したロールをアクセストークンのカスタム claim として設定します。モジュールの呼び出しが失敗した場合は、アクセスを拒否します。

> [tab] 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';
>
> /**
> * PostLogin フローの実行時に呼び出されるハンドラー。
> *
> * @param {Event} event - ユーザーと、そのユーザーがログインしているコンテキストの詳細。
> * @param {PostLoginAPI} api - ログインの動作を変更するために使用できるメソッドを提供するインターフェース。
> */
> 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);
>   }
> }
> ```
>
> このアクションには、外部サービスからロールを取得する `my-module` という名前のアクションモジュールが必要です。
>
> ```js title="my-module.js" theme={null}
> module.exports = {
>   /**
>    * ユーザーのロールを返します。
>    *
>    * @returns {{ roles: string[] }} ロールを含むペイロード。
>    */
>   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] 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';
>
> /**
> * PostLoginフローの実行中に呼び出されるHandlerです。
> *
> * @param {Event} event - ユーザーおよびそのユーザーがloginするコンテキストに関する詳細情報。
> * @param {PostLoginAPI} api - loginの動作を変更するためのメソッドを備えたインターフェイス。
> */
> 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);
>   }
> };
>
> ```
>
> このアクションには、外部サービスからロールを取得する `my-module` という名前のアクションモジュールが必要です。
>
> ```ts title="my-module.ts" theme={null}
> /**
>  * ユーザーのロールを返します。
>  *
>  * @returns ロールのペイロード。
>  */
> 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(`外部サービスがステータス ${response.status} を返しました`);
>   }
>
>   return response.json();
> };
>
> ```

## ユニットテスト

ユニットテストでは、アクションをカスタムモジュールとともに読み込み、`fetch` をモックして、成功時にロールが設定されること、および外部サービスがエラーステータスを返した場合やリクエストがネットワークエラーで失敗した場合にアクセスが拒否されることを検証します。

> [accordion] Jest
>
> > [tab] 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');
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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');
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルにコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test 提供）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction 経由で実行するため、ts-node や Vitest 自体の TS 変換を
> >  * 一切経由しません。したがって、アクションのソース（および actions: で登録された
> >  * モジュール）は、あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > }
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `jest.config.js` で TypeScript をコンパイルするように Jest を設定します。
> >
> > ```js title="jest.config.js" theme={null}
> > module.exports = {
> >   preset: 'ts-jest',
> >   testEnvironment: 'node',
> > };
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```

> [accordion] Mocha
>
> > [tab] 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');
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `.mocharc.json` で Mocha を設定します。
> >
> > ```json title=".mocharc.json" theme={null}
> > {
> >   "spec": "src/**/*.spec.js"
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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');
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルにコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test 提供）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction で実行するため、ts-node や Vitest 自身の TS 変換を経由しません。
> >  * そのため、アクションのソース（および actions: で登録されたモジュール）は、
> >  * あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > }
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `.mocharc.json` で Mocha を設定します。
> >
> > ```json title=".mocharc.json" theme={null}
> > {
> >   "require": "ts-node/register",
> >   "extension": ["ts"],
> >   "spec": "src/**/*.test.ts"
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```

> [accordion] Node.js テストランナー
>
> > [tab] 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']);
> >   });
> > });
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `jsconfig.json` を設定します。
> >
> > ```json title="jsconfig.json" theme={null}
> > {
> >   "compilerOptions": {
> >     "target": "ES2020",
> >     "module": "commonjs",
> >     "checkJs": false,
> >     "baseUrl": ".",
> >     "paths": {
> >       "actions:*": [
> >         "src/*"
> >       ]
> >     }
> >   },
> >   "include": [
> >     "src/**/*.js"
> >   ]
> > }
> >
> > ```
>
> > [tab] 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']);
> >   });
> > });
> >
> > ```
> >
> > このテストヘルパーは、実行時に読み込めるように TypeScript のアクションを一時ファイルにコンパイルします。
> >
> > ```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()（@auth0/actions/*\/test のもの）は対象ファイルをディスクから読み取り、
> >  * vm.compileFunction で実行するため、node ネイティブの TS 型除去を経由することはありません。
> >  * そのため、アクションのソース（および actions: で登録されたモジュール）は、
> >  * あらかじめディスク上でプレーンな JS にトランスパイルしておく必要があります。
> >  */
> > 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 };
> > };
> >
> > ```
> >
> > テスト用の依存関係を `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"
> >   }
> > }
> >
> > ```
> >
> > `actions:` のインポートエイリアスが `src/` に解決されるように `tsconfig.json` を設定します。
> >
> > ```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
> >   }
> > }
> >
> > ```
