---
title: "Testing — Auth for Laravel | Roundly"
description: "Act as an account with a real token pair, assert domain events with Event::fake(), and check login activity and token invalidation."
url: https://roundly-consulting.com/open-source/docs/auth-for-laravel/testing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Auth for Laravel](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)

# Testing

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

```php
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:)
```

| Helper | Description |
| --- | --- |
| `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, none of the sessions active when actingAsAccount() signed the account in survived (a later login, e.g. after a reset with passwords.reset.login\_after, is not judged; with only since:, no session at all may be active) — or, for a reason whose scope is none, it did not move. The scope comes from $guard, else the default guard: an account of another guard throws AuthenticationMisconfigured, so pass the guard for a non-default one. |

The default $authMethods (\[AuthMethodReference::Pwd\]) is a password-only login. To simulate a completed 2FA login — what the re-authentication gate expects of an account with a factor — pass \[AuthMethodReference::Pwd, AuthMethodReference::Otp, AuthMethodReference::Mfa\].

## 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:

```php
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:

```php
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.

## Asserting who acted

An admin action passes actor:; assert it on the event or on the notification:

```php
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
use RoundlyConsulting\Auth\Events\PasswordChanged;
use RoundlyConsulting\Auth\Notifications\PasswordChangedNotification;

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

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

Event::assertDispatched(PasswordChanged::class, fn (PasswordChanged $event): bool => $event->actor?->is($admin) === true);
Notification::assertSentTo($user, PasswordChangedNotification::class,
    fn (PasswordChangedNotification $notification): bool => $notification->data->actor?->is($admin) === true);
```

$event->actor is the model (null for the account’s own change); $notification->data->actor is an ActorData reference. To check the copy, render it: $notification->toMail($user)->subject.

## The package suite

```bash
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](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## 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.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/auth-for-laravel.md)
