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 */ 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 */ 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; } }