Projet
Chaque exemple sépare le code source de l’action, situé danssrc/, de ses tests unitaires, situés dans test/, en suivant la même structure pour les projets JavaScript et TypeScript.
- JavaScript
- TypeScript
/
src
test
/
src
test
Action
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.- JavaScript
- TypeScript
test-an-action-module.js
/** @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);
}
}
my-module, qui récupère les rôles auprès d’un service externe :my-module.js
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();
}
};
test-an-action-module.ts
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);
}
};
my-module, qui récupère les rôles auprès d’un service externe :my-module.ts
/**
* 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();
};
Test unitaire
Les tests unitaires chargent l’action avec le module personnalisé et simulentfetch 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.
Jest
Jest
- JavaScript
- TypeScript
test-an-action-module.spec.js
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 :package.json
{
"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"
}
}
jsconfig.json pour que l’alias d’importation actions: pointe vers src/ :jsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"checkJs": false,
"baseUrl": ".",
"paths": {
"actions:*": [
"src/*"
]
}
},
"include": [
"src/**/*.js"
]
}
test-an-action-module.test.ts
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');
});
});
load-compiled-action.ts
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 };
}
package.json :package.json
{
"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 :jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
tsconfig.json pour que l’alias d’importation actions: pointe vers src/ :tsconfig.json
{
"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
}
}
Mocha
Mocha
- JavaScript
- TypeScript
test-an-action-module.spec.js
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 :package.json
{
"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 :.mocharc.json
{
"spec": "src/**/*.spec.js"
}
jsconfig.json pour que l’alias d’importation actions: pointe vers src/ :jsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"checkJs": false,
"baseUrl": ".",
"paths": {
"actions:*": [
"src/*"
]
}
},
"include": [
"src/**/*.js"
]
}
test-an-action-module.test.ts
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');
});
});
load-compiled-action.ts
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 };
}
package.json :package.json
{
"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 :.mocharc.json
{
"require": "ts-node/register",
"extension": ["ts"],
"spec": "src/**/*.test.ts"
}
tsconfig.json pour que l’alias d’importation actions: pointe vers src/ :tsconfig.json
{
"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
}
}
Node.js Test Runner
Node.js Test Runner
- JavaScript
- TypeScript
test-an-action-module.spec.js
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 :package.json
{
"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"
}
}
jsconfig.json pour que l’alias d’importation actions: pointe vers src/ :jsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"checkJs": false,
"baseUrl": ".",
"paths": {
"actions:*": [
"src/*"
]
}
},
"include": [
"src/**/*.js"
]
}
test-an-action-module.test.ts
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']);
});
});
load-compiled-action.ts
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 };
};
package.json :package.json
{
"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"
}
}
tsconfig.json pour que l’alias d’importation actions: pointe vers src/ :tsconfig.json
{
"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
}
}