This is one of a two-part series. You can also learn how to automate the deployment of FeathersJS apps to Heroku.
In the software development lifecycle, testing offers benefits that reach far beyond the code itself. Testing assures all parties (developers, clients, project managers, etc) that, while the application may not be completely bug-free, it does what is expected, as expected. By highlighting any regressions that are introduced, testing also provides the confidence to make adjustments and improvements to the code.
Most development teams manage code from a central repository, using a Version Control System (VCS) to push updates and deploy to production servers. Ideally, tests are run before pushing to the central repository and after deploying to the production servers. This allows any issues to be spotted and dealt with before a user encounters them. In the past, this manual process introduced a bottleneck because updates could not be deployed as soon as possible.
There was also the risk of poor user experience if an issue was identified while testing the deployment, causing the team to temporarily disable the application while the issue was being resolved. These are just two of the issues that are remedied by the automation of the testing process before deploying a solution to the production environment.
In this article, I will show you how to automate the testing for a FeathersJS application using CircleCI. To help with writing tests, we will be using Mocha.
Prerequisites
Before you start, make sure these items are installed on your system:
- A minimum NodeJS version of 10.0.0
- An up-to-date JavaScript package manager such as NPM or Yarn
- The FeathersJS CLI
You can install the FeathersJS CLI by running this command:
npm install -g @feathersjs/cli
For repository management and continuous integration/continuous deployment, you need:
Our tutorials are platform-agnostic, but use CircleCI as an example. If you don’t have a CircleCI account, sign up for a free one here.
Getting started
Create a new folder for the project:
mkdir auth-api-feathersjs
cd auth-api-feathersjs
Next, generate a new app using the Feathers CLI generate
command:
feathers generate app
For this project, we will be using JavaScript to create a REST API. Respond to the questions from the CLI as shown here:
? Do you want to use JavaScript or TypeScript? JavaScript
? Project name auth-api-feathersjs
? Description
? What folder should the source files live in? src
? Which package manager are you using (has to be installed globally)? npm
? What type of API are you making? REST
? Which testing framework do you prefer? Mocha + assert
? This app uses authentication Yes
? Which coding style do you want to use? ESLint
? What authentication strategies do you want to use? (See API docs for all 180+
supported oAuth providers) Username + Password (Local)
? What is the name of the user (entity) service? users
? What kind of service is it? NeDB
? What is the database connection string? nedb://../data
Once the CLI completes the scaffolding of the application, you can open the project in whatever code editor you prefer.
FeathersJS provides some basic tests to ensure the project works. You can find the files in the test
folder at the root of the project. Run the tests using this command:
npm test
Expect this output:
Feathers application tests
✓ starts and shows the index page
404
info: Page not found {"className":"not-found","code":404,"data":{"url":"/path/to/nowhere"},"errors":{},"name":"NotFound","type":"FeathersError"}
✓ shows a 404 HTML page
info: Page not found {"className":"not-found","code":404,"data":{"url":"/path/to/nowhere"},"errors":{},"name":"NotFound","type":"FeathersError"}
✓ shows a 404 JSON error without stack trace
authentication
✓ registered the authentication service
local strategy
✓ authenticates user and creates accessToken (79ms)
'users' service
✓ registered the service
6 passing (282ms)
Configuring CircleCI
Next, add the pipeline configuration for CircleCI. For this project, the pipeline will consist of one step:
- Build and Test - Here we build the project, install the project dependencies and run project tests.
At the root of your project, create a folder named .circleci
and within it, create a file named config.yml
. In the newly created file, add this configuration:
# Use the latest 2.1 version of CircleCI pipeline process engine.
version: 2.1
orbs:
node: circleci/node@5.0.2
jobs:
build-and-test:
executor: node/default
steps:
- checkout
- node/install-packages:
cache-path: ~/project/node_modules
override-ci-command: npm install
- run: npm test
workflows:
test-my-app:
jobs:
- build-and-test
This configuration uses the Node.js orb circleci/node
to install packages with caching enabled by default. It also makes npm available for you to run your tests.
The pipeline has just one job, build-and-test
, for the node orbte to execu. The first step in this job is pulling the code from the GitHub repository. Next, it installs the packages specified in the package.json
file. This process is accelerated by using the cache in the specified directory. The configuration overrides the default command for installing packages by using override-ci-command
. This ensures that the correct installation command for this project is passed.
The last step in this job is to run the npm test
command.
Setting up the project on GitHub
Now you need to convert this project into a Git repository and then set it up on GitHub. See this post for help: pushing your project to GitHub.
Log into your CircleCI account. If you signed up with your GitHub account, all your repositories will be displayed on your dashboard.
Click Set Up Project for the auth-api-feathersjs
project.
Enter the name of the branch where your code is housed on GitHub. Then click Set Up Project.
Your first build process will start running and complete successfully!
Click build-and-test to review the job steps and the status of each job.
Adding tests to your FeathersJS application
One of the major selling points of FeathersJS is that it makes it easy to build prototypes in minutes and production-ready apps in days. Without writing a line of code, you already have an API endpoint to handle registration and authentication. You also have endpoints to get all users, update a user, and delete a user. The API includes these endpoints:
GET /users
lists all users page by page.POST /users
creates a new user. This endpoint will be used for registration.POST /authentication
authenticates a user using the provided strategy. For this tutorial, we are using ‘local’ authentication. This strategy uses an email address and password combination saved in the local database.GET /users/123
returns the details of the user with id 123. You can also include queries in this request, like/users/123?email=yemiwebby@circleci.com
.PATCH /users/123
andPUT /users/123
update the details of the user with id 123.DELETE /users/123
deletes the user with id 123.
You can test these endpoints in Postman. Run the server using this command:
npm run dev
Notice that the password of the newly created user isn’t returned in the JSON response. This is specified out-of-the-box in the user service hook (located in src/services/users/users.hooks.js
).
The user service hook also helps with ensuring that before a user can make requests to any endpoint (apart from the registration endpoint) a JWT token is specified in the header of the request.
You have completed an important step in this tutorial, but there are still some steps remaining. Assume that your application has a security requirement that a user can delete only their own account. Trying to delete another user’s account should return a 403
error response. The same should be true for trying to update or patch another user’s account.
Your next step is to write a test suite for this security requirements. To get started, you will need to set up a database to use only with the tests. To do so, update the test environment configuration in config/test.json
with this:
{
"nedb": "../test/data"
}
You will also need to make sure that the database is cleaned up before every test run. To make that possible across platforms, first, run:
npm install shx --save-dev
Next, update the scripts
section of the package.json file to this:
"scripts": {
"test": "npm run lint && npm run mocha",
"lint": "eslint src/. test/. --config .eslintrc.json --fix",
"dev": "nodemon src/",
"start": "node src/",
"clean": "shx rm -rf test/data/",
"mocha": "npm run clean && NODE_ENV=test mocha test/ --recursive --exit"
},
This will make sure that the test/data
folder is removed before every test run.
Finally, update the code in test/services/users.test.js
to match this:
const axios = require('axios');
const assert = require('assert');
const url = require('url');
const app = require('../../src/app');
const port = app.get('port') || 8998;
const getUrl = (pathname) =>
url.format({
hostname: app.get('host') || 'localhost',
protocol: 'http',
port,
pathname,
});
describe('\'users\' service', () => {
it('registered the service', () => {
const service = app.service('users');
assert.ok(service, 'Registered the service');
});
});
describe('Additional security checks on user endpoints', () => {
let alice = {
email: 'alice@feathersjs.com',
password: 'supersecret12',
};
let bob = {
email: 'bob@feathersjs.com',
password: 'supersecret1',
};
const getTokenForUser = async (user) => {
const { accessToken } = await app.service('authentication').create({
strategy: 'local',
...user,
});
return accessToken;
};
const setupUser = async (user) => {
const { _id } = await app.service('users').create(user);
user._id = _id;
user.accessToken = await getTokenForUser(user);
};
let server;
before(async () => {
await setupUser(alice);
await setupUser(bob);
server = app.listen(port);
});
after(async () => {
server.close();
});
it('should return 403 when user tries to delete another user', async () => {
const { accessToken } = alice;
const { _id: targetId } = bob;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
try {
await axios.delete(getUrl(`/users/${targetId}`), config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
it('should return 403 when user tries to put another user', async () => {
try {
const { accessToken } = bob;
const { _id: targetId } = alice;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
const testData = { password: bob.password };
await axios.put(getUrl(`/users/${targetId}`), testData, config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
it('should return 403 when user tries to patch another user', async () => {
try {
const { accessToken } = alice;
const { _id: targetId } = bob;
const config = { headers: { Authorization: `Bearer ${accessToken}` } };
const testData = { password: alice.password };
await axios.patch(getUrl(`/users/${targetId}`), testData, config);
} catch (error) {
const { response } = error;
assert.equal(response.status, 403);
assert.equal(
response.data.message,
'You are not authorized to perform this operation on another user'
);
}
});
});
In this test suite, we have two actors (Alice and Bob) registered on our application.
In one test scenario, Alice tries to delete Bob’s account. Because this is not permitted, you can expect the API to return a 403
response.
In the second scenario, Bob tries to make a PUT
request to reset Alice’s password. If this request were to be handled successfully, Bob would be able to log in as Alice and use her account however he pleases. You want to prevent this and expect a 403
response to be returned.
The third scenario is similar to the second except this time Alice tries to reset Bob’s password via an UPDATE
request. Again, you can expect a 403
response to be returned.
Great stuff! You have the code and the tests for your expected scenarios. All that is left to do is push your code and let the new features take effect.
git commit -am 'Additional security checks on user endpoints'
git push origin main
Go back to CircleCI to see your new build running beautifully. Everything is smooth sailing until we there is a build failed message. What could be the problem?
It looks like the application fails the tests in the new test suite. The test reports reveal that in the first scenario, Alice successfully deleted Bob’s account. To make things even more interesting, because we deleted Bob’s account, the API returned 404
responses in the second and third scenarios.
Imagine that it turns out you still need to write the code for this feature and that you forgot to run the tests locally before pushing them to the central repository. This scenario may seem contrived, but the truth is that mistakes like this happen. Having an automated testing process prevents these kinds of human error. Because a layer of tests must be passed before the update is deployed, this nightmare will never get past the test environment.
Well, you should fix this bug before someone finds out. By adding a verification check to the user service hook, you can prevent users from performing certain operations on other accounts.
Open src/services/users/users.hooks.js
. Just above the line beginning with module.exports = {
, add this:
...
const {Forbidden} = require('@feathersjs/errors');
const verifyCanPerformOperation = async context => {
const {_id: authenticatedUserId} = context.params.user;
const {id: targetUserId} = context;
if (authenticatedUserId !== targetUserId) {
throw new Forbidden('You are not authorized to perform this operation on another user');
}
};
...
This function takes the hook context as a parameter and gets two values from it: the id
of the authenticated user and the id
of the target user. If these values are not the same, then a forbidden
error is thrown. This error is handled by the hook’s error handler, which returns a 403
response.
Next, update the before
entry in src/services/users/users.hooks.js
to match this:
...
before: {
all: [],
find: [authenticate('jwt')],
get: [authenticate('jwt')],
create: [hashPassword('password')],
update: [hashPassword('password'), authenticate('jwt'), verifyCanPerformOperation],
patch: [hashPassword('password'), authenticate('jwt'), verifyCanPerformOperation],
remove: [authenticate('jwt'), verifyCanPerformOperation]
},
...
With this change, the verification check is carried out after the authentication check for the update
, patch
, and remove
service methods. These methods are mapped to the PUT
, PATCH
,and DELETE
endpoints respectively. This ensures access to the logged-in user to retrieve their id.
All that is left is to run the tests locally to be sure everything is in order, commit the latest changes, and then push these changes to your GitHub repository. This triggers the CircleCI build-and-test
pipeline.
Nice work!
Conclusion
In this article, you built an authentication and user management API using FeathersJS. You also set up a CircleCI pipeline to automatically test changes to the repo before deploying the changes to the production server.
The benefit of this approach is that human error does not compromise the security of your application. By automating the testing process, you removed the risk of human error wreaking unexpected havoc in the production environment. It also adds an additional level of quality control and assurance to the software being maintained. Give continuous integration a try and make code base bottlenecks a thing of the past for your team!
The entire codebase for this tutorial is available on GitHub.
Oluyemi is a tech enthusiast with a background in Telecommunication Engineering. With a keen interest in solving day-to-day problems encountered by users, he ventured into programming and has since directed his problem solving skills at building software for both web and mobile. A full stack software engineer with a passion for sharing knowledge, Oluyemi has published a good number of technical articles and blog posts on several blogs around the world. Being tech savvy, his hobbies include trying out new programming languages and frameworks.