Auth0 Actions Unit Test
@auth0/actions) パッケージを使用すると、外部プロジェクトで TypeScript を用いた開発が可能になり、開発者はベストプラクティスに従って、TypeScript の型定義に基づくユニットテストを強化できます。
仕組み
event オブジェクトと api オブジェクトをモックする必要があります。これらのモックは、auth0/actions に含まれる TypeScript の定義を使用して作成できるため、テストで本番環境を正確に反映できます。モックや機能の検証を管理するには、Jest のようなテストフレームワークが適しています。
ユニットテストは、ローカル環境、バージョン管理、または CI/CD プロセスで実行できます。これにより、Auth0 テナントに変更を反映する前に、全体的な品質保証と検証を強化できます。
例
これらの例では Jest (
https://www.npmjs.com/package/jest) を使用していますが、任意のテストライブラリを使用できます。設定
package.json で開発依存関係を定義すると、Action の作成時に IntelliSense の補完支援を利用できます。
- JavaScript
- TypeScript
{
"name": "actions-js",
"version": "1.0.0",
"description": "Actions JS",
"main": "example.js",
"scripts": {
"test": "jest"
},
"author": "John Doe",
"license": "ISC",
"devDependencies": {
"@auth0/actions": "^0.7.1",
"jest": "^29.7.0"
}
}
{
"name": "actions-ts",
"version": "1.0.0",
"description": "Actions TS",
"main": "example.ts",
"scripts": {
"test": "jest"
},
"author": "John Doe",
"license": "ISC",
"devDependencies": {
"@auth0/actions": "^0.7.1",
"@types/jest": "^29.5.12",
"@types/node": "22.14.0",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"typescript": "^5.9.2"
}
}
tsconfig.json ファイルでは、TypeScript コンパイラの動作を定義します。{
"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
},
"include": [
"**/*.ts"
],
"exclude": [
"node_modules",
"dist"
]
}
jest.config.js では、Jest の環境プリセットを定義します。module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
ユーザー登録前のアクセス制御とユーザーメタデータの設定
api.access.deny() を呼び出します。一致しない場合は、Custom Prompts の追加フィールド経由で氏名が提供されているかどうかを確認し、提供されていればユーザープロファイルの user_metadata に氏名を設定します。提供されていない場合は、Universal Login にバリデーションエラーを返します。
- JavaScript
- TypeScript
/** @import {Event, PreUserRegistrationAPI} from "@auth0/actions/pre-user-registration/v2" */
/**
* PreUserRegistration フローの実行中に呼び出されるハンドラー。
*
* @param {Event} event - 登録を試行しているコンテキストとユーザーの詳細。
* @param {PreUserRegistrationAPI} api - サインアップの動作を変更するために使用できるメソッドを持つインターフェース。
*/
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);
}
import type { Event, PreUserRegistrationAPI } from '@auth0/actions/pre-user-registration/v2';
/**
* PreUserRegistration フローの実行中に呼び出されるハンドラー。
*
* @param {Event} event - 登録を試行しているコンテキストとユーザーの詳細。
* @param {PreUserRegistrationAPI} api - サインアップの動作を変更するために使用できるメソッドを持つインターフェース。
*/
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);
};
event オブジェクトと api オブジェクトをモック化し、コードカバレッジを最大化するためにいくつかの検証を行います。
- JavaScript
- TypeScript
const { onExecutePreUserRegistration } = require('./preUserRegistration');
describe('onExecutePreUserRegistration', () => {
const mockApi = {
access: {
deny: jest.fn(),
},
user: {
setUserMetadata: jest.fn(),
},
validation: {
error: jest.fn(),
},
};
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
it('forbids email domain', async () => {
const mockEvent = {
user: {
email: 'johndoe@example.com',
}
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).toHaveBeenCalledWith('forbidden', 'Forbidden email domain');
expect(mockApi.validation.error).not.toHaveBeenCalled();
expect(mockApi.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain without full name', async () => {
const mockEvent = {
request: {
body: {},
},
user: {
email: 'johndoe@test.com',
},
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).not.toHaveBeenCalled();
expect(mockApi.validation.error).toHaveBeenCalledWith('invalid_payload', 'Missing full name');
expect(mockApi.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain with full name', async () => {
const mockEvent = {
request: {
body: {
'ulp-fullName': 'John Doe'
},
},
user: {
email: 'johndoe@test.com',
},
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).not.toHaveBeenCalled();
expect(mockApi.validation.error).not.toHaveBeenCalled();
expect(mockApi.user.setUserMetadata).toHaveBeenCalledWith('full_name', 'John Doe');
});
});
const { onExecutePreUserRegistration } = require('./preUserRegistration');
describe('onExecutePreUserRegistration', () => {
const mockApi = {
access: {
deny: jest.fn(),
},
user: {
setUserMetadata: jest.fn(),
},
validation: {
error: jest.fn(),
},
};
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
it('forbids email domain', async () => {
const mockEvent = {
user: {
email: 'johndoe@example.com',
}
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).toHaveBeenCalledWith('forbidden', 'Forbidden email domain');
expect(mockApi.validation.error).not.toHaveBeenCalled();
expect(mockApi.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain without full name', async () => {
const mockEvent = {
request: {
body: {},
},
user: {
email: 'johndoe@test.com',
},
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).not.toHaveBeenCalled();
expect(mockApi.validation.error).toHaveBeenCalledWith('invalid_payload', 'Missing full name');
expect(mockApi.user.setUserMetadata).not.toHaveBeenCalled();
});
it('allows email domain with full name', async () => {
const mockEvent = {
request: {
body: {
'ulp-fullName': 'John Doe'
},
},
user: {
email: 'johndoe@test.com',
},
};
await onExecutePreUserRegistration(mockEvent, mockApi);
expect(mockApi.access.deny).not.toHaveBeenCalled();
expect(mockApi.validation.error).not.toHaveBeenCalled();
expect(mockApi.user.setUserMetadata).toHaveBeenCalledWith('full_name', 'John Doe');
});
});
カスタムメールプロバイダーとHTTPリクエスト
- JavaScript
- TypeScript
/** @import {Event, CustomEmailProviderAPI} from "@auth0/actions/custom-email-provider/v1" */
/**
* メール通知の送信時に実行されるハンドラー
*
* @param {Event} event - ユーザーと、そのユーザーのログイン時のコンテキストに関する詳細。
* @param {CustomEmailProviderAPI} api - メール通知の送信動作を変更するためのメソッドとユーティリティ。
*/
exports.onExecuteCustomEmailProvider = async (event, api) => {
const notification = event.notification;
const message = {
body: notification.html
};
try {
await fetch(event.secrets.URL, {
method: 'POST',
headers: {
'X-API-Key': event.secrets.API_KEY,
},
body: JSON.stringify(message),
});
} catch (err) {
api.notification.drop('External service failure');
}
}
/** @import {Event, CustomEmailProviderAPI} from "@auth0/actions/custom-email-provider/v1" */
/**
* メール通知の送信時に実行されるハンドラー
*
* @param {Event} event - ユーザーと、そのユーザーのログイン時のコンテキストに関する詳細。
* @param {CustomEmailProviderAPI} api - メール通知の送信動作を変更するためのメソッドとユーティリティ。
*/
exports.onExecuteCustomEmailProvider = async (event, api) => {
const notification = event.notification;
const message = {
body: notification.html
};
try {
await fetch(event.secrets.URL, {
method: 'POST',
headers: {
'X-API-Key': event.secrets.API_KEY,
},
body: JSON.stringify(message),
});
} catch (err) {
api.notification.drop('External service failure');
}
}
fetch 関数に加えて event オブジェクトと api オブジェクトをモック化し、いくつかの検証を行います。
- JavaScript
- TypeScript
const { onExecuteCustomEmailProvider } = require('./customEmailProvider');
describe('onExecuteCustomEmailProvider', () => {
const mockApi = {
notification: {
drop: jest.fn(),
},
};
const mockEvent = {
notification: {
html: '<h1>Hello world</h1>',
},
secrets: {
URL: 'https://example.com/service',
API_KEY: 'ApiKeySecret1234.',
},
user: {
email: 'johndoe@example.com',
},
};
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
it('succeeds on external service request', async () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => Promise.resolve({
ok: true,
status: 200,
json: async () => ({ message: 'Success!' }),
}));
await onExecuteCustomEmailProvider(mockEvent, mockApi);
expect(global.fetch).toHaveBeenCalled();
expect(mockApi.notification.drop).not.toHaveBeenCalled();
});
it('fails on external service request', async () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => Promise.reject({
ok: false,
status: 500,
json: async () => ({ error: 'Server Error' }),
}));
await onExecuteCustomEmailProvider(mockEvent, mockApi);
expect(mockApi.notification.drop).toHaveBeenCalledWith('External service failure');
});
});
const { onExecuteCustomEmailProvider } = require('./customEmailProvider');
describe('onExecuteCustomEmailProvider', () => {
const mockApi = {
notification: {
drop: jest.fn(),
},
};
const mockEvent = {
notification: {
html: '<h1>Hello world</h1>',
},
secrets: {
URL: 'https://example.com/service',
API_KEY: 'ApiKeySecret1234.',
},
user: {
email: 'johndoe@example.com',
},
};
beforeEach(() => {
jest.resetAllMocks();
});
afterEach(() => {
jest.resetAllMocks();
});
it('succeeds on external service request', async () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => Promise.resolve({
ok: true,
status: 200,
json: async () => ({ message: 'Success!' }),
} as Response));
await onExecuteCustomEmailProvider(mockEvent, mockApi);
expect(global.fetch).toHaveBeenCalled();
expect(mockApi.notification.drop).not.toHaveBeenCalled();
});
it('fails on external service request', async () => {
jest.spyOn(global, 'fetch').mockImplementationOnce(() => Promise.reject({
ok: false,
status: 500,
json: async () => ({ error: 'Server Error' }),
} as Response));
await onExecuteCustomEmailProvider(mockEvent, mockApi);
expect(mockApi.notification.drop).toHaveBeenCalledWith('External service failure');
});
});
@auth0/actions の詳細については、https://www.npmjs.com/package/@auth0/actions を参照してください。
Actions の作成方法について詳しくは、初めての Action を作成する を参照してください。