TDD course with AdonisJs - 6. Validation
Published 9/28/2019
This article is part of a series:
1 TDD course with AdonisJs - 1. Let's build a reddit clone
2 TDD course with AdonisJs - 2. Our first test
3 TDD course with AdonisJs - 3. Model factories & DB transactions
4 TDD course with AdonisJs - 4. Using the auth middleware
5 TDD course with AdonisJs - 5. Middlewares
6 TDD course with AdonisJs - 6. Validation
7 TDD course with AdonisJs - 7. Moderators
8 TDD course with AdonisJs - 8. Third party APIs, ioc and custom validators
Currently it is possible to create a thread without a body or title. So let's add validation to our controller.
You can find all the changes in this commit: https://github.com/MZanggl/tdd-adonisjs/commit/5e1e4cb1c4f78ffc947cdeec00609f4dfc4648ba
As always, let's create the test first.
test('can not create thread with no body', async ({ client }) => {
const user = await Factory.model('App/Models/User').create()
const response = await client.post('/threads').loginVia(user).send({ title: 'test title' }).end()
response.assertStatus(400)
})
And the response we get is:
1. can not create thread with no body
expected 200 to equal 400
200 => 400
Let's make the test pass by adding validation. Before we go into creating a custom validation though, let's first apply the easiest, simplest and fastest solution we can possibly think of. Adding the validation manually in the ThreadController. Put this at the top of the store method.
if (!request.input('body')) {
return response.badRequest()
}
And it passes!
Let's add the same thing for the title, we can do this in the same test even. It will look like this
test('can not create thread with no body or title', async ({ client }) => {
const user = await Factory.model('App/Models/User').create()
let response = await client.post('/threads').loginVia(user).send({ title: 'test title' }).end()
response.assertStatus(400)
response = await client.post('/threads').loginVia(user).send({ body: 'test body' }).end()
response.assertStatus(400)
})
Because we only added validation for the 'body' field, it will fail with the same error as before, so let's also add validation for the title field as well.
if (!request.input('body') || !request.input('title')) {
return response.badRequest()
}
And that makes the tests pass!
Check out my e-book!
Refactor
Let's try using Adonis' validation methods instead of the custom validation we have right now.
First, import the validator at the top of the ThreadController.
const { validate } = use('Validator')
Now, replace the custom validation with
const rules = { title: 'required', body: 'required' }
const validation = await validate(request.all(), rules)
if (validation.fails()) {
return response.badRequest()
}
Running this will fail, if you console.log response.error
in the tests, it will tell us that we haven't installed the validation dependency yet.
So let's do this by running the command
adonis install @adonisjs/validator
Next, go to start/app.js
and add the validator to the providers array.
const providers = [
// ...
'@adonisjs/validator/providers/ValidatorProvider'
]
And the tests pass. Finally let's take all this logic and put it in a separate file. First, let's make a validator file by running the following command:
adonis make:validator StoreThread
Next let's copy the rules from the ThreadController to the StoreThread.js file.
get rules () {
return {
title: 'required',
body: 'required'
}
}
And the way we can apply the validator is by adding it to "start/routes.js".
// start/routes.js
Route.resource('threads', 'ThreadController').only(['store', 'destroy', 'update'])
.middleware(new Map([
[['store', 'destroy', 'update'], ['auth']],
[['destroy', 'update'], ['modifyThreadPolicy']]
]))
.validator(new Map([
[['store'], ['StoreThread']],
]))
Let's refactor this later, it is becoming very complex...
Let's remove all the validation we had in the ThreadController. Then try running the tests again, still green!
Btw. we didn't add a unit test to the validator because that part is already tested by adonis, once we have a custom validator we would need to test it though.
Now that we have proper validation, we can also test the validation message it returns in our tests
response.assertJSONSubset([{ message: 'required validation failed on body' }])
However, this fails with the error expected {} to contain subset [ Array(1) ]
.
Taking a look at the documentation, AdonisJs' validator respects the 'accept' header and just doesn't return JSON by default. Let's fix this by adding the "accept JSON" header to our test.
await client.post('/threads').header('accept', 'application/json')...
Do this for both of the API requests in our test.
Resource routes provided us a benefit in the beginning but with middleware and validators added, it now looks more complicated than it needs to be.
routes.js
Route.resource('threads', 'ThreadController').only(['store', 'destroy', 'update'])
.middleware(new Map([
[['store', 'destroy', 'update'], ['auth']],
[['destroy', 'update'], ['modifyThreadPolicy']]
]))
.validator(new Map([
[['store'], ['StoreThread']],
]))
Let's simplify it again:
Route.group(() => {
Route.post('', 'ThreadController.store').middleware('auth').validator('StoreThread')
Route.put(':id', 'ThreadController.update').middleware('auth', 'modifyThreadPolicy')
Route.delete(':id', 'ThreadController.destroy').middleware('auth', 'modifyThreadPolicy')
}).prefix('threads')
Thanks to the "luxury" of having tests, we can change things around the way we want and don't have to worry about breaking things! See for yourself and run the tests.
Let's also add the validation to updating threads:
test('can not update thread with no body or title', async ({ client }) => {
const thread = await Factory.model('App/Models/Thread').create()
const user = await thread.user().first()
const put = () => client.put(thread.url()).header('accept', 'application/json').loginVia(user)
let response = await put().send({ title: 'test title' }).end()
response.assertStatus(400)
response.assertJSONSubset([{ message: 'required validation failed on body' }])
response = await put().send({ body: 'test body' }).end()
response.assertStatus(400)
response.assertJSONSubset([{ message: 'required validation failed on title' }])
})
This will fail, so let's also add the validator to the routes.js:
Route.put(':id', 'ThreadController.update').middleware('auth', 'modifyThreadPolicy').validator('StoreThread')
To complete all routes for our cruddy controller, let's add tests for fetching threads real quick.
test('can access single resource', async ({ client }) => {
const thread = await Factory.model('App/Models/Thread').create()
const response = await client.get(thread.url()).send().end()
response.assertStatus(200)
response.assertJSON({ thread: thread.toJSON() })
})
test('can access all resources', async ({ client }) => {
const threads = await Factory.model('App/Models/Thread').createMany(3)
const response = await client.get('threads').send().end()
response.assertStatus(200)
response.assertJSON({ threads: threads.map(thread => thread.toJSON()).sort((a, b) => a.id - b.id) })
})
The first test fetches a single thread, while the second one fetches all threads.
Note: if you are confused about the "sort" method: "Factory.createMany" currently creates the records in random order, and since we assign an autoincrement we have to sort the threads again to fix their order.
Here are the routes we have to add in "start/routes.js":
Route.get('', 'ThreadController.index')
Route.get(':id', 'ThreadController.show')
and the methods in "ThreadController":
async index({ response }) {
const threads = await Thread.all()
return response.json({ threads })
}
async show({ params, response }) {
const thread = await Thread.findOrFail(params.id)
return response.json({ thread })
}
And that's it. Next time we will revisit the existing authorization tests and add the possibility for moderators to modify and delete threads!