Skip to content

Commit 873a47c

Browse files
committed
feat: add tests for optional route parameters
1 parent 7001e22 commit 873a47c

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Router\Tests;
6+
7+
use ReflectionMethod;
8+
use Tempest\Http\Method;
9+
use Tempest\Reflection\MethodReflector;
10+
use Tempest\Router\HttpMiddleware;
11+
use Tempest\Router\Route;
12+
use Tempest\Router\Routing\Construction\DiscoveredRoute;
13+
use Tempest\Router\Routing\Construction\MarkedRoute;
14+
15+
final class FakeRouteBuilderWithOptionalParams implements Route
16+
{
17+
private MethodReflector $handler;
18+
19+
public function __construct(
20+
public Method $method = Method::GET,
21+
public string $uri = '/',
22+
/** @var class-string<HttpMiddleware>[] */
23+
public array $middleware = [],
24+
public array $without = [],
25+
private string $handlerMethod = 'handler',
26+
) {
27+
$this->handler = new MethodReflector(new ReflectionMethod($this, $this->handlerMethod));
28+
}
29+
30+
public function withUri(string $uri): self
31+
{
32+
return new self($this->method, $uri, $this->middleware, $this->without, $this->handlerMethod);
33+
}
34+
35+
public function withMethod(Method $method): self
36+
{
37+
return new self($method, $this->uri, $this->middleware, $this->without, $this->handlerMethod);
38+
}
39+
40+
public function withHandler(string $handlerMethod): self
41+
{
42+
return new self($this->method, $this->uri, $this->middleware, $this->without, $handlerMethod);
43+
}
44+
45+
public function asMarkedRoute(string $mark): MarkedRoute
46+
{
47+
return new MarkedRoute($mark, $this->asDiscoveredRoute());
48+
}
49+
50+
public function asDiscoveredRoute(): DiscoveredRoute
51+
{
52+
return DiscoveredRoute::fromRoute($this, [], $this->handler);
53+
}
54+
55+
public function handler(): void
56+
{
57+
}
58+
59+
public function handlerWithOptionalId(string $id = 'default-id'): void
60+
{
61+
}
62+
63+
public function handlerWithTwoOptionalParams(string $id = 'default-id', string $slug = 'default-slug'): void
64+
{
65+
}
66+
67+
public function handlerWithRequiredAndOptional(string $id, string $slug = 'default-slug'): void
68+
{
69+
}
70+
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Router\Tests;
6+
7+
use PHPUnit\Framework\TestCase;
8+
use Tempest\Http\GenericRequest;
9+
use Tempest\Http\Method;
10+
use Tempest\Router\RouteConfig;
11+
use Tempest\Router\Routing\Construction\RoutingTree;
12+
use Tempest\Router\Routing\Matching\GenericRouteMatcher;
13+
14+
/**
15+
* @internal
16+
*/
17+
final class OptionalParametersTest extends TestCase
18+
{
19+
public function test_route_with_optional_parameter_matches_both_paths(): void
20+
{
21+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
22+
$tree = new RoutingTree();
23+
24+
$markedRoute = $routeBuilder
25+
->withUri('/users/{id?}')
26+
->withHandler('handlerWithOptionalId')
27+
->asMarkedRoute('a');
28+
29+
$tree->add($markedRoute);
30+
$regexes = $tree->toMatchingRegexes();
31+
32+
$routeConfig = new RouteConfig(
33+
staticRoutes: [],
34+
dynamicRoutes: [
35+
'GET' => [
36+
'a' => $markedRoute->route,
37+
],
38+
],
39+
matchingRegexes: $regexes,
40+
);
41+
42+
$matcher = new GenericRouteMatcher($routeConfig);
43+
44+
$matchedWithoutParam = $matcher->match(new GenericRequest(Method::GET, '/users'));
45+
$this->assertNotNull($matchedWithoutParam);
46+
$this->assertEquals('/users/{id?}', $matchedWithoutParam->route->uri);
47+
$this->assertEquals(['id' => 'default-id'], $matchedWithoutParam->params);
48+
49+
$matchedWithParam = $matcher->match(new GenericRequest(Method::GET, '/users/123'));
50+
$this->assertNotNull($matchedWithParam);
51+
$this->assertEquals('/users/{id?}', $matchedWithParam->route->uri);
52+
$this->assertEquals(['id' => '123'], $matchedWithParam->params);
53+
}
54+
55+
public function test_route_with_multiple_optional_parameters(): void
56+
{
57+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
58+
$tree = new RoutingTree();
59+
60+
$markedRoute = $routeBuilder
61+
->withUri('/posts/{id?}/{slug?}')
62+
->withHandler('handlerWithTwoOptionalParams')
63+
->asMarkedRoute('a');
64+
65+
$tree->add($markedRoute);
66+
$regexes = $tree->toMatchingRegexes();
67+
68+
$routeConfig = new RouteConfig(
69+
staticRoutes: [],
70+
dynamicRoutes: [
71+
'GET' => [
72+
'a' => $markedRoute->route,
73+
],
74+
],
75+
matchingRegexes: $regexes,
76+
);
77+
78+
$matcher = new GenericRouteMatcher($routeConfig);
79+
80+
$matchedNoParams = $matcher->match(new GenericRequest(Method::GET, '/posts'));
81+
$this->assertNotNull($matchedNoParams);
82+
$this->assertEquals(['id' => 'default-id', 'slug' => 'default-slug'], $matchedNoParams->params);
83+
84+
$matchedOneParam = $matcher->match(new GenericRequest(Method::GET, '/posts/123'));
85+
$this->assertNotNull($matchedOneParam);
86+
$this->assertEquals(['id' => '123', 'slug' => 'default-slug'], $matchedOneParam->params);
87+
88+
$matchedTwoParams = $matcher->match(new GenericRequest(Method::GET, '/posts/123/my-post'));
89+
$this->assertNotNull($matchedTwoParams);
90+
$this->assertEquals(['id' => '123', 'slug' => 'my-post'], $matchedTwoParams->params);
91+
}
92+
93+
public function test_route_with_required_and_optional_parameters(): void
94+
{
95+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
96+
$tree = new RoutingTree();
97+
98+
$markedRoute = $routeBuilder
99+
->withUri('/posts/{id}/{slug?}')
100+
->withHandler('handlerWithRequiredAndOptional')
101+
->asMarkedRoute('a');
102+
103+
$tree->add($markedRoute);
104+
$regexes = $tree->toMatchingRegexes();
105+
106+
$routeConfig = new RouteConfig(
107+
staticRoutes: [],
108+
dynamicRoutes: [
109+
'GET' => [
110+
'a' => $markedRoute->route,
111+
],
112+
],
113+
matchingRegexes: $regexes,
114+
);
115+
116+
$matcher = new GenericRouteMatcher($routeConfig);
117+
118+
$matchedRequired = $matcher->match(new GenericRequest(Method::GET, '/posts/123'));
119+
$this->assertNotNull($matchedRequired);
120+
$this->assertEquals(['id' => '123', 'slug' => 'default-slug'], $matchedRequired->params);
121+
122+
$matchedBoth = $matcher->match(new GenericRequest(Method::GET, '/posts/123/my-post'));
123+
$this->assertNotNull($matchedBoth);
124+
$this->assertEquals(['id' => '123', 'slug' => 'my-post'], $matchedBoth->params);
125+
}
126+
127+
public function test_route_with_optional_parameter_and_custom_regex(): void
128+
{
129+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
130+
$tree = new RoutingTree();
131+
132+
$markedRoute = $routeBuilder
133+
->withUri('/users/{id?:\d+}')
134+
->withHandler('handlerWithOptionalId')
135+
->asMarkedRoute('a');
136+
137+
$tree->add($markedRoute);
138+
$regexes = $tree->toMatchingRegexes();
139+
140+
$routeConfig = new RouteConfig(
141+
staticRoutes: [],
142+
dynamicRoutes: [
143+
'GET' => [
144+
'a' => $markedRoute->route,
145+
],
146+
],
147+
matchingRegexes: $regexes,
148+
);
149+
150+
$matcher = new GenericRouteMatcher($routeConfig);
151+
152+
$matchedWithoutParam = $matcher->match(new GenericRequest(Method::GET, '/users'));
153+
$this->assertNotNull($matchedWithoutParam);
154+
$this->assertEquals(['id' => 'default-id'], $matchedWithoutParam->params);
155+
156+
$matchedWithNumeric = $matcher->match(new GenericRequest(Method::GET, '/users/123'));
157+
$this->assertNotNull($matchedWithNumeric);
158+
$this->assertEquals(['id' => '123'], $matchedWithNumeric->params);
159+
160+
$matchedWithNonNumeric = $matcher->match(new GenericRequest(Method::GET, '/users/abc'));
161+
$this->assertNull($matchedWithNonNumeric);
162+
}
163+
164+
public function test_optional_parameters_are_parsed_correctly(): void
165+
{
166+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
167+
168+
$route = $routeBuilder
169+
->withUri('/users/{id?}')
170+
->withHandler('handlerWithOptionalId')
171+
->asDiscoveredRoute();
172+
173+
$this->assertEquals(['id'], $route->parameters);
174+
$this->assertEquals(['id' => true], $route->optionalParameters);
175+
}
176+
177+
public function test_mixed_optional_and_required_parameters_are_parsed_correctly(): void
178+
{
179+
$routeBuilder = new FakeRouteBuilderWithOptionalParams();
180+
181+
$route = $routeBuilder
182+
->withUri('/posts/{id}/{slug?}')
183+
->withHandler('handlerWithRequiredAndOptional')
184+
->asDiscoveredRoute();
185+
186+
$this->assertEquals(['id', 'slug'], $route->parameters);
187+
$this->assertEquals(['id' => false, 'slug' => true], $route->optionalParameters);
188+
}
189+
}

0 commit comments

Comments
 (0)