Unit Testing Auth0 Custom Database Scripts
Tags: auth0, testing, tdd, javascript
Published: 2024-03-13 14:11:43

# Introduction
Welcome, fellow developers! Today I want to present you a step-by-step technique on how to test [Auth0](https://auth0.com/)’s custom actions and databases in Javascript. For those of you who don’t know Auth0, it’s an identity management platform that you can connect to your existing or new applications, and configure it to easily provide authentication and authorization mechanisms. It’s one of the easiest solutions for IAM nowadays.

You can check the [reference repository](https://github.com/thiagomini/auth0-test-sample/tree/master) if you just want to see the final result, but I encourage you to keep reading if you also want to understand the thought process. Here's what we'll learn:

- How to use `auth0-deploy-cli` to keep you tenant's configuration, code, and settings in a local repo
- How to create tests for your custom database
- How to apply TDD to create the tests above and implement the scripts
- How to use Spies instead of Mocks to test network calls to external services
- How to inject dependencies in database scripts
- How to use globally-defined objects for testing
- How to properly handle errors in Auth0

Without further ado, let's get into it!

## Auth0 Custom Database Scripts

So, what are Auth0’s custom database scripts? To be concise, these are scripts used to connect to an existing application’s database to perform the following actions:

- **Create** - This script will create a user in your database when someone signs up via Auth0
- **Login -** This script authenticates a user against the credentials stored in your database.
- **Delete -** Once a user is deleted via Auth0’s dashboard or [Management API](https://auth0.com/docs/api/management/v2), this script ensures the user is also deleted in your database.
- **Get User -** This script determines whether a user with a given email exists. It’s executed when users change their password to determine if they exist.
- **Verify** - This script marks the current user’s email address as verified in your database
- **Change Password -** This script should change the password stored for the current user in your database.


![Diagram illustrating how each database script interacts with Auth0 and your database](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2gv9ff8idfvhe7bm4lz5.png)

These actions must be highly maintainable and reliable — after all, your application’s authorization and authentication flows will go through them. Creating unit tests for these custom database scripts is extremely valuable to ensure everything is working as expected while also serving as live documentation for IAM. In the next section, we’ll see what we need to start creating tests for our Auth0 Tenant scripts.

### Pulling the tenant data

Before creating tests for our scripts, we need our [tenant](https://auth0.com/docs/get-started/auth0-overview/create-tenants)’s data and configuration in our local machines. To do so, we must install [auth0-deploy-cli](https://www.npmjs.com/package/auth0-deploy-cli). This tool can download your tenant’s configuration and schema to local folders. Follow the steps below to configure it and pull your tenant’s data:

> The steps below assume you already have an Auth0 tenant created.

1. Create a new Database authentication method. We’ll use this as our example external database that Auth0 needs to connect to and synchronize with. Go to Authentication > Database > Create DB Connection to create it.
    1. Go to the **Custom Database** tab and toggle the `Use my own Database` switch. This will enable all the custom database action scripts.
2. Install the CLI tool using your preferred package manager: `yarn add -D auth0-deploy-cli`
3. Create an Auth0 [Application](https://auth0.com/docs/get-started/applications) to represent the CLI tool access. To do so, go to the dashboard, Applications > Applications > Create Application > Machine to Machine.
    1. Select the Auth0 Management API as the authorized party. Then, you can select all the permissions for the purpose of this exercise - ideally, you’d go with the least-privileges strategy. 
4. Create a file named `config.json` in the root of your project, and put the following data into it:

```json
{
  "AUTH0_DOMAIN": "auth0-domain", // You can check this in the application you just created, under the `settings` tab
  "AUTH0_CLIENT_ID": "auth0-deploy-application-client-id",
  "AUTH0_CLIENT_SECRET": "auth0-deploy-application-client-secret",
  "AUTH0_ALLOW_DELETE": false
}
// These values are used by the CLI to connect to your Auth0 tenant and potentially pull / push data.
```

1. Create a script in your `package.json` file to pull changes from the tenant to your local file system: 

```json
{
  "scripts": {
	  "a0deploy export -c config.json --format=yaml --output_folder=tenant"
  }
}
```

1. Execute the script with your package manager. Then, your directory structure will look like the following:

```
📦tenant
 ┣ 📂databases
 ┃ ┗ 📂Database // This folder represents the Database Authentication we've created
 ┃ ┃ ┣ 📜change_password.js
 ┃ ┃ ┣ 📜create.js
 ┃ ┃ ┣ 📜delete.js
 ┃ ┃ ┣ 📜get_user.js
 ┃ ┃ ┣ 📜login.js
 ┃ ┃ ┗ 📜verify.js
 ┣ 📂emailTemplates
 ┣ 📂hooks
 ┣ 📂pages
 ┣ 📂rules
 ┗ 📜tenant.yaml // This file stores all the tenant's settings and data
```

At this point, we’re ready to start creating tests for our database scripts. We’ll see how to do that in the next section.

### Creating tests for database scripts
Finally, we have (almost) everything set up to start writing our tests - but before we do so, let’s make it clear that we need an example definition of an API that is a port to our external database. You can check out all the requirements in the [Readme file](https://github.com/thiagomini/auth0-test-sample/blob/master/readme.md), but let’s take care of a single endpoint for now, the signup:

```markdown
POST /signup - used to create users
- Body:
  - `email`: The user's email
  - `password`: The user's password
  - `passwordConfirmation`: The user's password confirmation
- Response:
  - ✅ **Success**:
    - **status**: 201
    - **body**: 
      - `id`: The user's id
  - ❌ **Error**: 
    - **status**: 400 | 500
    - **body**: 
      - `error`: The error message
```

With that information, we’ll start defining which tests we need before implementing anything. Create a new file under `/tenant/test/databases/create.test.js` . There, we’ll determine what needs to be done:

```javascript
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');

describe('Create user script', () => {
  test.todo('successfully creates a user');
  test.todo('calls axios post with correct parameters');
  test.todo('throws validation error when user already exists');
  test.todo('throws error with descriptive message when something goes wrong');
});
```
Notice that we’re using node’s native [test](https://nodejs.org/api/test.html) module - the idea is to use the least number of dependencies, and node’s test runner has all features we need here - while also being [way faster than jest](https://medium.com/@lucas_levandoski/node-native-test-runner-benchmark-5cb47d4706cb).

To execute test files with node’s test runner, we need to add another script to our `package.json` file:

```json
{
	"scripts": {
	  "test": "node --test"
	}
}
```
Now, you can simply execute it like `yarn test` and see the results. Remember, you’ll need Node’s LTS version to make it work!

So, we must pick one test case above and define its body. Then, we’ll implement the least amount of code that we need to make it pass - this is how we apply the famous [Test-Driven Development](https://martinfowler.com/bliki/TestDrivenDevelopment.html) (TDD). Let’s start with the **first test,** namely, “successfully creates a user”. This is the easiest one to begin with - if you look at the generated comments for the `create.js`  script, the expected result when the user is successfully created is that it should call the `callback` function with `null`:

```javascript
// There are three ways this script can finish:
  // 1. A user was successfully created
  //     callback(null);
  // 2. This user already exists in your database
  //     callback(new ValidationError("user_exists", "my error message"));
  // 3. Something went wrong while trying to reach your database
  //     callback(new Error("my error message"));
```

Considering that requirement now, let’s define the test body as follows:
```js
const { test, describe, mock } = require('node:test');
const assert = require('node:assert/strict');
const { randomUUID } = require('node:crypto');
const { create } = require('../../databases/Database/create');

describe('Create user script', () => {
  test('successfully creates a user', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mock.fn();

    // Act
    await create(user, callback);

    // Assert
    assert.equal(callback.mock.callCount(), 1);
    assert.deepEqual(callback.mock.calls[0].arguments, [null]);
  });
  test.todo('calls axios post with correct parameters');
  test.todo('throws validation error when user already exists');
  test.todo('throws error with descriptive message when something goes wrong');
});

function makeUserData() {
  return {
    email: 'user@mail.com',
    password: randomUUID(),
    tenant: 'test-tenant',
    client_id: 'test-client-id',
    connection: 'Database',
  };
}
```
As you can see, the only expected behavior here is that our `callback` function, created using node’s `mock` utility, was called with `null` argument. the `makeUserData()` utility function returns an object with the interface the `create` script expects.

Now, after executing this test and seeing it failing, we’ll change the source code to make it pass:

```jsx
// You have to manually  change the code below to explicitly export the `create` function,
// otherwise you can't import that function into the test file. It still works in Auth0, so no need to worry :)
module.exports.create = async function create(user, callback) {
  // Auto-generated docs redacted
  return callback(null);
};
```

The first test should pass now. Great! That's our first step towards implementing this `create` custom script. Before we move to the next ones, we can perform a slight improvement to our "Assert" block in our test code, which reads a bit weird now:

```js
// Assert
assert.equal(callback.mock.callCount(), 1);
assert.deepEqual(callback.mock.calls[0].arguments, [null]);
```

We can improve the readability of this type of test by creating a new utility:

```js
// mock-extended.js file
const { mock } = require('node:test');
const assert = require('node:assert/strict');

function mockFn() {
  const mockedFunction = mock.fn();
  mockedFunction.shouldHaveBeenCalledOnceWith = (expectedArgument) => {
    assert.equal(mockedFunction.mock.callCount(), 1);
    assert.deepEqual(
      mockedFunction.mock.calls[0].arguments[0],
      expectedArgument
    );
  };
  return mockedFunction;
}

module.exports = {
  mockFn,
};
```

The `mock-extended` file above exposes a factory to create an enhanced node's native `mock` object. This object has an additional `shouldHaveBeenCalledOnceWith(argument)` method to increase the test readability. Here's how we can rewrite the test:

```js
test('successfully creates a user', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mockFn();

    // Act
    await create(user, callback);

    // Assert
    callback.shouldHaveBeenCalledOnceWith(null);
  });
```

Way more expressive, don't you agree? The next step is to implement another `todo` test, `test.todo('calls axios post with correct parameters');`. This test will require more sophistication, so we'll dive into it in the next section.

### Creating and injecting Spies to communicate with external services

Mocking is a common practice when testing your application's behavior when communicating with external services. Mocks are a particular type of [Test Double](https://martinfowler.com/bliki/TestDouble.html), an object used to replace the production one for testing purposes. However, mocks in Javascript are not so great because they don't provide a nice API for stubbing returned values or checking for called values (as you can notice with the `callback` mock example, we had to create a new utility).

A more helpful approach when we want to override communication with external services is to create a Spy. A Spy is an object that follows the same API as the production one but returns canned data and stores what was called internally. This is useful to make the test code cleaner and readable while also encapsulating the details of how the stubbing process works underneath it. Moreover, you can reutilize the Spy definition across different tests.

So, let's get back to what we needed for the new test:

```js
test.todo('calls axios post with correct parameters');
```

This test aims to ensure we called `axios.post` with the URL and body parameters as defined in the requirements:

- **URL**: `POST /signup`
- **Body**: `email`, `password`, and `passwordConfirmation`

We'll use an [AxiosSpy](https://github.com/thiagomini/auth0-test-sample/blob/master/tenant/test/utils/axios.spy.js) class to test this with a nice fluent interface. However, to use this spy, we also need the `create` script to accept an instance of `axios` as an additional parameter, otherwise we can't inject our spy:

```js
module.exports.create = async function create(user, callback, axios = require('axios') {
  // Auto-generated docs redacted
  return callback(null);
};
```
The default value of this `axios` instance will be used in the production environment. Now, let's get back to the test:

```js
const { test, describe } = require('node:test');
const { mockFn } = require('../utils/mock-extended');
const { AxiosSpy, HttpMethod } = require('../utils/axios.spy');
const { randomUUID } = require('node:crypto');
const { create } = require('../../databases/Database/create');

describe('Create user script', () => {
  test('successfully creates a user', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mockFn();
    const axiosSpy = new AxiosSpy();

    // Act
    await create(user, callback, axiosSpy);

    // Assert
    callback.shouldHaveBeenCalledOnceWith(null);
  });
  test('calls axios post with correct parameters', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mockFn();
    const axiosSpy = new AxiosSpy();

    // Act
    await create(user, callback, axiosSpy);

    // Assert
    axiosSpy
      .shouldHaveSentNumberOfRequests(1)
      .withUrl(new URL('https://backend/signup'))
      .withMethod(HttpMethod.Post)
      .withBody({
        email: user.email,
        password: user.password,
        passwordConfirmation: user.password,
      });
  });
  test.todo('throws validation error when user already exists');
  test.todo('throws error with descriptive message when something goes wrong');
});

function makeUserData() {
  return {
    email: 'user@mail.com',
    password: randomUUID(),
    tenant: 'test-tenant',
    client_id: 'test-client-id',
    connection: 'Database',
  };
}
```

Notice that we also had to change the first test case to inject the `AxiosSpy` instance, otherwise, it would try to use the actual `axios` object. We execute the test to see it failing, and then we can add the missing implementation:

```js
module.exports.create = async function create(
  user,
  callback,
  axios = require('axios')
) {
  const { email, password } = user;

  await axios.post(new URL('https://backend/signup'), {
    email,
    password,
    passwordConfirmation: password,
  });

  return callback(null);
};
```

This should suffice to make the test pass, so we'll dive into the next step: using global objects.

### Using global references in tests

Checking the next test we need to implement you'll notice it requires a globally defined object:

```js
test.todo('throws validation error when user already exists');
```

This `ValidationError` is an internal error created by Auth0's runtime that you can use without importing it. The `create` function template comes with the following comment:
```js
// 2. This user already exists in your database
  //     callback(new ValidationError("user_exists", "my error message"));
```

So, how do we make it available for our tests? Simple enough, just define a `ValidationError` class in the test file [global](https://nodejs.org/api/globals.html#global-objects) context:

```js
global.ValidationError = class ValidationError extends Error {
  constructor(code, message) {
    super(message);
    this.code = code;
  }
};
```

Now, we can write down the test specification:

```js
test('throws validation error when user already exists', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mockFn();
    const axiosSpy = new AxiosSpy();
    axiosSpy.stubResponseFor(
      HttpMethod.Post,
      new URL('https://backend/signup'),
      {
        status: 404,
        body: {
          error: 'user already exists',
        },
      }
    );

    // Act
    await create(user, callback, axiosSpy);

    // Assert
    callback.shouldHaveBeenCalledOnceWith(
      new ValidationError('user_exists', 'User already exists')
    );
  });
```

We can execute this test to guarantee it's failing for the right reason now: the `callback` function wasn't called with that validation error. Here's what we need to change to make it pass:

```js
module.exports.create = async function create(
  user,
  callback,
  axios = require('axios')
) {
const { email, password } = user;

try {
  await axios.post(new URL('https://backend/signup'), {
    email,
    password,
    passwordConfirmation: password,
  });
} catch (error) {
  return callback(new ValidationError('user_exists', 'User already exists'));
}
  return callback(null);
};

```

Now the test works again ✅. We can use this same strategy for other globally-defined objects in Auth0, such as the `configuration` object used to store sensitive information.

The final step now is to make sure the last test also passes. It should be straightforward, as we don't need any additional constructs:

```js
test('throws error with descriptive message when something goes wrong', async () => {
    // Arrange
    const user = makeUserData();
    const callback = mockFn();
    const axiosSpy = new AxiosSpy();
    axiosSpy.stubUnexpectedErrorFor(
      HttpMethod.Post,
      new URL('https://backend/signup'),
      new Error('Network error')
    );

    // Act
    await create(user, callback, axiosSpy);

    // Assert
    callback.shouldHaveBeenCalledOnceWith(
      new Error(
        'Something went wrong while trying to sign up user (Network error)'
      )
    );
  });
```

This test ensures we handle unexpected errors properly, wrapping their message into our own. Here's the implementation to make it pass:

```js
module.exports.create = async function create(
  user,
  callback,
  axios = require('axios')
) {
const { email, password } = user;

try {
  await axios.post(new URL('https://backend/signup'), {
    email,
    password,
    passwordConfirmation: password,
  });
} catch (error) {
  if (error.response?.status === 404)
    return callback(new ValidationError('user_exists', 'User already exists'));

  return callback(
    new Error(
      `Something went wrong while trying to sign up user (${error.message})`
    )
  );
}
  return callback(null);
};

```
Now all unit tests are passing, and we've completed the `create` script.

### Conclusion
Automated tests are undeniably essential to ensure our code's maintainability. Without proper tests, we can't trust any code changes and have to rely on expensive manual tests. Auth0's custom scripts aren't an exception—in fact, since they play such an important role in our system, they should be thoroughly tested. This article explained how to do it while also teaching a few tips and tricks.

If readers show enough interest, the next article in this series will discuss how to perform integration tests and use them in your CI. I hope this one has helped you start testing Auth0 scripts.

See you in the next one!