divineniiquaye /
flight-routing
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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
Loading history...
|
|||||||
| 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 |