TDD course with AdonisJs - 7. Moderators

michi

Michael Z

Posted on October 6, 2019

TDD course with AdonisJs - 7. Moderators

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

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

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

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

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

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

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

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())
})

Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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)
})
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
michi
Michael Z

Posted on October 6, 2019

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

Sign up to receive the latest update from our blog.

Related