apiRoutes() as $route) { $action = $route->getActionName(); if ($action === 'Closure' || ! str_contains($action, '@')) { continue; } [$class, $method] = explode('@', $action, 2); if (! class_exists($class) || ! method_exists($class, $method)) { $missing[] = implode('|', $route->methods()) . ' ' . $route->uri() . ' -> ' . $action; } } $this->assertSame([], $missing, "These API routes reference missing controllers or methods:\n" . implode("\n", $missing)); } public function test_api_routes_do_not_duplicate_method_and_uri_pairs(): void { $seen = []; $duplicates = []; foreach ($this->apiRoutes() as $route) { foreach ($route->methods() as $method) { if ($method === 'HEAD') { continue; } $key = $method . ' ' . $route->uri(); if (isset($seen[$key])) { $duplicates[] = $key; } $seen[$key] = true; } } $this->assertSame([], array_values(array_unique($duplicates)), "Duplicate API route method/URI pairs found. Ambiguous routing is not a feature, despite humanity's best efforts."); } public function test_mutating_api_routes_are_protected_unless_explicitly_public(): void { $publicAllowList = [ 'api/login', 'api/register', 'api/v1/login', 'api/v1/register', 'api/v1/auth/login', 'api/v1/auth/register', 'api/v1/pages/contact', 'api/v1/contact', 'api/set_authorized_user_password/{authorizedUserId}', 'api/v1/badge_scan/scan', 'api/v1/scanner/process', ]; $unprotected = []; foreach ($this->apiRoutes() as $route) { $methods = array_diff($route->methods(), ['HEAD', 'OPTIONS']); if (array_intersect($methods, ['POST', 'PUT', 'PATCH', 'DELETE']) === []) { continue; } if (in_array($route->uri(), $publicAllowList, true)) { continue; } $middleware = $route->gatherMiddleware(); $hasProtection = collect($middleware)->contains(function (string $middleware): bool { return str_contains($middleware, 'auth') || str_contains($middleware, 'throttle') || str_contains($middleware, 'teacher.portal.auth') || str_contains($middleware, 'badge_scan'); }); if (! $hasProtection) { $unprotected[] = implode('|', $methods) . ' ' . $route->uri(); } } $this->assertSame([], $unprotected, "Mutating API routes without an obvious auth/throttle middleware:\n" . implode("\n", $unprotected)); } /** * @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/'); })); } }