"METHOD path" pairs registered in index.php */ private function registeredRoutes(): array { $src = file_get_contents(dirname(__DIR__) . '/public/index.php'); $this->assertNotFalse($src, 'index.php unreadable'); preg_match_all("/\\\$r->add\(\s*'([A-Z]+)'\s*,\s*'([^']+)'/", $src, $m, PREG_SET_ORDER); return array_map(static fn ($x) => $x[1] . ' ' . $x[2], $m); } public function testEveryExpectedRouteIsRegistered(): void { // Adding a route must also update this list — that is the point: the list is the // reviewed inventory of the app's public surface. $expected = [ 'POST /api/events', 'POST /api/heartbeat', 'GET /api/config', 'GET /api/csrf', 'POST /login', 'POST /logout', 'GET /api/events', 'GET /api/devices', 'GET /events/{id}', 'POST /events/{id}/ack', 'POST /events/{id}/false-positive', 'GET /api/zones/{camera}', 'GET /api/heatmap/{camera}', 'GET /api/fp-stats', 'GET /api/report', 'GET /api/export/labels', 'GET /api/actions', 'POST /api/actions', 'GET /api/cameras/{camera}/latest-snapshot', 'POST /api/zones', 'POST /api/zones/{id}/delete', 'GET /snap/{uuid}', 'GET /', 'GET /login', 'GET /zones', 'GET /report', 'GET /events/{id}/view', ]; $actual = $this->registeredRoutes(); sort($expected); $sorted = $actual; sort($sorted); $this->assertSame($expected, $sorted); } public function testNoRouteIsRegisteredTwice(): void { // A duplicate silently shadows the first registration (first match wins) — the kind of // copy-paste slip that survives review because both lines look correct in isolation. $routes = $this->registeredRoutes(); $this->assertSame(array_values(array_unique($routes)), $routes); } public function testEveryEndpointTheFrontendCallsExists(): void { // The frontend is vanilla JS with no build step, so a typo'd URL is caught by nothing — // no bundler, no type checker. Cross-check the two halves against each other. $registered = array_map(static fn ($r) => explode(' ', $r, 2)[1], $this->registeredRoutes()); $calls = []; foreach (glob(dirname(__DIR__) . '/public/assets/*.js') as $js) { preg_match_all("/B \+ '([^']+)'/", (string)file_get_contents($js), $m); foreach ($m[1] as $raw) { // The JS concatenates ids onto a literal prefix ("B + '/events/' + id"), so what // is visible here is a PREFIX, not a full path. Strip the query string and the // trailing slash to get the literal part. $path = rtrim(strtok($raw, '?'), '/'); $calls[$path === '' ? '/' : $path] = basename($js); } } foreach ($calls as $path => $file) { // Match on a SEGMENT boundary: '/events' is satisfied by '/events/{id}', but a typo // like '/api/actio' is NOT satisfied by '/api/actions' (no '/api/actio/' route). $hit = in_array($path, $registered, true); foreach ($registered as $route) { if ($hit) break; $hit = str_starts_with($route, rtrim($path, '/') . '/'); } $this->assertTrue($hit, "$file calls $path but index.php registers no such route"); } } }