1 | <?php declare(strict_types=1); |
||||||
2 | |||||||
3 | /* |
||||||
4 | * This file is part of Flight Routing. |
||||||
5 | * |
||||||
6 | * PHP version 8.0 and above required |
||||||
7 | * |
||||||
8 | * @author Divine Niiquaye Ibok <[email protected]> |
||||||
9 | * @copyright 2019 Divine Niiquaye Ibok (https://divinenii.com/) |
||||||
10 | * @license https://opensource.org/licenses/BSD-3-Clause License |
||||||
11 | * |
||||||
12 | * For the full copyright and license information, please view the LICENSE |
||||||
13 | * file that was distributed with this source code. |
||||||
14 | */ |
||||||
15 | |||||||
16 | use Flight\Routing\Exceptions\{UriHandlerException, UrlGenerationException}; |
||||||
17 | use Flight\Routing\RouteUri as GeneratedUri; |
||||||
18 | use PHPUnit\Framework as t; |
||||||
19 | |||||||
20 | dataset('patterns', [ |
||||||
21 | [ |
||||||
22 | '/foo', |
||||||
23 | '{^/foo$}', |
||||||
24 | [], |
||||||
25 | ['/foo'], |
||||||
26 | ], |
||||||
27 | [ |
||||||
28 | '/foo/', |
||||||
29 | '{^/foo/?$}', |
||||||
30 | [], |
||||||
31 | ['/foo', '/foo/'], |
||||||
32 | ], |
||||||
33 | [ |
||||||
34 | '/foo/{bar}', |
||||||
35 | '{^/foo/(?P<bar>[^\/]+)$}', |
||||||
36 | ['bar' => null], |
||||||
37 | ['/foo/bar', '/foo/baz'], |
||||||
38 | ], |
||||||
39 | [ |
||||||
40 | '/foo/{bar}@', |
||||||
41 | '{^/foo/(?P<bar>[^\/]+)@$}', |
||||||
42 | ['bar' => null], |
||||||
43 | ['/foo/bar@', '/foo/baz@'], |
||||||
44 | ], |
||||||
45 | [ |
||||||
46 | '/foo-{bar}', |
||||||
47 | '{^/foo-(?P<bar>[^\/]+)$}', |
||||||
48 | ['bar' => null], |
||||||
49 | ['/foo-bar', '/foo-baz'], |
||||||
50 | ], |
||||||
51 | [ |
||||||
52 | '/foo/{bar}/{baz}/', |
||||||
53 | '{^/foo/(?P<bar>[^\/]+)/(?P<baz>[^\/]+)/?$}', |
||||||
54 | ['bar' => null, 'baz' => null], |
||||||
55 | ['/foo/bar/baz', '/foo/bar/baz/'], |
||||||
56 | ], |
||||||
57 | [ |
||||||
58 | '/foo/{bar:\d+}', |
||||||
59 | '{^/foo/(?P<bar>\d+)$}', |
||||||
60 | ['bar' => null], |
||||||
61 | ['/foo/123', '/foo/444'], |
||||||
62 | ], |
||||||
63 | [ |
||||||
64 | '/foo/{bar:\d+}/{baz}/', |
||||||
65 | '{^/foo/(?P<bar>\d+)/(?P<baz>[^\/]+)/?$}', |
||||||
66 | ['bar' => null, 'baz' => null], |
||||||
67 | ['/foo/123/baz', '/foo/123/baz/'], |
||||||
68 | ], |
||||||
69 | [ |
||||||
70 | '/foo/{bar:\d+}/{baz:slug}', |
||||||
71 | '{^/foo/(?P<bar>\d+)/(?P<baz>[A-Za-z0-9]+(?:-[A-Za-z0-9]+)*)$}', |
||||||
72 | ['bar' => null, 'baz' => null], |
||||||
73 | ['/foo/123/foo', '/foo/44/baz'], |
||||||
74 | ], |
||||||
75 | [ |
||||||
76 | '/foo/{bar=0}', |
||||||
77 | '{^/foo/(?P<bar>[^\/]+)$}', |
||||||
78 | ['bar' => '0'], |
||||||
79 | ['/foo/0'], |
||||||
80 | ], |
||||||
81 | [ |
||||||
82 | '/foo/{bar=baz}/{baz}/', |
||||||
83 | '{^/foo/(?P<bar>[^\/]+)/(?P<baz>[^\/]+)/?$}', |
||||||
84 | ['bar' => 'baz', 'baz' => null], |
||||||
85 | ['/foo/baz/baz', '/foo/baz/baz/'], |
||||||
86 | ], |
||||||
87 | [ |
||||||
88 | '/[{foo}]', |
||||||
89 | '{^/?(?:(?P<foo>[^\/]+))?$}', |
||||||
90 | ['foo' => null], |
||||||
91 | ['/', '/foo', '/bar'], |
||||||
92 | ], |
||||||
93 | [ |
||||||
94 | '/[{bar:(foo|bar)}]', |
||||||
95 | '{^/?(?:(?P<bar>(foo|bar)))?$}', |
||||||
96 | ['bar' => null], |
||||||
97 | ['/', '/foo', '/bar'], |
||||||
98 | ], |
||||||
99 | [ |
||||||
100 | '/foo[/{bar}]/', |
||||||
101 | '{^/foo(?:/(?P<bar>[^\/]+))?/?$}', |
||||||
102 | ['bar' => null], |
||||||
103 | ['/foo', '/foo/', '/foo/bar', '/foo/bar/'], |
||||||
104 | ], |
||||||
105 | [ |
||||||
106 | '/[{foo:upper}]/[{bar:lower}]', |
||||||
107 | '{^/?(?:(?P<foo>[A-Z]+))?/?(?:(?P<bar>[a-z]+))?$}', |
||||||
108 | ['foo' => null, 'bar' => null], |
||||||
109 | ['/', '/FOO', '/FOO/', '/FOO/bar', '/bar'], |
||||||
110 | ], |
||||||
111 | [ |
||||||
112 | '/[{foo}][/{bar:month}]', |
||||||
113 | '{^/?(?:(?P<foo>[^\/]+))?(?:/(?P<bar>0[1-9]|1[012]+))?$}', |
||||||
114 | ['foo' => null, 'bar' => null], |
||||||
115 | ['/', '/foo', '/bar', '/foo/12', '/foo/01'], |
||||||
116 | ], |
||||||
117 | [ |
||||||
118 | '/[{foo:lower}/[{bar:upper}]]', |
||||||
119 | '{^/?(?:(?P<foo>[a-z]+)/?(?:(?P<bar>[A-Z]+))?)?$}', |
||||||
120 | ['foo' => null, 'bar' => null], |
||||||
121 | ['/', '/foo', '/foo/', '/foo/BAR', '/foo/BAZ'], |
||||||
122 | ], |
||||||
123 | [ |
||||||
124 | '/[{foo}/{bar}]', |
||||||
125 | '{^/?(?:(?P<foo>[^\/]+)/(?P<bar>[^\/]+))?$}', |
||||||
126 | ['foo' => null, 'bar' => null], |
||||||
127 | ['/', '/foo/bar', '/foo/baz'], |
||||||
128 | ], |
||||||
129 | [ |
||||||
130 | '/who{are}you', |
||||||
131 | '{^/who(?P<are>[^\/]+)you$}', |
||||||
132 | ['are' => null], |
||||||
133 | ['/whoareyou', '/whoisyou'], |
||||||
134 | ], |
||||||
135 | [ |
||||||
136 | '/[{lang:[a-z]{2}}/]hello', |
||||||
137 | '{^/?(?:(?P<lang>[a-z]{2})/)?hello$}', |
||||||
138 | ['lang' => null], |
||||||
139 | ['/hello', '/en/hello', '/fr/hello'], |
||||||
140 | ], |
||||||
141 | [ |
||||||
142 | '/[{lang:[\w+\-]+=english}/]hello', |
||||||
143 | '{^/?(?:(?P<lang>[\w+\-]+)/)?hello$}', |
||||||
144 | ['lang' => 'english'], |
||||||
145 | ['/hello', '/en/hello', '/fr/hello'], |
||||||
146 | ], |
||||||
147 | [ |
||||||
148 | '/[{lang:[a-z]{2}}[-{sublang}]/]{name}[/page-{page=0}]', |
||||||
149 | '{^/?(?:(?P<lang>[a-z]{2})(?:-(?P<sublang>[^\/]+))?/)?(?P<name>[^\/]+)(?:/page-(?P<page>[^\/]+))?$}', |
||||||
150 | ['lang' => null, 'sublang' => null, 'name' => null, 'page' => '0'], |
||||||
151 | ['/hello', '/en/hello', '/en-us/hello', '/en-us/hello/page-1', '/en-us/hello/page-2'], |
||||||
152 | ], |
||||||
153 | [ |
||||||
154 | '/hello/{foo:[a-z]{3}=bar}{baz}/[{id:[0-9a-fA-F]{1,8}}[.{format:html|php}]]', |
||||||
155 | '{^/hello/(?P<foo>[a-z]{3})(?P<baz>[^\/]+)/?(?:(?P<id>[0-9a-fA-F]{1,8})(?:\.(?P<format>html|php))?)?$}', |
||||||
156 | ['foo' => 'bar', 'baz' => null, 'id' => null, 'format' => null], |
||||||
157 | ['/hello/barbaz', '/hello/barbaz/', '/hello/barbaz/1', '/hello/barbaz/1.html', '/hello/barbaz/1.php'], |
||||||
158 | ], |
||||||
159 | [ |
||||||
160 | '/hello/{foo:\w{3}}{bar=bar1}/world/[{name:[A-Za-z]+}[/{page:int=1}[/{baz:year}]]]/abs.{format:html|php}', |
||||||
161 | '{^/hello/(?P<foo>\w{3})(?P<bar>[^\/]+)/world/?(?:(?P<name>[A-Za-z]+)(?:/(?P<page>[0-9]+)(?:/(?P<baz>[0-9]{4}))?)?)?/abs\.(?P<format>html|php)$}', |
||||||
162 | ['foo' => null, 'bar' => 'bar1', 'name' => null, 'page' => '1', 'baz' => null, 'format' => null], |
||||||
163 | [ |
||||||
164 | '/hello/foobar/world/abs.html', |
||||||
165 | '/hello/barfoo/world/divine/abs.php', |
||||||
166 | '/hello/foobaz/world/abs.php', |
||||||
167 | '/hello/bar100/world/divine/11/abs.html', |
||||||
168 | '/hello/true/world/divine/11/2022/abs.html', |
||||||
169 | ], |
||||||
170 | ], |
||||||
171 | [ |
||||||
172 | '{foo}.example.com', |
||||||
173 | '{^(?P<foo>[^\/]+)\.example\.com$}', |
||||||
174 | ['foo' => null], |
||||||
175 | ['foo.example.com', 'bar.example.com'], |
||||||
176 | ], |
||||||
177 | [ |
||||||
178 | '{locale}.example.{tld}', |
||||||
179 | '{^(?P<locale>[^\/]+)\.example\.(?P<tld>[^\/]+)$}', |
||||||
180 | ['locale' => null, 'tld' => null], |
||||||
181 | ['en.example.com', 'en.example.org', 'en.example.co.uk'], |
||||||
182 | ], |
||||||
183 | [ |
||||||
184 | '[{lang:[a-z]{2}}.]example.com', |
||||||
185 | '{^(?:(?P<lang>[a-z]{2})\.)?example\.com$}', |
||||||
186 | ['lang' => null], |
||||||
187 | ['en.example.com', 'example.com', 'fr.example.com'], |
||||||
188 | ], |
||||||
189 | [ |
||||||
190 | '[{lang:[a-z]{2}}.]example.{tld=com}', |
||||||
191 | '{^(?:(?P<lang>[a-z]{2})\.)?example\.(?P<tld>[^\/]+)$}', |
||||||
192 | ['lang' => null, 'tld' => 'com'], |
||||||
193 | ['en.example.com', 'example.com', 'fr.example.gh'], |
||||||
194 | ], |
||||||
195 | [ |
||||||
196 | '{id:int}.example.com', |
||||||
197 | '{^(?P<id>[0-9]+)\.example\.com$}', |
||||||
198 | ['id' => null], |
||||||
199 | ['1.example.com', '2.example.com'], |
||||||
200 | ], |
||||||
201 | ]); |
||||||
202 | |||||||
203 | dataset('reversed', [ |
||||||
204 | [ |
||||||
205 | '/foo', |
||||||
206 | ['/foo' => []], |
||||||
207 | ], |
||||||
208 | [ |
||||||
209 | '/foo/{bar}', |
||||||
210 | ['/foo/bar' => ['bar' => 'bar']], |
||||||
211 | ], |
||||||
212 | [ |
||||||
213 | '/foo/{bar}/{baz}', |
||||||
214 | ['/foo/bar/baz' => ['bar' => 'bar', 1 => 'baz']], |
||||||
215 | ], |
||||||
216 | [ |
||||||
217 | '/divine/{id:\d+}[/{a}{b}[{c}]][.p[{d}]]', |
||||||
218 | [ |
||||||
219 | '/divine/23' => ['id' => '23'], |
||||||
220 | '/divine/23/ab' => ['23', 'a' => 'a', 'b' => 'b'], |
||||||
221 | '/divine/23/abc' => ['23', 'a' => 'a', 'b' => 'b', 'c' => 'c'], |
||||||
222 | '/divine/23/abc.php' => ['23', 'a' => 'a', 'b' => 'b', 'c' => 'c', 'd' => 'hp'], |
||||||
223 | '/divine/23.phtml' => ['id' => '23', 'd' => 'html'], |
||||||
224 | ], |
||||||
225 | ], |
||||||
226 | ]); |
||||||
227 | |||||||
228 | test('if route path is a valid regex', function (string $path, string $regex, array $vars, array $matches): void { |
||||||
229 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
230 | [$pathRegex, $pathVar] = $compiler->compile($path); |
||||||
231 | |||||||
232 | t\assertEquals($regex, $pathRegex); |
||||||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||||
233 | t\assertSame($vars, $pathVar); |
||||||
0 ignored issues
–
show
The function
assertSame was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
234 | |||||||
235 | // Match every pattern... |
||||||
236 | foreach ($matches as $match) { |
||||||
237 | t\assertMatchesRegularExpression($pathRegex, $match); |
||||||
0 ignored issues
–
show
The function
assertMatchesRegularExpression was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
238 | } |
||||||
239 | })->with('patterns'); |
||||||
240 | |||||||
241 | test('if compiled route path is reversible', function (string $path, array $matches): void { |
||||||
242 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
243 | |||||||
244 | foreach ($matches as $match => $params) { |
||||||
245 | t\assertEquals($match, (string) $compiler->generateUri(['path' => $path], $params)); |
||||||
0 ignored issues
–
show
The function
assertEquals was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
246 | } |
||||||
247 | })->with('reversed'); |
||||||
248 | |||||||
249 | test('if route path placeholder is characters length is invalid', function (): void { |
||||||
250 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
251 | $compiler->compile('/{sfkdfglrjfdgrfhgklfhgjhfdjghrtnhrnktgrelkrngldrjhglhkjdfhgkj}'); |
||||||
252 | })->throws( |
||||||
253 | UriHandlerException::class, |
||||||
254 | \sprintf( |
||||||
255 | 'Variable name "%s" cannot be longer than 32 characters in route pattern "/{%1$s}".', |
||||||
256 | 'sfkdfglrjfdgrfhgklfhgjhfdjghrtnhrnktgrelkrngldrjhglhkjdfhgkj' |
||||||
257 | ) |
||||||
258 | ); |
||||||
259 | |||||||
260 | test('if route path placeholder begins with a number', function (): void { |
||||||
261 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
262 | $compiler->compile('/{1foo}'); |
||||||
263 | })->throws( |
||||||
264 | UriHandlerException::class, |
||||||
265 | 'Variable name "1foo" cannot start with a digit in route pattern "/{1foo}". Use a different name.' |
||||||
266 | ); |
||||||
267 | |||||||
268 | test('if route path placeholder is used more than once', function (): void { |
||||||
269 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
270 | $compiler->compile('/{foo}/{foo}'); |
||||||
271 | })->throws( |
||||||
272 | UriHandlerException::class, |
||||||
273 | 'Route pattern "/{foo}/{foo}" cannot reference variable name "foo" more than once.' |
||||||
274 | ); |
||||||
275 | |||||||
276 | test('if route path placeholder has regex values', function (string $path, array $segment): void { |
||||||
277 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
278 | [$pathRegex] = $compiler->compile($path, $segment); |
||||||
279 | t\assertMatchesRegularExpression($pathRegex, '/a'); |
||||||
0 ignored issues
–
show
The function
assertMatchesRegularExpression was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
280 | t\assertMatchesRegularExpression($pathRegex, '/b'); |
||||||
281 | })->with([ |
||||||
282 | ['/{foo}', ['foo' => ['a', 'b']]], |
||||||
283 | ['/{foo}', ['foo' => 'a|b']], |
||||||
284 | ]); |
||||||
285 | |||||||
286 | test('if route path placeholder requirement is empty', function (string $assert): void { |
||||||
287 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
288 | $compiler->compile('/{foo}', ['foo' => $assert]); |
||||||
289 | })->with(['', '^$', '^', '$', '\A\z', '\A', '\z'])->throws( |
||||||
290 | UriHandlerException::class, |
||||||
291 | 'Routing requirement for "foo" cannot be empty.' |
||||||
292 | ); |
||||||
293 | |||||||
294 | test('if reversed generated route path can contain http scheme and host', function (): void { |
||||||
295 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
296 | $_SERVER['HTTP_HOST'] = 'example.com'; |
||||||
297 | $route = ['path' => '/{foo}', 'schemes' => ['http' => true]]; |
||||||
298 | |||||||
299 | t\assertEquals('/a', (string) $compiler->generateUri($route, ['foo' => 'a'])); |
||||||
0 ignored issues
–
show
The function
assertEquals was not found. Maybe you did not declare it correctly or list all dependencies?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||||
300 | t\assertEquals('./b', (string) $compiler->generateUri($route, ['foo' => 'b'], GeneratedUri::RELATIVE_PATH)); |
||||||
301 | t\assertEquals('//example.com/c', (string) $compiler->generateUri($route, ['c'], GeneratedUri::NETWORK_PATH)); |
||||||
302 | t\assertEquals('http://example.com/d', (string) $compiler->generateUri($route, [0 => 'd'], GeneratedUri::ABSOLUTE_URL)); |
||||||
303 | t\assertEquals('http://localhost/e', (string) $compiler->generateUri( |
||||||
304 | $route += ['hosts' => ['localhost' => true]], |
||||||
305 | ['foo' => 'e'], |
||||||
306 | GeneratedUri::ABSOLUTE_URL |
||||||
307 | )); |
||||||
308 | }); |
||||||
309 | |||||||
310 | test('if reversed generated route fails to certain placeholders', function (): void { |
||||||
311 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
312 | $compiler->generateUri(['path' => '/{foo:int}'], ['foo' => 'a']); |
||||||
313 | })->throws( |
||||||
314 | UriHandlerException::class, |
||||||
315 | 'Expected route path "/<foo>" placeholder "foo" value "a" to match "[0-9]+".' |
||||||
316 | ); |
||||||
317 | |||||||
318 | test('if reversed generate route is missing required placeholders', function (): void { |
||||||
319 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
320 | $compiler->generateUri(['path' => '/{foo}'], []); |
||||||
321 | })->throws( |
||||||
322 | UrlGenerationException::class, |
||||||
323 | 'Some mandatory parameters are missing ("foo") to generate a URL for route path "/<foo>".' |
||||||
324 | ); |
||||||
325 | |||||||
326 | test('if reversed generate route can contain a negative port port', function (): void { |
||||||
327 | $compiler = new \Flight\Routing\RouteCompiler(); |
||||||
328 | $compiler->generateUri(['path' => '/{foo}'], ['flight-routing'])->withPort(-9); |
||||||
329 | })->throws(UrlGenerationException::class, 'Invalid port: -9. Must be between 0 and 65535'); |
||||||
330 |