TDD course with AdonisJs - 5. Middlewares
Tags: tdd, testing, node, javascript
Published: 2019-09-21 13:44:13

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

In the last episode we added authorization to our cruddy ThreadController. However, just because a user is authenticated doesn't mean he is authorized to delete any thread. This should be restricted to moderators and the user who created the thread.

As always, you can find all the changes in this commit: https://github.com/MZanggl/tdd-adonisjs/commit/d845ed83700210ac1b520a25c702373df0782b69

Before we jump into testing a middleware, let's remember to keep it as simple as possible. Let's just add the authorization logic in the controller. So for that, let's extend our functional `thread.spec.js` file with the following test:

```javascript
test('thread can not be deleted by a user who did not create it', async ({ client }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  const notOwner = await Factory.model('App/Models/User').create()
  const response = await client.delete(thread.url()).send().loginVia(notOwner).end()
  response.assertStatus(403)
})
```

Remember that the factory for threads is now also creating a user since it depends on it.

The test fails with the error
```javascript
expected 204 to equal 403
  204 => 403
```

Let's go into the ThreadController and add the authorization logic there:

```javascript
async destroy({ params, auth, response }) {
    const thread = await Thread.findOrFail(params.id)

    if (thread.user_id !== auth.user.id) {
        return response.status(403).send()
    }

    await thread.delete()
}
```

Now the test passes. However, we have broken the test "authorized user can delete threads" because it now returns a 403 even though we expect it to return 204.

That makes perfect sense. If we take a look at the test we authenticate using not the owner of the thread, but using a new user. Let's get that fixed.

We can replace

```javascript
const user = await Factory.model('App/Models/User').create()
const thread = await Factory.model('App/Models/Thread').create()
const response = await client.delete(thread.url()).send().loginVia(user).end()
```

with

```javascript
const thread = await Factory.model('App/Models/Thread').create()
const response = await client.delete(thread.url()).send().loginVia(await thread.user().first()).end()
```

As you can see we will get the user from the thread instance. Since we haven't defined that relationship (only the other way around), we will receive the error `thread.user is not a function`. So let's add the relationship to "App/Models/Thread.js".

```javascript
user() {
    return this.belongsTo('App/Models/User')
}
```

And there we go, the tests are green.

---

Let's get a quick refactoring in place. In the ThreadController we added `return response.status(403).send()`. Simply replace that bit with `return response.forbidden()` and you should still get green!

Before we abstract the authorization logic into a policy, let's first make it worth doing so. What I mean is, let's first create some duplication, before we abstract things, and what's a better fit for this than updating threads!


```javascript
test('authorized user can update title and body of threads', async ({ assert, client }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  const attributes = { title: 'new title', body: 'new body' }
  const updatedThreadAttributes = { ...thread.toJSON(), ...attributes }

  const response = await client.put(thread.url()).loginVia(await thread.user().first()).send(attributes).end()
  await thread.reload()

  response.assertStatus(200)
  response.assertJSON({ thread: thread.toJSON() })
  assert.deepEqual(thread.toJSON(), updatedThreadAttributes)
})
```

So first we create a thread, define all the attributes we want to update and then merge the two together to create an image of how the thread should be updated. Then we send off the request and refresh our thread model.

Finally we assert the response status and text as well as check if the attributes were updated accordingly.

Running the test suite results in a 404, so let's add it to `start/routes.js`.

```javascript
Route.resource('threads', 'ThreadController').only(['store', 'destroy', 'update']).middleware('auth')
```

You should already be familiar with the pattern at this point. You get a 500, so add `console.log(response.error)` in the unit test right after we fire the request. This should log `RuntimeException: E_UNDEFINED_METHOD: Method update missing on App/Controllers/Http/ThreadController`.

Time to add the method to our ThreadController

```javascript
async update({ response }) {
    return response.json({ })
}
```

And we now get the error `expected {} to deeply equal { Object (thread) }`.

So time to get serious with the update method, here is the full code

```javascript
async update({ request, params, response }) {
    const thread = await Thread.findOrFail(params.id)
    thread.merge(request.only(['title', 'body']))
    await thread.save()
    return response.json({ thread })
}
```

This gets the tests to pass.



Let's add a test to confirm that the auth middleware is applied

```javascript
test('unauthenticated user cannot update threads', async ({ assert, client }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  const response = await client.put(thread.url()).send().end()
  response.assertStatus(401)
})
```

Passes!

And a test to check that only the owner of a thread can really update it.

```javascript
test('thread can not be updated by a user who did not create it', async ({ client }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  const notOwner = await Factory.model('App/Models/User').create()
  const response = await client.put(thread.url()).send().loginVia(notOwner).end()
  response.assertStatus(403)
})
```

Fails :/


Great, so let's copy over the authorization logic from the destroy method.

```javascript
async update({ request, auth, params, response }) {
    const thread = await Thread.findOrFail(params.id)
    if (thread.user_id !== auth.user.id) {
        return response.forbidden()
    }

    thread.merge(request.only(['title', 'body']))
    await thread.save()
    return response.json({ thread })
}
```

The test passes, but now we have created duplication. Time to create a policy! For this we will go away from our feature test and break down to a unit test.
Now Adonis doesn't have a concept of policies, so we will use a middleware for this, hence the title "Testing middlewares".


First let's create a new unit test for the non existing middleware.

```bash
adonis make:test ModifyThreadPolicy
```

and select "Unit test".

Now replace the example test with the following test case

```javascript
test('non creator of a thread cannot modify it', async ({ assert, client }) => {
  
})
```

Great. So what is the best way to test a middleware? Well, we can simply import "Route" and dynamically create a route that is only valid during testing.

Let's do just that and pull in all traits and modules we need later on.

```javascript
'use strict'

const { test, trait } = use('Test/Suite')('Modify Thread Policy')

const Route = use('Route')
const Factory = use('Factory')

trait('Test/ApiClient')
trait('Auth/Client')
trait('DatabaseTransactions')

test('non creator of a thread cannot modify it', async ({ assert, client }) => {
  const action = ({ response }) => response.json({ ok: true })
  Route.post('test/modify-thread-policy/:id', action).middleware(['auth', 'modifyThreadPolicy'])
})
```

Now that we have the route, let's send off a request and do some assertions!

```javascript
  // ...
  const thread = await Factory.model('App/Models/Thread').create()
  const notOwner = await Factory.model('App/Models/User').create()
  let response = await client.post(`test/modify-thread-policy/${thread.id}`).loginVia(notOwner).send().end()
  console.log(response.error)
  response.assertStatus(403)
```

Running the test should throw the error `RuntimeException: E_MISSING_NAMED_MIDDLEWARE: Cannot find any named middleware for {modifyThreadPolicy}. Make sure you have registered it inside start/kernel.js file.`.

So let's do as it says and add the following line in the `namedMiddleware` array in "start/kernel.js".

```javascript
  modifyThreadPolicy: 'App/Middleware/ModifyThreadPolicy'
```

Running the test now returns an error that Adonis couldn't find the module.

Let's create the policy by running 

```javascript
adonis make:middleware ModifyThreadPolicy
```

and select 'For HTTP requests'.

Let's run the test again. Since we didn't add any logic to the middleware it will  not do anything and forward the request to the action, which returns the status code 200.

```
expected 200 to equal 403
  200 => 403
```

Since we already have the logic we need in the controller, let's go ahead and copy it over to the middleware.

Altogether, our middleware looks like this

```javascript
'use strict'


const Thread = use('App/Models/Thread')

class ModifyThreadPolicy {
  async handle ({ params, auth, response }, next) {
    const thread = await Thread.findOrFail(params.id)
    if (thread.user_id !== auth.user.id) {
      return response.forbidden()
    }

    await next()
  }
}

module.exports = ModifyThreadPolicy
```

And it passes!

Let's add another unit test in "modify-thread-policy.spec.js" to test the happy path.

```javascript
test('creator of a thread can modify it', async ({ assert, client }) => {
  const action = ({ response }) => response.json({ ok: true })
  Route.post('test/modify-thread-policy/:id', action).middleware(['auth', 'modifyThreadPolicy'])

  const thread = await Factory.model('App/Models/Thread').create()
  let response = await client.post(`test/modify-thread-policy/${thread.id}`).loginVia(await thread.user().first()).send().end()
  response.assertStatus(200)
})
```

To avoid creating the route twice, let's add a `before` section to the test file.

Import it at the top of the file like so: `
const { test, trait, before } = use('Test/Suite')('Modify Thread Policy')`, remove the route creation logic from each test and put the following code before the tests:

```javascript
before(() => {
  const action = ({ response }) => response.json({ ok: true })
  Route.post('test/modify-thread-policy/:id', action).middleware(['auth', 'modifyThreadPolicy'])
})
```

Alright, with our unit test in place, let's go back to our functional test.
Delete the authorization check from the ThreadController's destroy and update method.

```javascript
// delete this

if (thread.user_id !== auth.user.id) {
    return response.forbidden()
}
```

And as expected, the two tests now fail

```
1. thread can not be deleted by a user who did not create it
  expected 204 to equal 403
  204 => 403

  2. thread can not be updated by a user who did not create it
  expected 200 to equal 403
  200 => 403
```

So let's head over to `start/routes.js` and add the middleware we created to the update and destroy route.

```javascript
Route.resource('threads', 'ThreadController').only(['store', 'destroy', 'update']).middleware(new Map([
    [['store', 'destroy', 'update'], ['auth']],
    [['destroy', 'update'], ['modifyThreadPolicy']]
]))
```

And the tests pass again!

Since we already check for the thread's existence in the middleware, we can refactor our ThreadController's destroy method to simply do

```javascript
async destroy({ params }) {
    await Thread.query().where('id', params.id).delete()
}
```

And that's all there is for this episode! Next time let's take a look at validation since we are currently able to insert an empty title and body.