diff --git a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php index 0e7b7d16319a..1f7c520bcc89 100644 --- a/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php +++ b/src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php @@ -12,6 +12,7 @@ use Illuminate\Testing\TestResponse; use Symfony\Component\HttpFoundation\File\UploadedFile as SymfonyUploadedFile; use Symfony\Component\HttpFoundation\Request as SymfonyRequest; +use Symfony\Component\Routing\Exception\RouteNotFoundException; trait MakesHttpRequests { @@ -581,6 +582,41 @@ public function json($method, $uri, array $data = [], array $headers = [], $opti ); } + public function callRoute(BackedEnum|string $name, array $parameters = [], array $cookies = [], array $files = [], array $headers = [], ?string $content = null, ?string $method = null): TestResponse + { + if ($name instanceof BackedEnum && ! is_string($name = $name->value)) { + throw new InvalidArgumentException('Attribute [name] expects a string backed enum.'); + } + + if (is_null($route = app('router')->getRoutes()->getByName($name))) { + throw new RouteNotFoundException("Route [{$name}] not defined."); + } + + if (count($route->methods) === 2 && in_array('GET', $route->methods) && in_array('HEAD', $route->methods) && $method === null) { + $method = 'GET'; + } + + if (count($route->methods) > 1 && $method === null) { + throw new InvalidArgumentException('This route supports multiple HTTP methods. Please provide one of: '.implode(', ', $route->methods)); + } + + if ($method !== null && in_array($method, $route->methods)) { + throw new InvalidArgumentException("HTTP method [{$method}] not support by this route. Please provide one of: ".implode(', ', $route->methods)); + } + + $method ??= $route->methods[0]; + + return $this->call( + $method, + $route->uri, + $parameters, + $cookies, + $files, + $this->transformHeadersToServerVars($headers), + $content, + ); + } + /** * Call the given URI and return the Response. * diff --git a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php index 86340e27f347..f0d3740ffab1 100644 --- a/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php +++ b/tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php @@ -6,7 +6,11 @@ use Illuminate\Contracts\Routing\UrlGenerator; use Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests; use Illuminate\Http\RedirectResponse; +use InvalidArgumentException; use Orchestra\Testbench\TestCase; +use PHPUnit\Framework\Attributes\DataProvider; +use Symfony\Component\Routing\Exception\RouteNotFoundException; +use UnitEnum; class MakesHttpRequestsTest extends TestCase { @@ -239,6 +243,64 @@ public function testWithPrecognition() ->assertHeader('Precognition', 'true') ->assertHeader('Precognition-Success', 'true'); } + + #[DataProvider('providesCallRouteExceptions')] + public function testCallRouteFails(string|UnitEnum $name, ?string $method, \Throwable $exception) + { + $this->expectExceptionObject($exception); + + $router = $this->app['router']->match(['PUT', 'PATCH'], '/', fn () => '')->name('test'); + $this->callRoute($name, method: $method); + } + + /** + * @return array + */ + public static function providesCallRouteExceptions(): array + { + return [ + 'integer-backed enum' => [RouteNumbers::Foo, null, new InvalidArgumentException('Attribute [name] expects a string backed enum.')], + 'non-existing route' => ['foo', null, new RouteNotFoundException("Route [{$name}] not defined.")], + 'ambigious HTTP method' => ['test', null, new InvalidArgumentException('This route supports multiple HTTP methods. Please provide one of: PUT, PATCH')], + 'wrong HTTP method' => ['test', 'DELETE', new InvalidArgumentException('HTTP method [DELETE] not support by this route. Please provide one of: PUT, PATCH')], + ]; + } + + #[DataProvider('providesCallRouteOptions')] + public function testCallRouteSucceeds(string|UnitEnum $name, ?string $method) + { + $this->app['router']->get('/', fn () => 'tada!')->name('foo'); + $this->app['router']->match(['PUT', 'PATCH'], '/', fn () => 'tada!')->name('bar'); + $this->app['router']->delete('/', fn () => 'tada!')->name('baz'); + $response = $this->callRoute($name, method: $method); + $response->assertOk(); + $response->assertSeeText('tada!'); + } + + /** + * @return array + */ + public static function providesCallRouteOptions(): array + { + return [ + 'unambigious HTTP method' => ['baz', null], + 'GET/HEAD HTTP method fallback' => [RouteNames::Foo, null], + 'provide HTTP method for ambigious route' => [RouteNames::Bar, 'PUT'], + 'provide HTTP method despite being unambigious' => ['baz', 'DELETE'], + ]; + } +} + +enum RouteNumbers: int +{ + case Foo = 1; + case Bar = 2; +} + +enum RouteNames: string +{ + case Foo = 'foo'; + case Bar = 'bar'; } class MyMiddleware