174 lines
5.6 KiB
PHP
174 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;
|
|
}
|
|
}
|