Test Lambdas
On this page
In this guide, you create a JWT populate lambda in a local FusionAuth instance, then change the lambda once with the FusionAuth CLI. You test the change in two ways:
- Before you upload the change, a unit test runs the lambda in a Node.js harness, without FusionAuth.
- After you upload the change, an integration test logs in to FusionAuth and checks the token FusionAuth issues.
The lambda round trip in this guide.
For the objects and methods available in a lambda, see Lambdas.
Prerequisites#
To follow this guide, you need:
Start FusionAuth#
Create a folder for the project, and open a terminal in the folder. Download the FusionAuth Docker Compose file:
$ curl -o docker-compose.yml https://fusionauth.io/docs/docker-compose.yml
Create a kickstart folder in the project folder. Save the following file in the kickstart folder as kickstart.json:
{
"variables": {
"applicationId": "E9FDB985-9173-4E01-9D73-AC2D60D1DC8E",
"apiKey": "lambda_testing_key",
"defaultTenantId": "d7d09513-a3f5-401c-9685-34ab6c552453",
"adminEmail": "admin@example.com",
"adminPassword": "password",
"userEmail": "richard@example.com",
"userPassword": "password",
"userUserId": "00000000-0000-0000-0000-111111111111"
},
"apiKeys": [
{
"key": "#{apiKey}",
"description": "Unrestricted API key"
}
],
"requests": [
{
"method": "POST",
"url": "/api/user/registration",
"body": {
"user": {
"email": "#{adminEmail}",
"password": "#{adminPassword}"
},
"registration": {
"applicationId": "#{FUSIONAUTH_APPLICATION_ID}",
"roles": [
"admin"
]
}
}
},
{
"method": "POST",
"url": "/api/application/#{applicationId}",
"tenantId": "#{defaultTenantId}",
"body": {
"application": {
"name": "Example app",
"roles": [
{ "name": "admin" },
{ "name": "editor" },
{ "name": "viewer" }
]
}
}
},
{
"method": "POST",
"url": "/api/user/registration/#{userUserId}",
"body": {
"user": {
"email": "#{userEmail}",
"firstName": "Richard",
"lastName": "Hendricks",
"password": "#{userPassword}"
},
"registration": {
"applicationId": "#{applicationId}",
"roles": [
"editor"
]
}
}
}
]
}FusionAuth reads the KickstartA file of API requests that FusionAuth runs on first startup to configure a new instance. file on first start. The Kickstart file creates the following:
- An application named
Example appwith the rolesadmin,editor, andviewer. - An API key with the value
lambda_testing_key. - A user with the email
richard@example.com, the passwordpassword, and theeditorrole. - An administrator with the email
admin@example.comand the passwordpassword.
Start FusionAuth:
$ docker compose up -d
FusionAuth runs at http://localhost:9011. For more about the Docker Compose file, see Install FusionAuth.
The Kickstart file contains passwords and an unrestricted API key. Use these values only on a local instance, and never commit real secrets to version control.
Create the Lambda#
The following lambda adds a message claim and a permissions claim to the access token. The permissions depend on the user's roles for the application:
function populate(jwt, user, registration) {
jwt.message = 'Hello World!';
var roles = registration && registration.roles ? registration.roles : [];
jwt.permissions = [];
if (roles.includes('admin')) {
jwt.permissions.push('all');
} else if (roles.includes('editor')) {
jwt.permissions.push('read', 'write');
} else if (roles.includes('viewer')) {
jwt.permissions.push('read');
}
}To create the lambda:
- Log in to the Admin UI as
admin@example.com. - Navigate to Customizations -> Lambdas .
- Click .
- Set Id to
f3b3b547-7754-452d-8729-21b50d111505. - Set Name to
[ATest]. The brackets sort the lambda to the top of the list. - Set Type to
JWT Populate. - Replace the body with the lambda code.
- Click .

To assign the lambda to the application:
- Navigate to Applications and click on
Example app. - Select the JWT tab.
- Under Lambda settings , set Access Token populate lambda to
[ATest]. - Click .

To create and assign a lambda from a script instead, use the Lambda API and the Application API.
Confirm the Lambda Runs#
Log in as richard@example.com with the Login API:
$ curl -X POST http://localhost:9011/api/login \
-H "Authorization: lambda_testing_key" \
-H "Content-Type: application/json" \
-d '{"applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e", "loginId": "richard@example.com", "password": "password"}'
Copy the token value from the response, and paste the value into Token in the FusionAuth JWT Decoder. The decoded payload contains the following claims:
"roles": ["editor"],
"message": "Hello World!",
"permissions": ["read", "write"]
Download the Lambda With the CLI#
The FusionAuth CLI downloads and uploads lambdas, so you can keep lambda code in version control. The CLI can retrieve, update, and delete a lambda. To create a lambda or assign a lambda to an application, use the Admin UI, the Lambda API, or a client library.
The CLI needs an API key. The Kickstart file created lambda_testing_key, which has access to every endpoint. In production, create a key limited to the endpoints you use, such as /api/lambda. See Managing API Keys.
Create a Node.js project in the project folder, and install the CLI:
$ npm init -y
$ npm install --save-dev @fusionauth/cli@^1.9.0
Download the lambda:
$ npx fusionauth lambda:retrieve f3b3b547-7754-452d-8729-21b50d111505 --key lambda_testing_key
The CLI saves the lambda to lambdas/f3b3b547-7754-452d-8729-21b50d111505.json. The file contains the Lambda object from the Lambda API, including the body and type fields. To skip --key on each command, set the FUSIONAUTH_API_KEY environment variable. The CLI connects to http://localhost:9011 unless you pass --host or set FUSIONAUTH_HOST.
Change the Lambda#
In the downloaded file, find Hello World! in the body value and change the text to Goodbye World!. Don't upload the change yet.
Test the Change With a Unit Test#
A unit test runs the lambda without FusionAuth, so the test is fast and needs no running instance. The example-lambdas folder of the fusionauth-site repository contains a harness that runs a lambda the way FusionAuth does. The harness has no dependencies. The harness provides only the globals FusionAuth provides, such as fetch, Headers, and console. The harness passes arguments through JSON, discards changes to read-only arguments, and applies the FusionAuth rules for reserved JWT claims. A lambda that passes a harness test runs the same way in FusionAuth, apart from engine differences such as locale data.
Download the harness into a harness folder:
$ curl -fs --create-dirs -o "harness/#1" "https://raw.githubusercontent.com/FusionAuth/fusionauth-site/main/astro/extractedcode/example-lambdas/harness/{index.js,lambda-types.js,sandbox.js,package.json}"
Create lambda.test.mjs with the following code:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { loadLambda } from './harness/index.js';
// the file that npx fusionauth lambda:retrieve downloaded
const file = new URL('./lambdas/f3b3b547-7754-452d-8729-21b50d111505.json', import.meta.url);
const { body, type } = JSON.parse(readFileSync(file, 'utf8'));
const lambda = loadLambda(body, type);
function populate(registration) {
return lambda.invoke({ jwt: {}, user: { email: 'richard@example.com' }, registration }).jwt;
}
function registrationWith(roles) {
return { applicationId: 'e9fdb985-9173-4e01-9d73-ac2d60d1dc8e', roles };
}
test('sets the updated message', () => {
assert.equal(populate(registrationWith([])).message, 'Goodbye World!');
});
test('admins get all permissions', () => {
assert.deepEqual(populate(registrationWith(['admin', 'viewer'])).permissions, ['all']);
});
test('editors can read and write', () => {
assert.deepEqual(populate(registrationWith(['editor'])).permissions, ['read', 'write']);
});
test('users without a registration get no permissions', () => {
assert.deepEqual(populate(null).permissions, []);
});The test reads the body and type fields from the downloaded file and passes them to loadLambda(). Each test calls invoke() with the lambda arguments by name, then checks the jwt that FusionAuth would keep. The test needs only the fields the lambda reads, so the user and registration objects stay small. The .mjs extension and the package.json file in the harness folder let the test use ES modules in a project that uses CommonJS.
Run the test:
$ node --test lambda.test.mjs
Node.js reports four passing tests. If you skipped the change to Goodbye World!, the first test fails. Run the unit test after every change, such as in a CI job, before you upload the lambda.
The harness runs lambdas with the Node.js vm module, which isn't a security boundary. A malicious lambda can escape the harness and reach your file system and network. Run only lambdas you trust, such as lambdas from your own FusionAuth instance and source control.
Upload the Lambda#
Upload the changed lambda:
$ npx fusionauth lambda:update f3b3b547-7754-452d-8729-21b50d111505 --key lambda_testing_key
To confirm the upload, navigate to Customizations -> Lambdas in the Admin UI and open [ATest]. The body now sets the message to Goodbye World!.
Test the Upload With an Integration Test#
An integration test runs the lambda in FusionAuth. The test logs in and checks the token FusionAuth issues. An integration test catches problems that a unit test can't, such as an unassigned lambda or a missing license.
The integration test uses the TypeScript client library to call FusionAuth, tape as the test runner, and jsonwebtoken to decode the token. Install the packages:
$ npm install --save-dev @fusionauth/typescript-client@^1.70.0 tape@^5.6.6 jsonwebtoken@^9.0.1
Create userLogin.js with the following code:
const client = require("@fusionauth/typescript-client");
const jwt = require("jsonwebtoken");
async function login(loginId, password, applicationId, fusionUrl) {
const fusion = new client.FusionAuthClient("lambda_testing_key", fusionUrl);
// the client library rejects with the response when the login fails
const clientResponse = await fusion.login({ applicationId, loginId, password });
return jwt.decode(clientResponse.response.token);
}
module.exports = {
login,
};login() calls the Login APIThe FusionAuth API that logs a user in without the hosted login pages. and returns the claims of the token in the response. When the login fails, the client library rejects the promise with the response, so the test fails with the error from FusionAuth.
Create test_1.js with the following code:
const test = require("tape");
const { login } = require("./userLogin.js");
const applicationId = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e";
const fusionUrl = "http://localhost:9011";
test("the access token has the claims from the uploaded lambda", async function (t) {
const claims = await login("richard@example.com", "password", applicationId, fusionUrl);
t.equal(claims.message, "Goodbye World!");
t.deepEqual(claims.permissions, ["read", "write"]);
});The test logs in as richard@example.com, the user the Kickstart file created. The test checks that the token contains the message from the uploaded lambda and the permissions for the editor role.
Run the test:
$ node test_1.js
The output looks like the following:
TAP version 13
# the access token has the claims from the uploaded lambda
ok 1 should be strictly equal
ok 2 should be deeply equivalent
1..2
# tests 2
# pass 2
# ok
FusionAuth can run the previous lambda body for a few seconds after an upload. If the test fails right after lambda:update, run the test again.
Test a Lambda That Calls an API#
A lambda that calls an API with fetch() needs Lambda HTTP Connect, which requires the Essentials or Enterprise plan. A unit test doesn't need a plan, because the harness answers fetch() calls with mock responses. A request with no matching mock throws an error, so the test can't reach the network.
The following test checks the lambda from Call an API from a Lambda. The test mocks the billing API, supplies the lambda secret, and checks the fallback when the API fails:
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { loadLambdaFile, networkError } from '../../harness/index.js';
import { fixture } from '../../fixtures/index.js';
const billingUrl = 'https://billing.example.com/api/customers/00000000-0000-0000-0000-000000000002';
const secrets = { 'billing-api-key': 'test-billing-key' };
function run(fetch) {
const lambda = loadLambdaFile(new URL('./billing-tier.js', import.meta.url), 'JWTPopulate', { fetch, secrets });
return lambda.invoke({ jwt: fixture('jwt'), user: fixture('user'), registration: fixture('registration') }, { context: { scopes: [] } });
}
test('adds the tier from the billing API', () => {
const { jwt, requests } = run([{ url: billingUrl, response: { body: { tier: 'enterprise' } } }]);
assert.equal(jwt['https://example.com/tier'], 'enterprise');
assert.equal(requests[0].headers.Authorization[0], 'Bearer test-billing-key');
assert.equal(requests[0].connectTimeout, 1000);
assert.equal(requests[0].readTimeout, 1500);
});
test('falls back to basic on an error status', () => {
const { jwt, logs } = run([{ url: billingUrl, response: { status: 503 } }]);
assert.equal(jwt['https://example.com/tier'], 'basic');
assert.match(logs.error, /status 503/);
});
test('falls back to basic when the API is unreachable', () => {
const { jwt, logs } = run(() => networkError());
assert.equal(jwt['https://example.com/tier'], 'basic');
assert.match(logs.error, /status -1/);
});invoke() also returns logs, the console output, and requests, the fetch() calls the lambda made. requests lists only the headers the lambda set, not the default headers FusionAuth adds. For the full list of harness options, see the harness README.
A mock can't confirm that the real API accepts the request. After the unit tests pass, upload the lambda and run an integration test against a licensed FusionAuth instance.
Related
Learn about the FusionAuth CLI tool.
Learn about lambdas, the JavaScript functions that customize how FusionAuth logs users in and issues tokens.
Learn how to call an external API from a lambda.
Learn how to allow or reject the CIMD clients that register with a CIMD domain.