Files
landing-api/tests/Feature/Api/TokenEndpointTest.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

122 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Api;
use App\Models\User;
use Database\Seeders\RolePermissionSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use PHPUnit\Framework\Attributes\Test;
use Tests\FakesPireon;
use Tests\TestCase;
class TokenEndpointTest extends TestCase
{
use FakesPireon, RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->seed(RolePermissionSeeder::class);
}
#[Test]
public function it_exchanges_credentials_for_a_token_and_provisions_the_user(): void
{
$this->fakePireon();
$response = $this->postJson('/api/v1/auth/token', [
'grant_type' => 'password',
'username' => 'ada@example.com',
'password' => 'secret',
]);
$response->assertOk()
->assertJsonPath('access_token', 'fake-access-token')
->assertJsonPath('user.email', 'ada@example.com')
->assertJsonPath('user.roles.0', config('pireon.default_role'));
$this->assertDatabaseHas('users', ['oauth_id' => 4242]);
// The password grant must reach the identity server, never a local
// password column.
Http::assertSent(fn ($request) => str_contains($request->url(), '/oauth/token')
&& $request['grant_type'] === 'password');
}
#[Test]
public function it_does_not_leak_why_a_credential_was_rejected(): void
{
Http::fake([
"{$this->pireonServer}/oauth/token" => Http::response([
'error' => 'invalid_grant',
'error_description' => 'The user credentials were incorrect.',
], 400),
]);
$response = $this->postJson('/api/v1/auth/token', [
'grant_type' => 'password',
'username' => 'ada@example.com',
'password' => 'wrong',
]);
$response->assertStatus(422);
$this->assertStringNotContainsString('credentials were incorrect', $response->content());
}
#[Test]
public function it_refuses_an_account_awaiting_approval(): void
{
$this->fakePireon(identity: ['approved_at' => null]);
$this->postJson('/api/v1/auth/token', [
'grant_type' => 'password',
'username' => 'ada@example.com',
'password' => 'secret',
])
->assertForbidden()
->assertJsonPath('message', __('pireon.pending_approval'));
$this->assertDatabaseCount('users', 0);
}
#[Test]
public function the_password_grant_can_be_disabled(): void
{
config()->set('pireon.password_grant_enabled', false);
$this->postJson('/api/v1/auth/token', [
'grant_type' => 'password',
'username' => 'ada@example.com',
'password' => 'secret',
])->assertStatus(422);
}
#[Test]
public function it_validates_the_grant_type(): void
{
$this->postJson('/api/v1/auth/token', ['grant_type' => 'implicit'])
->assertUnprocessable()
->assertJsonValidationErrors('grant_type');
}
#[Test]
public function repeated_calls_with_the_same_token_hit_the_identity_server_once(): void
{
$user = User::factory()->create();
$user->syncRoles(['viewer']);
$headers = $this->apiHeadersFor($user);
$this->getJson('/api/v1/me', $headers)->assertOk();
$this->getJson('/api/v1/me', $headers)->assertOk();
$this->getJson('/api/v1/me', $headers)->assertOk();
// Introspection is cached, so three requests cost one upstream call.
Http::assertSentCount(1);
}
}