> ## Documentation Index
> Fetch the complete documentation index at: https://docs-dev-docs-event-stream-action-templates.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

> The **Actions NPM (`@auth0/actions`)** package enables the use of TypeScript development on external projects, allowing developers to follow best practices and improve their **Unit Testing** based on the TypeScript definitions.

# Actions Unit Test

# Auth0 Actions Unit Test

The **Actions NPM (`@auth0/actions`)** package enables the use of TypeScript development on external projects, allowing developers to follow best practices and improve their unit testing based on the TypeScript definitions.

***

## How it works

Follow the installation and import guidelines described at: [How Actions NPM works](/docs/customize/actions/actions-npm#how-it-works).

To unit test an Action, you must mock the `event` and `api` objects that your Action function receives. You can create these mocks using the TypeScript definitions included in `auth0/actions`, which ensures your tests accurately reflect the production environment. Testing frameworks like Jest are ideal for managing mocking and functionality.

The Unit Tests can be run in a local environment, version control, or CI/CD process, improving the overall quality assurance and validations before impacting changes on Auth0 Tenants.

***

## Examples

The following examples provide validation for a series of scenarios by mocking the necessary objects.

<Note>
  The examples use **Jest** (`https://www.npmjs.com/package/jest`), but any testing library can be used.
</Note>

### Configuration

In your `package.json`, define any development dependencies to have IntelliSense help when writing your Action.

<Tabs>
  <Tab title="Javascript">
    ```javascript theme={null}
    {
      "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"
      }
    }
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    {
      "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"
      }
    }
    ```

    In your `tsconfig.json` file, define how the TypeScript compiler should work:

    ```typescript theme={null}
    {
      "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"
      ]
    }
    ```

    In your `jest.config.js`, define `Jest` environment presets:

    ```typescript theme={null}
    module.exports = {
      preset: 'ts-jest',
      testEnvironment: 'node',
    };
    ```
  </Tab>
</Tabs>

## Pre-user registration access control and user metadata setup

The following example Action checks if the user email has a forbidden email domain, and calls `api.access.deny()` when matching. Otherwise, it checks if the full name has being provided through Custom Prompts additional fields and proceeds to set the full name at the user profile `user_metadata`, otherwise sends a validation error to Universal Login.

<Tabs>
  <Tab title="Javascript">
    ```javascript theme={null}
    /** @import {Event, PreUserRegistrationAPI} from "@auth0/actions/pre-user-registration/v2" */

    /**
    * Handler that will be called during the execution of a PreUserRegistration flow.
    *
    * @param {Event} event - Details about the context and user that is attempting to register.
    * @param {PreUserRegistrationAPI} api - Interface whose methods can be used to change the behavior of the signup.
    */
    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);
    }
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    import type { Event, PreUserRegistrationAPI } from '@auth0/actions/pre-user-registration/v2';

    /**
    * Handler that will be called during the execution of a PreUserRegistration flow.
    *
    * @param {Event} event - Details about the context and user that is attempting to register.
    * @param {PreUserRegistrationAPI} api - Interface whose methods can be used to change the behavior of the signup.
    */
    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);
    };
    ```
  </Tab>
</Tabs>

The Unit Test performs some validations to maximize code coverage, by mocking `event` and `api` objects.

<Tabs>
  <Tab title="Javascript">
    ```javascript theme={null}
    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');
      });
    });
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    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');
      });
    });
    ```
  </Tab>
</Tabs>

## Custom email provider and HTTP request

The following example Action attempts to send a message via HTTP request to an external service, handling the potential request error to drop the notification. It uses secrets for the external service URL and authorization API Key.

<Tabs>
  <Tab title="Javascript">
    ```javascript theme={null}
    /** @import {Event, CustomEmailProviderAPI} from "@auth0/actions/custom-email-provider/v1" */

    /**
    * Handler to be executed while sending an email notification
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {CustomEmailProviderAPI} api - Methods and utilities to help change the behavior of sending a email notification.
    */
    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');
      }
    }

    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    /** @import {Event, CustomEmailProviderAPI} from "@auth0/actions/custom-email-provider/v1" */

    /**
    * Handler to be executed while sending an email notification
    *
    * @param {Event} event - Details about the user and the context in which they are logging in.
    * @param {CustomEmailProviderAPI} api - Methods and utilities to help change the behavior of sending a email notification.
    */
    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');
      }
    }

    ```
  </Tab>
</Tabs>

The Unit Test performs some validations to maximize code coverage, by mocking event and api objects, in addition to the fetch function.

<Tabs>
  <Tab title="Javascript">
    ```javascript theme={null}
    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');
      });
    });

    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript theme={null}
    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');
      });
    });

    ```
  </Tab>
</Tabs>

To learn more about `@auth0/actions`, visit: [https://www.npmjs.com/package/@auth0/actions](https://www.npmjs.com/package/@auth0/actions).

To learn more about writing Actions, read [Write Your First Action](/docs/customize/actions/write-your-first-action).
