add api tests
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
<?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/');
|
||||
}));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user