Skip to content

Testing

These examples use Orchestra Testbench, matching the package's own test setup.

Test Case Setup

php
<?php

declare(strict_types=1);

namespace Tests\Feature;

use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Sopheak\JwtAuth\DTO\TokenContext;
use Sopheak\JwtAuth\Services\JwtTokenService;
use Tests\TestCase;

final class AuthTest extends TestCase
{
    private User $user;
    private JwtTokenService $jwt;

    protected function setUp(): void
    {
        parent::setUp();

        $this->user = User::factory()->create([
            'email' => 'test@example.com',
            'password' => Hash::make('secret123'),
        ]);

        $this->jwt = app(JwtTokenService::class);
    }
}

Issue a Token in Tests

php
public function test_protected_route_with_token(): void
{
    $pair = $this->jwt->issueTokenPair(
        $this->user,
        TokenContext::make()->scopes(['profile.read']),
    );

    $response = $this->withToken($pair->accessToken)
        ->getJson('/api/me');

    $response->assertOk();
}

Claims and tenant/company context work the same way in tests:

php
public function test_company_route_with_token_claims(): void
{
    $pair = $this->jwt->issueTokenPair(
        $this->user,
        TokenContext::make()
            ->companyId(42)
            ->scopes(['invoices.read'])
    );

    $response = $this->withToken($pair->accessToken)
        ->getJson('/api/invoices');

    $response->assertOk();
}

JwtTokenService::issueTokenPair() persists access and refresh token rows, so tests still need the package token tables. Use JwtTokenTestHelper when you want a compact test setup:

php
use Sopheak\JwtAuth\Testing\JwtTokenTestHelper;

$pair = JwtTokenTestHelper::createToken(
    user: $user,
    scopes: ['client'],
    claims: ['company_id' => 42],
    subjectType: 'company',
    subjectId: '42',
);

Test Login Endpoint

php
public function test_login_success(): void
{
    $response = $this->postJson('/api/auth/login', [
        'email' => 'test@example.com',
        'password' => 'secret123',
    ]);

    $response->assertOk()
        ->assertJsonStructure([
            'user' => ['id', 'name', 'email'],
            'token' => ['token_type', 'expires_in', 'access_token', 'refresh_token'],
        ]);
}

public function test_login_with_invalid_credentials(): void
{
    $response = $this->postJson('/api/auth/login', [
        'email' => 'test@example.com',
        'password' => 'wrong-password',
    ]);

    $response->assertUnprocessable();
}

Test Token Refresh

php
public function test_refresh_token_rotation(): void
{
    $pair = $this->jwt->issueTokenPair(
        $this->user,
        TokenContext::make(),
    );

    $response = $this->postJson('/api/auth/refresh', [
        'refresh_token' => $pair->refreshToken,
    ]);

    $response->assertOk();

    // Old refresh token is now invalid
    $response = $this->postJson('/api/auth/refresh', [
        'refresh_token' => $pair->refreshToken,
    ]);

    $response->assertStatus(422); // reuse detected
}

Test Scope Enforcement

php
public function test_route_requires_scope(): void
{
    $pair = $this->jwt->issueTokenPair(
        $this->user,
        TokenContext::make()->scopes(['profile.read']),
    );

    // Route requires invoices.write
    $response = $this->withToken($pair->accessToken)
        ->getJson('/api/invoices');

    $response->assertForbidden();
}

Test Logout

php
public function test_logout_revokes_token(): void
{
    $pair = $this->jwt->issueTokenPair(
        $this->user,
        TokenContext::make(),
    );

    $response = $this->withToken($pair->accessToken)
        ->postJson('/api/auth/logout');

    $response->assertOk();

    // Token is now revoked
    $response = $this->withToken($pair->accessToken)
        ->getJson('/api/me');

    $response->assertUnauthorized();
}

Test Password Reset

php
public function test_password_reset_flow(): void
{
    $dispatch = app(PasswordResetBroker::class)->createResetToken(
        $this->user,
        $this->user->email,
    );

    $response = $this->postJson('/api/auth/reset-password', [
        'token' => $dispatch->plaintextToken,
        'email' => $this->user->email,
        'password' => 'new-secret-456',
        'password_confirmation' => 'new-secret-456',
    ]);

    $response->assertOk();

    // Old password no longer works
    $response = $this->postJson('/api/auth/login', [
        'email' => $this->user->email,
        'password' => 'secret123',
    ]);
    $response->assertUnprocessable();
}

Test Helpers

Create a trait to reuse token issuance across test cases:

php
<?php

declare(strict_types=1);

namespace Tests;

use Illuminate\Contracts\Auth\Authenticatable;
use Sopheak\JwtAuth\DTO\TokenContext;
use Sopheak\JwtAuth\Services\JwtTokenService;

trait WithAccessToken
{
    protected function actAs(Authenticatable $user, array $scopes = ['*']): self
    {
        $pair = app(JwtTokenService::class)->issueTokenPair(
            $user,
            TokenContext::make()->scopes($scopes),
        );

        return $this->withToken($pair->accessToken);
    }
}
php
// In your test
public function test_something(): void
{
    $this->actAs($this->user, ['profile.read'])
        ->getJson('/api/me')
        ->assertOk();
}