Pragmatic Testing in AdonisJS with Japa #6.2

Testing Authorization with Bouncer

In This Lesson

We'll learn to test AdonisJS Authentication with Bouncer for actions like deleting a post. We'll cover happy paths where authorization is granted and sad paths where authorization is denied and the action is forbidden.

Created by
@tomgobich
Published

Notes Used to Craft this Lesson

Next, let's talk about authorization, which differs from authentication. Authentication deals with determining who you are, while authorization deals with determining what you can do.

So, in addition to testing things that require an authenticated user, we also want to test what that authenticated user is allowed to do within our application. For the most part, our PostsController is just a mock; however, at the bottom, there is a real destroy handler mapped to a model and route. This destroy method also performs authorization via Bouncer that ensures only administrators or the post owner can delete the post.

That is the authorization check we'll be testing here today. So, first, let's start with our happy paths, which will be:

  • It should allow a post owner to delete their post

  • It should allow an administrator to delete a post

Let's start with a post owner, and we can add this to our posts spec.

test("allow a post owner to delete their post", async ({
  assert,
  client,
  route,
}) => {
  // create a post with an owner
  const post = await PostFactory.with("user").create();

  const response = await client
    .delete(route("posts.destroy", { id: post.id }))
    .withCsrfToken()
    .loginAs(post.user) // <- login as the owner
    .redirects(0);

  // confirm expected response & flash message
  response.assertStatus(302);
  response.assertFlashMessage("success", "Your post was deleted");

  // confirm post was actually deleted
  const deletedPost = await Post.find(post.id);

  assert.isNull(deletedPost);
});
Copied!

Next, we can test with an admin.

test("allow an admin to delete a post", async ({ assert, client, route }) => {
  // create a post with an owner
  const post = await PostFactory.with("user").create();

  // create an admin (note: you can also use states like we did with our password reset)
  const admin = await UserFactory.merge({ roleId: Roles.ADMIN }).create();

  const response = await client
    .delete(route("posts.destroy", { id: post.id }))
    .withCsrfToken()
    .loginAs(admin) // <- login as the admin
    .redirects(0);

  // confirm expected response & flash message
  response.assertStatus(302);
  response.assertFlashMessage("success", "Your post was deleted");

  // confirm post was actually deleted
  const deletedPost = await Post.find(post.id);

  assert.isNull(deletedPost);
});
Copied!

Fantastic! Now, we're left with our sad authorization path. For this, we'll want to test that it should prevent a non-owner or admin from deleting a post.

test("prevent a non owner or admin from deleting a post", async ({
  assert,
  client,
  route,
}) => {
  // create a post with an owner
  const post = await PostFactory.with("user").create();

  // create another non-admin user
  const user = await UserFactory.create();

  const response = await client
    .delete(route("posts.destroy", { id: post.id }))
    .withCsrfToken()
    .loginAs(user) // <- login as that other user
    .redirects(0);

  // confirm request was forbidden
  response.assertForbidden();
  response.assertTextIncludes("Access denied");

  // confirm post still exists
  const deletedPost = await Post.findOrFail(post.id);

  assert.equal(deletedPost.id, post.id);
});
Copied!

Join the Discussion 0 comments

Create a free account to join in on the discussion
robot comment bubble

Be the first to comment!