TDD course with AdonisJs - 6. Validation

michi

Michael Z

Posted on September 28, 2019

TDD course with AdonisJs - 6. Validation

Originally posted at michaelzanggl.com. Subscribe to my newsletter 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.

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)
})
Enter fullscreen mode Exit fullscreen mode

And the response we get is:

  1. can not create thread with no body
  expected 200 to equal 400
  200 => 400
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
})
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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.

const { validate } = use('Validator')
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

const providers = [
  // ...
  '@adonisjs/validator/providers/ValidatorProvider'
]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

get rules () {
    return {
      title: 'required', 
      body: 'required'
    }
  }
Enter fullscreen mode Exit fullscreen mode

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']],
    ]))
Enter fullscreen mode Exit fullscreen mode

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' }])
Enter fullscreen mode Exit fullscreen mode

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')...
Enter fullscreen mode Exit fullscreen mode

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']],
    ]))
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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' }])
})
Enter fullscreen mode Exit fullscreen mode

This will fail, so let's also add the validator to the routes.js:

Route.put(':id', 'ThreadController.update').middleware('auth', 'modifyThreadPolicy').validator('StoreThread')
Enter fullscreen mode Exit fullscreen mode

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) })
})
Enter fullscreen mode Exit fullscreen mode

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')
Enter fullscreen mode Exit fullscreen mode

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 })
    }
Enter fullscreen mode Exit fullscreen mode

And that's it. Next time we will revisit the existing authorization tests and add the possibility for moderators to modify and delete threads!

💖 💪 🙅 🚩
michi
Michael Z

Posted on September 28, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related