Projet
Dans chaque exemple, le code source de l’action se trouve soussrc/ et ses tests unitaires sous test/, selon la même structure pour les projets JavaScript et TypeScript.
- JavaScript
- TypeScript
/
src
test
/
src
test
Action
L’action Post Login suivante vérifie les rôles de l’utilisateur à partir deevent.authorization et les ajoute comme demande personnalisée au jeton d’ID; l’accès est refusé si aucun rôle n’est présent.
- JavaScript
- TypeScript
test-an-action.js
/** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
/**
* Gestionnaire appelé lors de l'exécution d'un flux PostLogin.
*
* @param {Event} event - Détails 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) => {
const roles = event.authorization?.roles;
if (roles === undefined || roles.length === 0) {
api.access.deny('Restricted');
return;
}
api.idToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
}
test-an-action.ts
import type { PostLoginAPI, Event } from '@auth0/actions/post-login/v3';
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
/**
* Gestionnaire appelé lors de 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: Event, api: PostLoginAPI) => {
const roles = event.authorization?.roles;
if (roles === undefined || roles.length === 0) {
api.access.deny('Restricted');
return;
}
api.idToken.setCustomClaim(`${CUSTOM_CLAIM_NAMESPACE}/roles`, roles);
};
Test unitaire
Les tests unitaires simulent les objetsevent et api afin de vérifier que les utilisateurs administrateurs obtiennent bien la demande personnalisée, tandis que les utilisateurs sans rôle ou dont l’objet authorization est absent se voient refuser l’accès.
Jest
Jest
- JavaScript
- TypeScript
test-an-action.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.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.idToken, 'setCustomClaim');
});
afterEach(() => {
jest.resetAllMocks();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).toHaveBeenCalledWith(
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin']
);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).not.toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Restricted');
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).not.toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Restricted');
});
});
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.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.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
jest.resetAllMocks();
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.idToken, 'setCustomClaim');
});
afterEach(() => {
jest.resetAllMocks();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).toHaveBeenCalledWith(
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin']
);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).not.toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Restricted');
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
expect(api.idToken.setCustomClaim).not.toHaveBeenCalled();
expect(api.access.deny).toHaveBeenCalledWith('Restricted');
});
});
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.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.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.idToken, 'setCustomClaim');
});
afterEach(() => {
sinon.restore();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(
api.idToken.setCustomClaim,
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin']
);
sinon.assert.notCalled(api.access.deny);
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.idToken.setCustomClaim);
sinon.assert.calledWith(api.access.deny, 'Restricted');
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.idToken.setCustomClaim);
sinon.assert.calledWith(api.access.deny, 'Restricted');
});
});
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.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.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.idToken, 'setCustomClaim');
});
afterEach(() => {
sinon.restore();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(
api.idToken.setCustomClaim,
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin']
);
sinon.assert.notCalled(api.access.deny);
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.idToken.setCustomClaim);
sinon.assert.calledWith(api.access.deny, 'Restricted');
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.idToken.setCustomClaim);
sinon.assert.calledWith(api.access.deny, 'Restricted');
});
});
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.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.js');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.idToken, 'setCustomClaim');
});
afterEach(() => {
mock.reset();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.idToken.setCustomClaim.mock.calls[0].arguments, [
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin'],
]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.idToken.setCustomClaim.mock.calls.length, 0);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Restricted']);
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.idToken.setCustomClaim.mock.calls.length, 0);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Restricted']);
});
});
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.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.ts');
const CUSTOM_CLAIM_NAMESPACE = 'https://example.com';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.idToken, 'setCustomClaim');
});
afterEach(() => {
mock.reset();
});
it('Admin user', async () => {
event.authorization = { roles: ['admin'] };
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.idToken.setCustomClaim.mock.calls[0].arguments, [
`${CUSTOM_CLAIM_NAMESPACE}/roles`,
['admin'],
]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('Restricted User - No authorization', async () => {
delete event.authorization;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.idToken.setCustomClaim.mock.calls.length, 0);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Restricted']);
});
it('Restricted User - No roles', async () => {
event.authorization = { roles: [] };
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.idToken.setCustomClaim.mock.calls.length, 0);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Restricted']);
});
});
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
}
}