loading...
Cover image for TDD course with AdonisJs - 7. Moderators

TDD course with AdonisJs - 7. Moderators

michi profile image Michael Z Updated on ・4 min read

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

Let's build an option for moderators to delete/update any thread.

You can find all the changes in the following commit: https://github.com/MZanggl/tdd-adonisjs/commit/1618a0c17e80ac2f75c148f4bacb054757d1eaee

test('moderator can delete threads', async ({ assert, client }) => {
  const moderator = await Factory.model('App/Models/User').create({ type: 1 })
  const thread = await Factory.model('App/Models/Thread').create()
  const response = await client.delete(thread.url()).send().loginVia(moderator).end()
  response.assertStatus(204)
  assert.equal(await Thread.getCount(), 0)
})

As you can see we simply create a user through our factory. But this time, we pass an object. This is to override the factory settings.
To make the override work, let's head over to factory.js, where we receive the passed data as the third argument in our user factory.

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

Run the tests and we get the error SQLITE_ERROR: table users has no column named type.

So let's add the "type" field to our user migrations. We will simply add it to the existing migration file that ends with _user.js. in the "database/migrations" folder. (Tip: in vscode just search for "migration user" and the fuzzy search will find it)

table.integer('type').defaultTo(0)

The way the "type" field works for now is 0 = normal user and 1 = moderator.

Running the test again returns

expected 403 to equal 204
  403 => 204

This makes sense, moderators currently receive a 403 (forbidden), since we haven't made the change in our middleware yet. For that let's first break down from the feature test into a unit test in modify-thread-policy.spec.js

Add the following test

test('moderator can modify threads', async ({ client }) => {
  const moderator = await Factory.model('App/Models/User').create({ type: 1 })
  const thread = await Factory.model('App/Models/Thread').create()
  let response = await client.post(`test/modify-thread-policy/${thread.id}`).loginVia(moderator).send().end()
  response.assertStatus(200)
})

Now this test will also return a 403, so let's change the code in ModifyThreadPolicy.js.

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

    await next()
  }
}

Alright, that makes the tests pass. Now of course we have to refactor this! But now we have the tests to let us change the code with confidence.


First thing we want to refactor is auth.user.type !== 1. I don't like passing around these hardcoded values, so let's change it up like this

if (!auth.user.isModerator() // ...

If we run the tests, we will have broken most of them because the isModerator method does not exist yet. To create it, let's again first break down to a unit test that checks this one thing specifically.

Run the following command to create a new test "adonis make:test user" and choose "Unit test".

Replace the file with the following code testing if the user is a moderator.

'use strict'

const { test, trait } = use('Test/Suite')('User')

const Factory = use('Factory')

trait('DatabaseTransactions')

test('can check if user is moderator', async ({ assert }) => {
  const user = await Factory.model('App/Models/User').make({ type: 1 })
  assert.isTrue(user.isModerator())
})

The difference between Factory.model(...).make and .create is that "make" does not store the user in the database, therefore making it a little faster.

And run the test in isolation

npm t -- -f "user.spec.js"

This will return the same error as before TypeError: user.isModerator is not a function.

Now let's add the actual code in app/Models/User.js

isModerator() {
    return this.type === 1
}

And the test becomes green!

Let's add another test testing if the code also works for users that are not moderators.

test('can check if user is not a moderator', async ({ assert }) => {
  const user = await Factory.model('App/Models/User').make()
  assert.isFalse(user.isModerator())
})

And now when we run the entire test suite again, all tests are green!

Let's head back to the policy again. Personally I find our condition to be hard to read, it can certainly be simplified:

async handle ({ params, auth, response }, next) {
    const thread = await Thread.findOrFail(params.id)

    if (auth.user.isModerator()) {
      return next()
    }

    if (thread.user_id === auth.user.id) {
      return next()
    }

    return response.forbidden()  
  }

Finally let's add the missing test that moderators can update threads

test('moderator can update title and body of threads', async ({ assert, client }) => {
  const thread = await Factory.model('App/Models/Thread').create()
  const moderator = await Factory.model('App/Models/User').create({ type: 1})
  const attributes = { title: 'new title', body: 'new body' }

  const response = await client.put(thread.url()).loginVia(moderator).send(attributes).end()
  response.assertStatus(200)
})

Posted on by:

Discussion

markdown guide
 

I`m a bit confused... when do I have to use unit or functional test?

 

Hi,

unit tests literally only test a single unit (e.g. a method). They do not work directly with dependencies like database, filesystem, rest APIs etc.

Because adonis is a battery included framework and already comes with many features, we actually don't have to write too many unit tests, which is pretty neat.