Testing Authenticated and Authorized Paths

actingAs signs a user in for the test so you can assert the 403, the redirect to login, and the policy that produced them.

Syntax$this->actingAs(Illuminate\Contracts\Auth\Authenticatable $user, string|null $guard = null)

Most routes behave three ways: one for a guest, one for a signed-in stranger, one for the owner. That is three tests, and the one people skip is the guest.

Signing a user in

actingAs(Authenticatable $user, $guard = null) sets the user on a guard for the rest of the test. A guard is the named strategy that decides who the current user is, configured in config/auth.php; pass its name as the second argument and it also becomes the default guard for the test. be() is the same method under an older name. actingAsGuest() makes sure the request is unauthenticated. The state assertions sit on the test case, not on the response: assertAuthenticated($guard = null), assertAuthenticatedAs($user, $guard = null), assertGuest($guard = null).

Which layer refused, and with which status

Name the layer or you cannot read the failure. A guest hitting a route behind the auth middleware is redirected to the login route, so assert assertRedirect('/login'); if the request expects JSON, the same middleware answers 401, so assert assertUnauthorized(). A signed-in user who fails a policy gets 403 — assertForbidden() — because Gate::authorize() throws Illuminate\Auth\Access\AuthorizationException, which the handler renders as 403.

Testing the policy on its own

A policy is a class in app/Policies whose methods take the user and the model and return a bool, generated with php artisan make:policy PostPolicy --model=Post. You can check one without any HTTP: $user->can('update', $post) and Gate::forUser($user)->allows('update', $post) are the same check. If a policy test passes for a reason you did not expect, look for a before method on the policy or a Gate::before callback granting administrators everything.

Example

Example · php
<?php
// tests/Feature/PostUpdateTest.php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class PostUpdateTest extends TestCase
{
    use RefreshDatabase;

    public function test_a_guest_is_sent_to_the_login_page(): void
    {
        $post = Post::factory()->create();

        $this->put("/posts/{$post->id}", ['title' => 'Edited'])
            ->assertRedirect('/login');

        $this->assertGuest();
    }

    public function test_a_user_who_does_not_own_the_post_is_forbidden(): void
    {
        $post = Post::factory()->create();
        $stranger = User::factory()->create();

        $response = $this->actingAs($stranger)
            ->put("/posts/{$post->id}", ['title' => 'Edited']);

        $response->assertForbidden();   // assertStatus(403)
        $this->assertDatabaseMissing('posts', ['title' => 'Edited']);
    }

    public function test_the_owner_can_update_the_post(): void
    {
        // The Post factory creates the related user.
        $post = Post::factory()->create();

        $response = $this->actingAs($post->user)
            ->put("/posts/{$post->id}", ['title' => 'Edited']);

        $response->assertRedirect("/posts/{$post->id}");
        $this->assertDatabaseHas('posts', [
            'id' => $post->id,
            'title' => 'Edited',
        ]);
    }
}

When to use it

  • Cover the path a signed-out visitor takes through a route behind auth middleware.
  • Prove a user cannot edit another user's record, and that the record is unchanged.
  • Test an authorization rule branch by branch without writing an HTTP test for each.
  • Confirm an admin override comes from the gate you think it does.

More examples

The policy the 403 came from

Generated by php artisan make:policy PostPolicy --model=Post and discovered automatically because App\Models\Post maps to App\Policies\PostPolicy. The HTTP test above only tells you a 403 happened; this class is the thing that decided it.

Example · php
<?php
// app/Policies/PostPolicy.php

declare(strict_types=1);

namespace App\Policies;

use App\Models\Post;
use App\Models\User;

class PostPolicy
{
    public function update(User $user, Post $post): bool
    {
        return $user->id === $post->user_id;
    }

    public function delete(User $user, Post $post): bool
    {
        return $this->update($user, $post) && $post->published_at === null;
    }
}

Testing the policy without a request

One test per branch of the policy, with no routes involved, so a failure points straight at the rule. Gate::forUser() is the form to use when the user is not the authenticated one.

Example · php
<?php
// tests/Feature/PostPolicyTest.php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\Post;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Gate;
use Tests\TestCase;

class PostPolicyTest extends TestCase
{
    use RefreshDatabase;

    public function test_only_the_author_may_update_a_post(): void
    {
        $post = Post::factory()->create();
        $stranger = User::factory()->create();

        // These two lines are the same check.
        $this->assertTrue($post->user->can('update', $post));
        $this->assertTrue(Gate::forUser($post->user)->allows('update', $post));

        $this->assertFalse($stranger->can('update', $post));
    }

    public function test_a_published_post_cannot_be_deleted(): void
    {
        $post = Post::factory()->create(['published_at' => now()]);

        $this->assertFalse($post->user->can('delete', $post));
    }
}

A JSON route answers 401, not a redirect

The auth middleware redirects browsers and returns 401 to clients that ask for JSON. Assert the status your clients will actually receive, which means using the Json request methods for API routes.

Example · php
public function test_the_api_rejects_an_unauthenticated_request(): void
{
    $this->getJson('/api/posts')->assertUnauthorized();   // assertStatus(401)
}

public function test_a_signed_in_user_can_list_posts(): void
{
    $user = User::factory()->create();

    $this->actingAs($user)
        ->getJson('/api/posts')
        ->assertOk()
        ->assertJsonCount(0, 'data');
}

Discussion

  • Be the first to comment on this lesson.