TDD course with AdonisJs - 4. Using the auth middleware

Published 9/14/2019

Our routes can currently be accessed by users who are not authenticated, so let's write a new test to confirm this!

As always you can find all the changes we made here in the following commit on GitHub: https://github.com/MZanggl/tdd-adonisjs/commit/6f50e5f277674dfe460b692cedc28d5a67d1cc55

// test/functional/thread.spec.js

test('unauthenticated user cannot create threads', async ({ client }) => {
  const response = await client.post('/threads').send({
    title: 'test title',
    body: 'body',
  }).end()

  response.assertStatus(401)
})

The test fails since the response code is still 200. So let's add the integrated auth middleware to our routes.

// start/routes.js

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

This makes the test pass, but at the same time, we broke our other tests since they now return a status code 401 as well (unauthenticated). In order to make them pass again, we need to be able to authenticate with a user in the tests.

First, let's create a model factory for users, the same way we did with threads.

Head back into database/factory.js and add the following blueprint for users.

Factory.blueprint('App/Models/User', (faker) => {
  return {
    username: faker.username(),
    email: faker.email(),
    password: '123456',
  }
})

Let's try this out in our functional thread.spec.js test! We can "login" using the loginVia method.

test('can create threads', async ({ client }) => {
  const user = await Factory.model('App/Models/User').create()
  const response = await client.post('/threads').loginVia(user).send({
    title: 'test title',
    body: 'body',
  }).end()

  response.assertStatus(200)

  const thread = await Thread.firstOrFail()
  response.assertJSON({ thread: thread.toJSON() })
})

However, this fails with the error ...loginVia is not a function. Like previously, a trait can help us resolve this issue, so let's add trait('Auth/Client') to the top of the file and run the test again.

Sweet! Let's apply the same fix for our existing failing delete test.

test('can delete threads', async ({ assert, client }) => {
  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()
  response.assertStatus(204)

  assert.equal(await Thread.getCount(), 0)
})

Sure it's not optimal that any user can delete any thread, but we are getting there...

I think it's about time we rename the tests cases to something more meaningful.

test('can create threads') => test('authorized user can create threads')

test('can delete threads') => test('authorized user can delete threads')

Check out my e-book!

Learn to simplify day-to-day code and the balance between over- and under-engineering.

With that being done it makes sense to add the user_id column to the threads table.

For this we first have to refactor our test case 'authorized user can create threads'. We are currently not actually testing if the title and body are being inserted correctly, we just assert that the response matches the first thread found in the database. So let's add that part as well

test('authorized user can create threads', async ({ client }) => {
  const user = await Factory.model('App/Models/User').create()
  const attributes = {
    title: 'test title',
    body: 'body',
  }

  const response = await client.post('/threads').loginVia(user).send(attributes).end()
  response.assertStatus(200)

  const thread = await Thread.firstOrFail()
  response.assertJSON({ thread: thread.toJSON() })
  response.assertJSONSubset({ thread: attributes })
})

The test should still pass, but let's go ahead and add the user_id to the assertion we added

response.assertJSONSubset({ thread: {...attributes, user_id: user.id} })

We now receive the error

expected { Object (thread) } to contain subset { Object (thread) }
  {
    thread: {
    - created_at: "2019-09-08 08:57:59"
    - id: 1
    - updated_at: "2019-09-08 08:57:59"
    + user_id: 1
    }

So let's head over to the ThreadController and swap out the "store" method with this

async store({ request, auth, response }) {
    const attributes = { ...request.only(['title', 'body']), user_id: auth.user.id }
    const thread = await Thread.create(attributes)
    return response.json({ thread })
    }

Don't worry, we will refactor this after the tests are green.

The tests will now fail at the assertion response.assertStatus(200) with a 500 error code, so let's add console.log(response.error) in the previous line. It will reveal that our table is missing the column user_id.

Head over to the threads migration file and after body, add the user_id column like this

table.integer('user_id').unsigned().notNullable()

Let's also register the new column with a foreign key. I like to keep foreign keys after all the column declarations.

// ... column declarations

table.foreign('user_id').references('id').inTable('users')

Great, this test is passing again!

But it turns out we broke two other tests!

Our unit tests "can access url" and the functional test "authorized user can delete threads" are now failing because of SQLITE_CONSTRAINT: NOT NULL constraint failed: threads.user_id.

Both tests are making use of our model factory for threads, and of course we haven't yet updated it with the user id. So let's head over to database/factory.js and add the user_id to the thread factory like this:

return {
    title: faker.word(),
    body: faker.paragraph(),
    user_id: (await Factory.model('App/Models/User').create()).id
  }

Be sure to turn the function into an async function since we have to use await here.

If we run our test suite again we should get green!

Refactoring

Let's head over to the ThreadController and think of a more object oriented approach for this part:

const attributes = { ...request.only(['title', 'body']), user_id: auth.user.id }
const thread = await Thread.create(attributes)

Would be nice if we wouldn't have to define the relationship by ourselves. We can swap out these two lines with this

const thread = await auth.user.threads().create(request.only(['title', 'body']))

Since we haven't defined the relationship yet, we will get the error TypeError: auth.user.threads is not a function.

So all we have to do is go to "App/Models/User.js" and add the relationship

threads() {
    return this.hasMany('App/Models/Thread')
}

And that's it, a solid refactor!

Let's add another test real quick to make sure that unauthenticated users can not delete threads

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

Of course we have to add more tests here, not every user should be able to simply delete any thread. Next time, let's test and create a policy that takes care of this for us!