NewWe open-sourced 50+ Laravel packages
Custom AI apps, agents and automation — Roundly ConsultingRoundly
All packages

Add the InteractsWithAuthentication trait to a test case that has Laravel’s HTTP testing concern:

use RoundlyConsulting\Auth\Enums\ActivityOutcome;
use RoundlyConsulting\Auth\Enums\ActivityType;
use RoundlyConsulting\Auth\Enums\InvalidationReason;
use RoundlyConsulting\Auth\Testing\InteractsWithAuthentication;

abstract class TestCase extends BaseTestCase
{
    use InteractsWithAuthentication;
}

$this->actingAsAccount($user)->getJson('/api/orders')->assertOk();        // a REAL token pair (fires TokensIssued)
$this->assertLoginActivity('users', ActivityType::PasswordLogin, ActivityOutcome::Succeeded);
$this->assertTokensInvalidated($user, InvalidationReason::PasswordChanged);   // moved since actingAsAccount() (or pass since:)
HelperDescription
actingAsAccount($account, ?$guard, $authMethods)Issues a real token pair through issueTokens() (so TokensIssued fires) and sets Authorization: Bearer; requests run through the real jwt guard, audience, denylist and token-version checks.
assertLoginActivity($guard, ActivityType, ActivityOutcome)A matching login-activity row exists.
assertTokensInvalidated($account, InvalidationReason, ?$guard, ?$since)Since actingAsAccount() (or since:), the token version moved and, under scope all, no session survived — or, for a reason whose scope is none, it did not move.

Asserting domain events

There is no Authentication::fake() on purpose. Every write already fires a domain event — assert them with Laravel’s Event::fake() while the real flow runs — and the helpers above issue real token pairs, so a test runs through the guard, audience, denylist and token-version checks that a stub would hide:

use Illuminate\Support\Facades\Event;
use RoundlyConsulting\Auth\Events\PasswordChanged;
use RoundlyConsulting\Auth\Facades\Authentication;

Event::fake([PasswordChanged::class]);

Authentication::passwords()->set($user, 'n3w-Passphrase!');

Event::assertDispatched(PasswordChanged::class);

A full flow against the package endpoints:

use Illuminate\Support\Facades\Hash;
use RoundlyConsulting\Auth\Enums\ActivityOutcome;
use RoundlyConsulting\Auth\Enums\ActivityType;

it('signs in with a password', function () {
    $user = User::factory()->create(['password' => Hash::make('correct-horse-battery')]);

    $this->postJson('/users/auth/login', [       // routes.enabled = true, default prefix
        'identifier' => $user->email,
        'password' => 'correct-horse-battery',
    ])->assertOk()->assertJsonPath('status', 'authenticated');

    $this->assertLoginActivity('users', ActivityType::PasswordLogin, ActivityOutcome::Succeeded);
});

TwoFactor::fake() and Passkeys::fake() from the lower packages still work, and RoundlyConsulting\Passkeys\Testing\VirtualAuthenticator drives real passkey ceremonies. Factories ship for all four package models.

The package suite

composer test            # Pest
composer test-coverage   # Pest with --min=95
composer analyse         # Larastan level 7
composer format          # Pint

Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

More ways to support, including crypto

By donating, you agree to our donation terms.

Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.