mutatingRoutes() as $route) { $method = $this->primaryMethod($route); $uri = $route->uri(); foreach ($this->hostilePayloads() as $label => $payload) { $response = $this->requestAs($this->actorFor($uri), $method, $uri, $payload); $status = $response->getStatusCode(); if ($status >= 500) { $failures[] = "$method $uri crashed on $label hostile payload with $status: ".$response->getContent(); continue; } if (! in_array($status, [200, 201, 202, 204, 302, 400, 401, 403, 404, 405, 409, 419, 422, 429], true)) { $failures[] = "$method $uri returned unexpected $status on $label hostile payload: ".$response->getContent(); } } } $this->assertSame([], $failures, "Hostile payloads must fail cleanly, not leak framework explosions:\n".implode("\n", $failures)); } public function test_required_field_validation_uses_machine_readable_error_shape_when_validation_fails(): void { $failures = []; foreach ($this->validationSensitiveRoutes() as $route) { $method = $this->primaryMethod($route); $uri = $route->uri(); $response = $this->requestAs($this->actorFor($uri), $method, $uri, []); if ($response->getStatusCode() !== 422) { continue; } if (! $this->isJsonString($response->getContent())) { $failures[] = "$method $uri returned 422 but not JSON: ".$response->getContent(); continue; } $json = $response->json(); if (! is_array($json) || (! array_key_exists('errors', $json) && ! array_key_exists('message', $json))) { $failures[] = "$method $uri returned 422 without message/errors: ".$response->getContent(); } } $this->assertSame([], $failures, "Validation errors must be usable by clients, not poetic prose:\n".implode("\n", $failures)); } /** @return list */ private function mutatingRoutes(): array { return array_slice(array_values(array_filter($this->apiRoutes(), function (LaravelRoute $route): bool { return in_array($this->primaryMethod($route), ['POST', 'PUT', 'PATCH', 'DELETE'], true) && ! str_contains($route->uri(), 'auth/logout'); })), 0, 150); } /** @return list */ private function validationSensitiveRoutes(): array { return array_values(array_filter($this->mutatingRoutes(), function (LaravelRoute $route): bool { $uri = $route->uri(); return str_contains($uri, 'students') || str_contains($uri, 'users') || str_contains($uri, 'attendance') || str_contains($uri, 'finance') || str_contains($uri, 'scores') || str_contains($uri, 'support') || str_contains($uri, 'inventory'); })); } /** @return array> */ private function hostilePayloads(): array { return [ 'empty' => [], 'wrong_scalar_types' => ['id' => 'not-an-int', 'amount' => 'NaN', 'date' => 'not-a-date', 'email' => 'not-email'], 'unexpected_nested_arrays' => ['name' => ['nested' => ['too' => ['deep' => true]]], 'student_id' => ['x']], 'oversized_strings' => ['name' => str_repeat('x', 1024), 'description' => str_repeat('y', 2048)], ]; } }