DEV Community

Cover image for TDD course with AdonisJs - 4. Using the auth middleware
Michael Z
Michael Z

Posted on • Updated on • Originally published at michaelzanggl.com

TDD course with AdonisJs - 4. Using the auth middleware

Originally posted at michaelzanggl.com. Subscribe to my newsletter to never miss out on new content.

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

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

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

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

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

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')


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

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

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

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

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

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

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

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

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

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

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

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!

Latest comments (0)