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

110 lines
3.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\TestCase;
class ApiRouteContractTest extends TestCase
{
use RefreshDatabase;
public function test_every_api_route_points_to_an_existing_controller_method(): void
{
$missing = [];
foreach ($this->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<LaravelRoute>
*/
private function apiRoutes(): array
{
return array_values(array_filter(Route::getRoutes()->getRoutes(), function (LaravelRoute $route): bool {
return str_starts_with($route->uri(), 'api/');
}));
}
}