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 Pre User Registration suivante refuse les inscriptions provenant d’un domaine de courriel interdit, exige la saisie d’un nom complet et, le cas échéant, enregistre ce nom dansuser_metadata ainsi qu’un identifiant généré aléatoirement dans app_metadata.
- JavaScript
- TypeScript
mock-dependencies.js
/** @import {Event, PreUserRegistrationAPI} from "@auth0/actions/pre-user-registration/v2" */
const { randomUUID } = require('crypto');
/**
* Gestionnaire appelé lors de l’exécution d’un flux PreUserRegistration.
*
* @param {Event} event - Détails sur le contexte et l’utilisateur qui tente de s’inscrire.
* @param {PreUserRegistrationAPI} api - Interface dont les méthodes permettent de modifier le comportement de l’inscription.
*/
exports.onExecutePreUserRegistration = async (event, api) => {
const user = event.user;
if (user.email?.endsWith('@example.com')) {
api.access.deny('forbidden', 'Forbidden email domain')
return;
}
const fullName = event.request.body['ulp-fullName'];
if (fullName === undefined) {
api.validation.error('invalid_payload', 'Missing full name');
return;
}
api.user.setUserMetadata('full_name', fullName);
api.user.setAppMetadata('custom_id', randomUUID());
}
mock-dependencies.ts
import type { Event, PreUserRegistrationAPI } from '@auth0/actions/pre-user-registration/v2';
import { randomUUID } from 'crypto';
/**
* Gestionnaire appelé durant l’exécution d’un flux PreUserRegistration.
*
* @param {Event} event - Détails sur le contexte et l’utilisateur qui tente de s’inscrire.
* @param {PreUserRegistrationAPI} api - Interface dont les méthodes permettent de modifier le comportement de l’inscription.
*/
exports.onExecutePreUserRegistration = async (event: Event, api: PreUserRegistrationAPI) => {
const user = event.user;
if (user.email?.endsWith('@example.com')) {
api.access.deny('forbidden', 'Forbidden email domain')
return;
}
const fullName = event.request.body['ulp-fullName'];
if (fullName === undefined) {
api.validation.error('invalid_payload', 'Missing full name');
return;
}
api.user.setUserMetadata('full_name', fullName);
api.user.setAppMetadata('custom_id', randomUUID());
};
Test unitaire
Les tests unitaires simulent les objetsevent et api afin de vérifier que le domaine interdit est refusé, qu’un nom complet manquant déclenche une erreur de validation et qu’une soumission valide enregistre le nom complet ainsi qu’un UUID correctement formaté.
Jest
Jest
- JavaScript
- TypeScript
mock-dependencies.spec.js
const { getDefaultArguments, loadAction } = require('@auth0/actions/pre-user-registration/v2/test');
const path = require('path');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.js');
describe('onExecutePreUserRegistration', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.validation, 'error');
jest.spyOn(api.user, 'setUserMetadata');
jest.spyOn(api.user, 'setAppMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).toHaveBeenCalledWith('forbidden', 'Forbidden email domain');
expect(api.validation.error).not.toHaveBeenCalled();
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
expect(api.validation.error).toHaveBeenCalledWith('invalid_payload', 'Missing full name');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
expect(api.user.setAppMetadata).not.toHaveBeenCalled();
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
expect(api.validation.error).not.toHaveBeenCalled();
expect(api.user.setUserMetadata).toHaveBeenCalledWith('full_name', 'John Doe');
expect(api.user.setAppMetadata).toHaveBeenCalledWith('custom_id', expect.stringMatching(UUID_V4_PATTERN));
});
});
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"
]
}
mock-dependencies.test.ts
const { getDefaultArguments, loadAction } = require('@auth0/actions/pre-user-registration/v2/test');
const path = require('path');
const { compileActionModules } = require('./test-utils/load-compiled-action');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.ts');
describe('onExecutePreUserRegistration', () => {
let loader: any;
let event: any;
let api: any;
beforeEach(async () => {
const { compiledActionPath } = compileActionModules(ACTION_PATH);
loader = await loadAction(compiledActionPath);
[event, api] = getDefaultArguments();
jest.spyOn(api.access, 'deny');
jest.spyOn(api.validation, 'error');
jest.spyOn(api.user, 'setUserMetadata');
jest.spyOn(api.user, 'setAppMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).toHaveBeenCalledWith('forbidden', 'Forbidden email domain');
expect(api.validation.error).not.toHaveBeenCalled();
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
expect(api.validation.error).toHaveBeenCalledWith('invalid_payload', 'Missing full name');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
expect(api.user.setAppMetadata).not.toHaveBeenCalled();
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
expect(api.access.deny).not.toHaveBeenCalled();
expect(api.validation.error).not.toHaveBeenCalled();
expect(api.user.setUserMetadata).toHaveBeenCalledWith('full_name', 'John Doe');
expect(api.user.setAppMetadata).toHaveBeenCalledWith('custom_id', expect.stringMatching(UUID_V4_PATTERN));
});
});
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
mock-dependencies.spec.js
const sinon = require('sinon');
const { getDefaultArguments, loadAction } = require('@auth0/actions/pre-user-registration/v2/test');
const path = require('path');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.js');
describe('onExecutePreUserRegistration', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.validation, 'error');
sinon.spy(api.user, 'setUserMetadata');
sinon.spy(api.user, 'setAppMetadata');
});
afterEach(() => {
sinon.restore();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.calledWith(api.access.deny, 'forbidden', 'Forbidden email domain');
sinon.assert.notCalled(api.validation.error);
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.notCalled(api.access.deny);
sinon.assert.calledWith(api.validation.error, 'invalid_payload', 'Missing full name');
sinon.assert.notCalled(api.user.setUserMetadata);
sinon.assert.notCalled(api.user.setAppMetadata);
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.notCalled(api.access.deny);
sinon.assert.notCalled(api.validation.error);
sinon.assert.calledWith(api.user.setUserMetadata, 'full_name', 'John Doe');
sinon.assert.calledWith(api.user.setAppMetadata, 'custom_id', sinon.match(UUID_V4_PATTERN));
});
});
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"
]
}
mock-dependencies.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/pre-user-registration/v2/test');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.ts');
describe('onExecutePreUserRegistration', () => {
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.validation, 'error');
sinon.spy(api.user, 'setUserMetadata');
sinon.spy(api.user, 'setAppMetadata');
});
afterEach(() => {
sinon.restore();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.calledWith(api.access.deny, 'forbidden', 'Forbidden email domain');
sinon.assert.notCalled(api.validation.error);
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.notCalled(api.access.deny);
sinon.assert.calledWith(api.validation.error, 'invalid_payload', 'Missing full name');
sinon.assert.notCalled(api.user.setUserMetadata);
sinon.assert.notCalled(api.user.setAppMetadata);
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
sinon.assert.notCalled(api.access.deny);
sinon.assert.notCalled(api.validation.error);
sinon.assert.calledWith(api.user.setUserMetadata, 'full_name', 'John Doe');
sinon.assert.calledWith(api.user.setAppMetadata, 'custom_id', sinon.match(UUID_V4_PATTERN));
});
});
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
mock-dependencies.spec.js
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/pre-user-registration/v2/test');
const path = require('path');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.js');
describe('onExecutePreUserRegistration', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.validation, 'error');
mock.method(api.user, 'setUserMetadata');
mock.method(api.user, 'setAppMetadata');
});
afterEach(() => {
mock.reset();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['forbidden', 'Forbidden email domain']);
assert.strictEqual(api.validation.error.mock.calls.length, 0);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
assert.deepEqual(api.validation.error.mock.calls[0].arguments, ['invalid_payload', 'Missing full name']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
assert.strictEqual(api.user.setAppMetadata.mock.calls.length, 0);
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
assert.strictEqual(api.validation.error.mock.calls.length, 0);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['full_name', 'John Doe']);
const appMetadataCall = api.user.setAppMetadata.mock.calls[0];
assert.strictEqual(appMetadataCall.arguments[0], 'custom_id');
assert.match(appMetadataCall.arguments[1], UUID_V4_PATTERN);
});
});
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"
]
}
mock-dependencies.test.ts
const assert = require('node:assert');
const { describe, it, beforeEach, afterEach, mock } = require('node:test');
const { getDefaultArguments, loadAction } = require('@auth0/actions/pre-user-registration/v2/test');
const path = require('path');
const { compileActionModules } = require('./test-utils/load-compiled-action.ts');
const UUID_V4_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
const DIRNAME = path.dirname('../');
const ACTION_PATH = path.resolve(DIRNAME, './src/mock-dependencies.ts');
describe('onExecutePreUserRegistration', () => {
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.validation, 'error');
mock.method(api.user, 'setUserMetadata');
mock.method(api.user, 'setAppMetadata');
});
afterEach(() => {
mock.reset();
});
it('forbids email domain', async () => {
event.user.email = 'johndoe@example.com';
await loader.execute('onExecutePreUserRegistration', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['forbidden', 'Forbidden email domain']);
assert.strictEqual(api.validation.error.mock.calls.length, 0);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('allows email domain without full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {};
await loader.execute('onExecutePreUserRegistration', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
assert.deepEqual(api.validation.error.mock.calls[0].arguments, ['invalid_payload', 'Missing full name']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
assert.strictEqual(api.user.setAppMetadata.mock.calls.length, 0);
});
it('allows email domain with full name', async () => {
event.user.email = 'johndoe@test.com';
event.request.body = {
'ulp-fullName': 'John Doe',
};
await loader.execute('onExecutePreUserRegistration', event, api);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
assert.strictEqual(api.validation.error.mock.calls.length, 0);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['full_name', 'John Doe']);
const appMetadataCall = api.user.setAppMetadata.mock.calls[0];
assert.strictEqual(appMetadataCall.arguments[0], 'custom_id');
assert.match(appMetadataCall.arguments[1], UUID_V4_PATTERN);
});
});
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
}
}