apiRoutes() as $route) { $uri = $route->uri(); if ($this->domainFor($uri) === null) { $unmapped[] = implode('|', array_diff($route->methods(), ['HEAD'])).' '.$uri; } } $this->assertSame( [], $unmapped, "API routes without an explicit use-case domain:\n".implode("\n", $unmapped). "\n\nAdd the route to the matrix and then add/extend an E2E workflow test for that domain." ); } public function test_required_use_case_domains_have_route_coverage(): void { $domains = collect($this->apiRoutes()) ->map(fn (LaravelRoute $route): ?string => $this->domainFor($route->uri())) ->filter() ->unique() ->sort() ->values() ->all(); $missing = array_values(array_diff(array_keys($this->useCaseDomains()), $domains)); $this->assertSame( [], $missing, "Required use-case domains with no matching API route coverage:\n".implode("\n", $missing) ); } public function test_every_protected_api_route_rejects_anonymous_requests(): void { $failures = []; foreach ($this->protectedRoutes() as $route) { $method = $this->primaryMethod($route); $uri = '/'.$this->uriWithSafePlaceholders($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 protected API routes unexpectedly:\n".implode("\n", $failures) ); } /** * @return array */ private function useCaseDomains(): array { return [ 'public_content_and_support' => '#^api/(?:v1/)?(?:login|register|auth|documentation|docs|docs/public|policies|pages|contact|frontend|health|system/db-check|access_denied|certificates/verify|timeoff/notify|winners/competitions|confirm_authorized_user|set_authorized_user_password|badge_scan|scanner)(?:/|$)#', 'legacy_compatibility' => '#^api/(?:proofread|emails|compare|attendance-templates|attendance-comment-templates|administrator/attendance-templates)(?:/|$)#', 'preferences_navigation_and_ui' => '#^api/v1/(?:preferences|nav-builder|landing|info-icon|dashboard|utilities|ui|settings|configuration|system|stats|role-switcher)(?:/|$)#', 'administration_enrollment_and_school_years' => '#^api/v1/(?:admin|administrator|school-years|class-sections|users|role-permissions|ip-bans|notifications)(?:/|$)#', 'students_parents_and_families' => '#^api/v1/(?:students|parents|families|family-admin|emergency-contacts)(?:/|$)#', 'teachers_staff_and_class_operations' => '#^api/v1/(?:teacher|teachers|staff|assignments|class-prep|class-progress)(?:/|$)#', 'attendance_and_safety' => '#^api/v1/(?:attendance|attendance-tracking|attendance-comment-templates|attendance-templates|incidents)(?:/|$)#', 'grading_scores_and_reporting' => '#^api/v1/(?:scores|grading|reports|badges|certificates|print-requests|subjects/curriculum|exams/drafts)(?:/|$)#', 'finance_billing_and_inventory' => '#^api/v1/(?:finance|discounts|expenses|extra-charges|inventory|files)(?:/|$)#', 'communications_and_messaging' => '#^api/v1/(?:messages|broadcast-email|email|email-extractor|communications|support|whatsapp)(?:/|$)#', 'competition_and_public_results' => '#^api/v1/(?:competition-scores|competition-winners)(?:/|$)#', ]; } private function domainFor(string $uri): ?string { foreach ($this->useCaseDomains() as $domain => $pattern) { if (preg_match($pattern, $uri) === 1) { return $domain; } } return null; } /** * @return list */ private function apiRoutes(): array { return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool { return str_starts_with($route->uri(), 'api/'); })); } /** * @return list */ private function protectedRoutes(): array { return array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { if ($this->isStreamOrGeneratedFileRoute($route->uri())) { return false; } return collect($route->gatherMiddleware())->contains(function (string $middleware): bool { return str_contains($middleware, 'auth:api') || str_contains($middleware, 'jwt.auth') || str_contains($middleware, 'admin.access') || str_contains($middleware, 'parent.access') || str_contains($middleware, 'teacher.portal.auth'); }); })); } private function isStreamOrGeneratedFileRoute(string $uri): bool { return str_contains($uri, 'files/') || str_contains($uri, 'receipts/') || str_contains($uri, 'reimbursements/') || str_contains($uri, 'attachments/') || str_contains($uri, '/pdf') || str_contains($uri, '/csv'); } private function primaryMethod(LaravelRoute $route): string { foreach ($route->methods() as $method) { if (! in_array($method, ['HEAD', 'OPTIONS'], true)) { return $method; } } return 'GET'; } private function uriWithSafePlaceholders(string $uri): string { return preg_replace('/\{[^}]+\}/', '1', $uri) ?? $uri; } }