TDD course with AdonisJs - 6. Validation
Tags: tdd, testing, node, javascript
Published: 2019-09-28 06:02:28

> Originally posted at [michaelzanggl.com](https://michaelzanggl.com/articles/tdd-with-adonisjs-6). Subscribe to [my newsletter](https://michaelzanggl.com/) to never miss out on new content.

Currently it is possible to create a thread without a body or title. So let's add validation to our methods.

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.

```javascript
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.

```javascript
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

```javascript
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.

```javascript
if (!request.input('body') || !request.input('title')) {
  return response.badRequest()
}
```

And that makes the tests pass!

# 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.

```javascript
const { validate } = use('Validator')
```

Now, replace the custom validation with
```javascript
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

```bash
adonis install @adonisjs/validator
```

Next, go to `start/app.js` and add the validator to the providers array.

```javascript
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:

```bash
adonis make:validator StoreThread
```

Next let's copy the rules from the ThreadController to the StoreThread.js file.

```javascript
get rules () {
    return {
      title: 'required', 
      body: 'required'
    }
  }
```

And the way we can apply the validator is by adding it to "start/routes.js".

```javascript
// 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

```javascript
  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.

```javascript
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
```javascript
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:

```javascript
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:

```javascript
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:

```javascript
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.

```javascript
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":

```javascript
Route.get('', 'ThreadController.index')
Route.get(':id', 'ThreadController.show')
```

and the methods in "ThreadController":

```javascript
    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!