Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/Illuminate/Foundation/Testing/Concerns/MakesHttpRequests.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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.
*
Expand Down
62 changes: 62 additions & 0 deletions tests/Foundation/Testing/Concerns/MakesHttpRequestsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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<string, array{string|BackedEnum, ?string, \Throwable}>
*/
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<string, array{string|BackedEnum, ?string}>
*/
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
Expand Down
Loading