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 affiche un formulaire de consentement marketing aux utilisateurs qui n’y ont pas encore répondu, puis traite la réponse soumise lors de la tentative de connexion suivante : elle refuse l’accès si le formulaire a été ignoré, sinon elle enregistre le choix de consentement dansuser_metadata.
- JavaScript
- TypeScript
mock-form-render.js
/** @import {Event, PostLoginAPI} from "@auth0/actions/post-login/v3" */
const CONSENT_FORM_ID = 'marketing-consent-form';
/**
* Gestionnaire appelé pendant l’exécution d’un flux PostLogin.
* Affiche un formulaire personnalisé demandant le consentement à des communications marketing si l’utilisateur n’a pas
* encore répondu. `api.prompt.render` ne peut pas être combiné à
* `api.redirect.sendUserTo` dans la même exécution : utilisez-en un seul par action.
*
* @param {Event} event - Informations sur l’utilisateur et le contexte dans lequel il s’authentifie.
* @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
*/
exports.onExecutePostLogin = async (event, api) => {
const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';
if (hasAnsweredConsent) {
return;
}
api.prompt.render(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name ?? event.user.name },
});
};
/**
* Gestionnaire appelé lorsque l’utilisateur soumet le formulaire affiché par
* onExecutePostLogin.
*
* @param {Event} event - Informations sur l’utilisateur et le contexte dans lequel il s’authentifie.
* @param {PostLoginAPI} api - Interface dont les méthodes permettent de modifier le comportement de la connexion.
*/
exports.onContinuePostLogin = async (event, api) => {
const consent = event.prompt?.fields?.consent;
if (typeof consent !== 'boolean') {
api.access.deny('Marketing consent response was not submitted.');
return;
}
api.user.setUserMetadata('marketing_consent', consent);
};
mock-form-render.ts
import type { Event, PostLoginAPI } from '@auth0/actions/post-login/v3';
const CONSENT_FORM_ID = 'marketing-consent-form';
/**
* Gestionnaire appelé lors de l'exécution d'un flux PostLogin.
* Affiche un Form personnalisé demandant le consentement marketing lorsque
* l'utilisateur n'y a pas encore répondu. `api.prompt.render` ne peut pas être
* combiné à `api.redirect.sendUserTo` dans une même exécution – choisissez-en
* un seul par action.
*
* @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: Event, api: PostLoginAPI) => {
const hasAnsweredConsent = typeof event.user.user_metadata.marketing_consent === 'boolean';
if (hasAnsweredConsent) {
return;
}
api.prompt.render(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name ?? event.user.name },
});
};
/**
* Gestionnaire appelé lorsque l'utilisateur soumet le Form affiché par
* onExecutePostLogin.
*
* @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.onContinuePostLogin = async (event: Event, api: PostLoginAPI) => {
const consent = event.prompt?.fields?.consent;
if (typeof consent !== 'boolean') {
api.access.deny('Marketing consent response was not submitted.');
return;
}
api.user.setUserMetadata('marketing_consent', consent);
};
Unit Test
Les tests unitaires simulent les objetsevent et api afin de vérifier que le formulaire s’affiche uniquement tant qu’aucune réponse de consentement n’a été donnée, et que les soumissions de consentement, qu’elles soient acceptées ou refusées, sont enregistrées correctement, tandis que les soumissions manquantes sont rejetées.
Jest
Jest
- JavaScript
- TypeScript
mock-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
jest.resetAllMocks();
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
jest.spyOn(api.prompt, 'render');
});
afterEach(() => {
jest.resetAllMocks();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
});
describe('onContinuePostLogin', () => {
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.user, 'setUserMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
expect(api.access.deny).not.toHaveBeenCalled();
});
});
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
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.prompt, 'render');
});
afterEach(() => {
jest.resetAllMocks();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).toHaveBeenCalledWith(CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
expect(api.prompt.render).not.toHaveBeenCalled();
});
});
describe('onContinuePostLogin', () => {
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.user, 'setUserMetadata');
});
afterEach(() => {
jest.resetAllMocks();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
expect(api.access.deny).toHaveBeenCalledWith('Marketing consent response was not submitted.');
expect(api.user.setUserMetadata).not.toHaveBeenCalled();
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', true);
expect(api.access.deny).not.toHaveBeenCalled();
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
expect(api.user.setUserMetadata).toHaveBeenCalledWith('marketing_consent', false);
expect(api.access.deny).not.toHaveBeenCalled();
});
});
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-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.prompt, 'render');
});
afterEach(() => {
sinon.restore();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
sinon.spy(api.access, 'deny');
sinon.spy(api.user, 'setUserMetadata');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
sinon.assert.notCalled(api.access.deny);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
sinon.assert.notCalled(api.access.deny);
});
});
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
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.prompt, 'render');
});
afterEach(() => {
sinon.restore();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.calledWith(api.prompt.render, CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
});
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
sinon.assert.notCalled(api.prompt.render);
});
});
describe('onContinuePostLogin', () => {
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.user, 'setUserMetadata');
});
afterEach(() => {
sinon.restore();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.access.deny, 'Marketing consent response was not submitted.');
sinon.assert.notCalled(api.user.setUserMetadata);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', true);
sinon.assert.notCalled(api.access.deny);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
sinon.assert.calledWith(api.user.setUserMetadata, 'marketing_consent', false);
sinon.assert.notCalled(api.access.deny);
});
});
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-form-render.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/mock-form-render.js');
const CONSENT_FORM_ID = 'marketing-consent-form';
describe('onExecutePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.prompt, 'render');
});
afterEach(() => {
mock.reset();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
}]);
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
});
describe('onContinuePostLogin', () => {
let loader;
let event;
let api;
beforeEach(async () => {
loader = await loadAction(ACTION_PATH);
[event, api] = getDefaultArguments();
mock.method(api.access, 'deny');
mock.method(api.user, 'setUserMetadata');
});
afterEach(() => {
mock.reset();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
});
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-form-render.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/mock-form-render.ts');
const CONSENT_FORM_ID = 'marketing-consent-form';
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.prompt, 'render');
});
afterEach(() => {
mock.reset();
});
it('renders the consent form when the user has not answered yet', async () => {
await loader.execute('onExecutePostLogin', event, api);
assert.deepEqual(api.prompt.render.mock.calls[0].arguments, [CONSENT_FORM_ID, {
vars: { firstName: event.user.given_name },
}]);
});
it('skips rendering when the user has already answered', async () => {
event.user.user_metadata.marketing_consent = true;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
it('skips rendering when the user has already declined', async () => {
event.user.user_metadata.marketing_consent = false;
await loader.execute('onExecutePostLogin', event, api);
assert.strictEqual(api.prompt.render.mock.calls.length, 0);
});
});
describe('onContinuePostLogin', () => {
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.user, 'setUserMetadata');
});
afterEach(() => {
mock.reset();
});
it('denies access when the form response is missing', async () => {
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.access.deny.mock.calls[0].arguments, ['Marketing consent response was not submitted.']);
assert.strictEqual(api.user.setUserMetadata.mock.calls.length, 0);
});
it('stores an accepted consent answer submitted by the form', async () => {
event.prompt.fields.consent = true;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', true]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
it('stores a declined consent answer submitted by the form', async () => {
event.prompt.fields.consent = false;
await loader.execute('onContinuePostLogin', event, api);
assert.deepEqual(api.user.setUserMetadata.mock.calls[0].arguments, ['marketing_consent', false]);
assert.strictEqual(api.access.deny.mock.calls.length, 0);
});
});
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
}
}