Files
alrahma_sunday_school_api/tests/Feature/Api/ApiAuthenticationAndAuthorizationTest.php
T
2026-06-09 01:03:53 -04:00

175 lines
5.6 KiB
PHP

<?php
namespace Tests\Feature\Api;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Routing\Route as LaravelRoute;
use Illuminate\Support\Facades\Route;
use Tests\Concerns\CreatesApiTestUsers;
use Tests\TestCase;
class ApiAuthenticationAndAuthorizationTest extends TestCase
{
use CreatesApiTestUsers;
use RefreshDatabase;
public function test_api_login_returns_token_and_user_object(): void
{
$user = $this->createApiUserWithRole('teacher', [
'firstname' => 'Login',
'lastname' => 'Tester',
'email' => 'api.login@example.test',
]);
$this->postJson('/api/v1/auth/login', [
'email' => strtoupper($user->email),
'password' => 'password',
])
->assertOk()
->assertJsonPath('status', true)
->assertJsonStructure([
'token',
'access_token',
'token_type',
'expires_in',
'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
])
->assertJsonPath('user.email', 'api.login@example.test');
}
public function test_session_login_also_returns_token_and_user_object_for_spa_clients(): void
{
$user = $this->createApiUserWithRole('teacher', [
'firstname' => 'Session',
'lastname' => 'Tester',
'email' => 'session.login@example.test',
]);
$this->postJson('/user/login', [
'email' => $user->email,
'password' => 'password',
])
->assertOk()
->assertJsonPath('status', true)
->assertJsonStructure([
'token',
'access_token',
'user' => ['id', 'name', 'firstname', 'lastname', 'email', 'roles'],
'next_url',
])
->assertJsonPath('user.email', 'session.login@example.test');
}
public function test_auth_me_requires_authentication(): void
{
$this->getJson('/api/v1/auth/me')->assertUnauthorized();
}
public function test_auth_me_returns_the_current_authenticated_user(): void
{
$user = $this->createApiUserWithRole('teacher', [
'firstname' => 'Teacher',
'lastname' => 'Tester',
'email' => 'teacher.me@example.test',
]);
$this->actingAs($user, 'api')
->getJson('/api/v1/auth/me')
->assertOk()
->assertJsonPath('status', true)
->assertJsonPath('data.email', 'teacher.me@example.test');
}
public function test_admin_only_routes_reject_non_admin_users(): void
{
$parent = $this->createApiUserWithRole('parent');
$this->actingAs($parent, 'api');
foreach ($this->adminOnlyEndpoints() as [$method, $uri]) {
$response = $this->json($method, $uri);
$this->assertContains(
$response->getStatusCode(),
[401, 403],
"$method $uri should reject a non-admin user; got {$response->getStatusCode()}"
);
}
}
public function test_protected_api_routes_reject_anonymous_requests_before_business_logic(): void
{
$failures = [];
foreach ($this->sampleProtectedRoutes() as $route) {
$method = $this->primaryMethod($route);
$uri = '/' . $this->uriWithPlaceholders($route->uri());
$response = $this->json($method, $uri);
$status = $response->getStatusCode();
if (! in_array($status, [401, 403, 404, 405, 419, 422], true)) {
$failures[] = "$method $uri returned $status";
}
}
$this->assertSame([], $failures, "Anonymous requests reached unexpected responses:\n" . implode("\n", $failures));
}
/**
* @return list<array{0: string, 1: string}>
*/
private function adminOnlyEndpoints(): array
{
return [
['GET', '/api/v1/users'],
['POST', '/api/v1/users'],
['GET', '/api/v1/administrator/dashboard/metrics'],
['GET', '/api/v1/administrator/staff'],
['GET', '/api/v1/school-years'],
['GET', '/api/v1/administrator/role-permission/roles'],
];
}
/**
* @return list<LaravelRoute>
*/
private function sampleProtectedRoutes(): array
{
return collect(Route::getRoutes()->getRoutes())
->filter(fn (LaravelRoute $route): bool => str_starts_with($route->uri(), 'api/'))
->filter(function (LaravelRoute $route): bool {
return collect($route->gatherMiddleware())->contains(function (string $middleware): bool {
return str_contains($middleware, 'auth:api') || str_contains($middleware, 'jwt.auth');
});
})
->reject(function (LaravelRoute $route): bool {
$uri = $route->uri();
return str_contains($uri, 'files/')
|| str_contains($uri, 'receipts/')
|| str_contains($uri, 'reimbursements/')
|| str_contains($uri, 'attachments/');
})
->take(80)
->values()
->all();
}
private function primaryMethod(LaravelRoute $route): string
{
foreach ($route->methods() as $method) {
if (! in_array($method, ['HEAD', 'OPTIONS'], true)) {
return $method;
}
}
return 'GET';
}
private function uriWithPlaceholders(string $uri): string
{
return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri;
}
}