You’re a good developer. You like testing your code. Security is important to you, so you also decide that you’ll use a 3rd party service for authentication and choose Auth0. Out-of-the-box, however, doesn’t quite get you what you need so you add some custom rules and scripts to it. You realize, however, that even though these scripts run in a NodeJS instance on their side, they don’t follow conventional patterns. How do I go about unit testing auth0 scripts, you ask yourself?
If that sounds like you then you’ve come to the right place. I, too, found myself in this predicament. Today’s post will go over some patterns you can employ for unit testing Auth0 scripts — specifically rules. Find today’s code in this Github repo.
Framing the problem
If you’re not familiar with the Auth0 rules engine, let’s briefly summarize it here. Auth0 allows creating rules that execute during the authentication pipeline. These rules have the opportunity to modify the user, the access_token, id_token, or other portions of the pipeline. A common use case is to modify the JWT token by adding claims.
Auth0 rules integrate into the pipeline a bit weird; they do not follow common javascript conventions. That is, they do not export their bindings. Due to that lack of an export declaration, it becomes seemingly impossible to test them. Luckily, however, that is not the case. Auth0 themselves have provided some guidance for testing best practices. Let’s take that guidance as our springboard.
I want to note that this same issue exists with DB Connection custom scripts as well. Hooks and the recently introduced Actions feature both properly use module exports. As such, the examples here apply to DB connection scripts but not hooks or actions.
We want to test our scripts because it makes us better. That, and it helps us feel better about putting it into production, right?
Getting Started
For the following examples and code to work you’ll need at least NodeJS version 10. I’m personally using 12.18.3 at the time of writing. Next up is a CLI-deployable Auth0 tenant configuration folder. I find it easier to start by exporting an existing placeholder tenant configuration to a local folder with a tenant.yaml
file and associated scripts. My example project uses this method. You will also find this post easier if you have some familiarity with Jasmine. If you don’t, that’s ok, my examples should hopefully serve as good starters.
So now that we have the basics down, let’s dive right in.
Setting up the environment
Let’s operate on the pretext that you have an Auth0 project with some rules. In order to start writing and running tests, you need a test harness. Let’s go ahead and run npm i jasmine jasmine-console-adapter jasmine-core jasmine-node --save-dev
. Next up, I know that we’re going to have a few other dependencies as well. How about we just get them ready: npm i js-yaml fs vm lodash --save-dev
. If you make use of Axios to call out from your rules, let’s add that and a mocking adapter: npm i axios axios-mock-adapter --save-dev
.
Two questions:
- Why save-dev on all those? You don’t need them for deployment. This doesn’t package it up for sending to a server so I suppose it doesn’t really matter which way you go here.
- What are all those? Briefly;
jasmine
(and extensions) is the test harness and some additional functionality.js-yaml
is a package used to load yaml files into a JSON object.fs
is used for file-system access. Next,vm
is a virtual machine module. Lastly,Axios
is used for http calls.
Now we need the ability to run tests. Let’s edit our package.json and add the following script: "test": "jasmine --reporter=jasmine-console-reporter"
. This will run Jasmine and output to the console. Simple enough. If you happen to compare your package.json to mine, you might also notice I have a “deploy” script. That is just a quick and simple way for me to run the Auth0 CLI deployment tool.
Organizing the code
The next stop on our learning journey has to do with organization. Look, I don’t want to be prescriptive here. What follows works for me but that doesn’t mean you have to do it. I have all my Tenant related configuration and scripts all in a Tenant.Configuration
folder. Under there I organize them into hooks, rules, databases, emails, pages, etc. Basically, every resource that the YAML file refers to has a corresponding sub-folder.
Sibling to that folder you will find a spec folder. When you run npx jasmine init
, this is the result. I’ve taken it a step farther, however. I further sub-divide that spec folder into integration-tests
, unit-tests
, fakes
, and test-helpers
folders. This just helps me put things in logical groupings.
The fakes folder just contains some class fakes for test purposes. In this case, just a few fake Error sub-classes. Next, the test-helpers folder contains just what it sounds like: helpers. Some default data, some helper functions, and perhaps most importantly, our vm-context-helpers.js file
. We’ll come back to that shortly. The other two folders should describe themselves pretty accurately.
The glue that binds us
I mentioned vm-context-helpers.js
, didn’t I? This is the key to everything. The vm
module (as per description) enables compiling and running code within V8 Virtual Machine contexts. I wrote a little helper on top of that module to further simplify my life. It allows passing an object with a filePath and fileName, and then any number of additional parameters. You call this helper which executes the file you requested and passes those arguments to it. In the case of a rule, that would be the user
, context
, and callback
parameters.
Example execution:
runFile({
filePath: `./Tenant.Configuration/rules/do-something-else.js`,
fileName: 'do-something-else.js'
}, specUser, testContext, callback);
Putting it all together
While the actual spec suite file defining a test has a bit more going on, below you’ll find a sample it
declaration. These are individual specs, as Jasmine calls them. The following example is from this file.
it('succeeds doing something else', async () => {
// arrange
let specUser = _.merge({}, testUser);
let statusCode = 200;
let callback = (err, user, context) => {
expect(err).toBeNull();
};
mockAxios.onGet(/fakeurl/).reply(statusCode);
// act
runFile({
filePath: `./Tenant.Configuration/rules/do-something-else.js`,
fileName: 'do-something-else.js'
}, specUser, testContext, callback);
await flushPromises();
// assert
expect(console.log).toHaveBeenCalledWith(LOG_PREFIX, 'Doing something else');
expect(console.log).toHaveBeenCalledWith(LOG_PREFIX, 'Successfully did something else');
});
Let’s go over the magic here because there are a couple of gotchas I ran into. First, you will notice that I declared this with an async function. This is because my rule itself is an async function which uses async/await
on an Axios call. I set up a mock around my Axios call and my intended reply status code. I have three assertions in this test as well. One actually occurs within the callback and not in the “normal” spot (at the bottom of the test with the assertions). I could do it down there, of course, but I didn’t want to add additional logic for the sake of lining my assertions up in a neat row.
What I hope you note, however, is the await flushPromises()
call right before my assertions. I found that when executing the tests and without this, the assertions would actually fire early. I needed to flush (or clear) the promises in order to guarantee execution order here. A quick Google search on the problem I ran into led me to this StackOverflow Q/A about it.
Another note I want to call out is my usage of Lodash instead of Object.assign
/Object.extend
. Simply put: Lodash allows a deep clone where Object.assign does not. That’s all. None of the tests in this library actually need a deep clone but my real-life tests do.
Faking it
Some of the code that magically exists on Auth0 side needs to “exist” in order for our code to execute. That’s where our fakes and other helpers come in. I’ve “faked” the ValidationError class and specifically used it within a rule for sample purposes. I use beforeEach to assign it as a global and afterEach to remove it from globals. I use a similar method to attach a DEBUG method and other global functions/data. Here is a small sampling of that in action:
beforeEach(function () {
testUser = _.merge({}, defaultUser);
testContext = _.cloneDeep(defaultRuleContext);
setGlobals();
global.helpers = defaultHelpers;
global.require = require;
// create axios mock
mockAxios = new MockAdapter(axios);
spyOn(console, 'log');
})
afterEach(function () {
restoreGlobals();
global.require = undefined;
// restore axios mock
mockAxios.restore();
})
“Integration” tests
I’m quoting this on purpose. It isn’t really an integration test, but it is a seemingly good way to fake it anyway. I use js-yaml to load my tenant.yaml file in and get a list of rules. From there, I can now execute them in order to ensure they “integrate” and execute properly. The sample is pretty contrived but I hope it helps illustrate one way to go about it.
That’s all I really want to say on that matter.
Conclusion
Testing code is important. Testing Auth0 code is hard. We’ve gone over a way for unit testing Auth0 scripts by using the vm
module. I also showed how we can use that same method to do “integration” testing.
Additional Resources
I do wish to call out that I had some help along my way. I’ve already linked a few resources I had used when trying to figure out unit testing auth0 scripts, but here are a couple more:
- Jamie Tanna’s post about Unit Testing Auth0 Rules.
- Laurent Dutheil’s post about How to do TDD on Auth0 scripts.
Credits
Photo by Testalize.me on Unsplash