110 lines
3.6 KiB
PHP
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/');
|
|
}));
|
|
}
|
|
}
|