Issues (120)

tests/RouterTest.php (18 issues)

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\MethodNotAllowedException;
17
use Flight\Routing\Exceptions\RouteNotFoundException;
18
use Flight\Routing\Exceptions\UriHandlerException;
19
use Flight\Routing\Exceptions\UrlGenerationException;
20
use Flight\Routing\Handlers\CallbackHandler;
21
use Flight\Routing\Handlers\ResourceHandler;
22
use Flight\Routing\Handlers\RouteHandler;
23
use Flight\Routing\RouteCollection;
24
use Flight\Routing\RouteCompiler;
25
use Flight\Routing\Router;
26
use Flight\Routing\ROuteUri as GeneratedUri;
27
use Flight\Routing\Tests\Fixtures\BlankRequestHandler;
28
use Laminas\Stratigility\Next;
29
use Nyholm\Psr7\Factory\Psr17Factory;
30
use Nyholm\Psr7\ServerRequest;
31
use Nyholm\Psr7\Uri;
32
use PHPUnit\Framework as t;
33
use Psr\Http\Message\ServerRequestInterface;
34
use Psr\Http\Server\RequestHandlerInterface;
35
36
use function Laminas\Stratigility\middleware;
37
38
test('if method not found in matched route', function (): void {
39
    $collection = new RouteCollection();
40
    $collection->add('/hello', ['POST']);
41
42
    $router = Router::withCollection($collection);
43
44
    try {
45
        $router->match('GET', new Uri('/hello'));
46
        $this->fail('Expected a method nor found exception to be thrown');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $this seems to be never defined.
Loading history...
47
    } catch (MethodNotAllowedException $e) {
48
        t\assertSame(['POST'], $e->getAllowedMethods());
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 ignore-call  annotation

48
        /** @scrutinizer ignore-call */ 
49
        t\assertSame(['POST'], $e->getAllowedMethods());
Loading history...
49
50
        throw $e;
51
    }
52
})->throws(
53
    MethodNotAllowedException::class,
54
    'Route with "/hello" path requires request method(s) [POST], "GET" is invalid.'
55
);
56
57
test('if scheme not found in matched route', function (): void {
58
    $collection = new RouteCollection();
59
    $collection->add('/hello', ['GET'])->scheme('ftp');
60
61
    $router = Router::withCollection($collection);
62
    $router->match('GET', new Uri('http://localhost/hello'));
63
})->throws(
64
    UriHandlerException::class,
65
    'Route with "/hello" path requires request scheme(s) [ftp], "http" is invalid.'
66
);
67
68
test('if host not found in matched route', function (): void {
69
    $collection = new RouteCollection();
70
    $collection->add('/hello', ['GET'])->domain('mydomain.com');
71
72
    $router = Router::withCollection($collection);
73
    t\assertNull($router->match('GET', new Uri('//localhost/hello')));
0 ignored issues
show
The function assertNull 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 ignore-call  annotation

73
    /** @scrutinizer ignore-call */ 
74
    t\assertNull($router->match('GET', new Uri('//localhost/hello')));
Loading history...
74
});
75
76
test('if route can match a static host', function (): void {
77
    $collection = new RouteCollection();
78
    $collection->add('/world', ['GET'])->domain('hello.com');
79
80
    $router = Router::withCollection($collection);
81
    $route = $router->match('GET', new Uri('//hello.com/world'));
82
    t\assertSame('hello.com', \array_key_first($route['hosts']));
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 ignore-call  annotation

82
    /** @scrutinizer ignore-call */ 
83
    t\assertSame('hello.com', \array_key_first($route['hosts']));
Loading history...
83
    t\assertCount(0, $route['arguments'] ?? []);
0 ignored issues
show
The function assertCount 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 ignore-call  annotation

83
    /** @scrutinizer ignore-call */ 
84
    t\assertCount(0, $route['arguments'] ?? []);
Loading history...
84
});
85
86
test('if route can match a dynamic host', function (): void {
87
    $collection = new RouteCollection();
88
    $collection->add('/world', ['GET'])->domain('hello.{tld}');
89
90
    $router = Router::withCollection($collection);
91
    $route = $router->match('GET', new Uri('//hello.ghana/world'));
92
    t\assertSame('hello.{tld}', \array_key_first($route['hosts']));
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 ignore-call  annotation

92
    /** @scrutinizer ignore-call */ 
93
    t\assertSame('hello.{tld}', \array_key_first($route['hosts']));
Loading history...
93
    t\assertSame(['tld' => 'ghana'], $route['arguments'] ?? []);
94
});
95
96
test('if route cannot be found by name to generate a reversed uri', function (): void {
97
    $router = Router::withCollection();
98
    $router->generateUri('hello');
99
})->throws(UrlGenerationException::class, 'Route "hello" does not exist.');
100
101
test('if route handler can be intercepted by middlewares', function (): void {
102
    $collection = new RouteCollection();
103
    $collection->add('/{name}', ['GET'], new CallbackHandler(fn (ServerRequestInterface $req): string => $req->getAttribute('hello')))->piped('guard');
104
105
    $router = Router::withCollection($collection);
106
    $router->pipe(middleware(function (ServerRequestInterface $req, RequestHandlerInterface $handler) {
107
        t\assertIsArray($route = $req->getAttribute(Router::class));
0 ignored issues
show
The function assertIsArray 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 ignore-call  annotation

107
        /** @scrutinizer ignore-call */ 
108
        t\assertIsArray($route = $req->getAttribute(Router::class));
Loading history...
108
        t\assertNotEmpty($route);
0 ignored issues
show
The function assertNotEmpty 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 ignore-call  annotation

108
        /** @scrutinizer ignore-call */ 
109
        t\assertNotEmpty($route);
Loading history...
109
        t\assertArrayHasKey('name', $hello = $route['arguments'] ?? []);
0 ignored issues
show
The function assertArrayHasKey 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 ignore-call  annotation

109
        /** @scrutinizer ignore-call */ 
110
        t\assertArrayHasKey('name', $hello = $route['arguments'] ?? []);
Loading history...
110
111
        return $handler->handle($req->withAttribute('hello', 'Hello '.$hello['name']));
112
    }));
113
    $router->pipes('guard', middleware(function (ServerRequestInterface $req, RequestHandlerInterface $handler) {
114
        $route = $req->getAttribute(Router::class);
115
116
        if ('divine' !== $route['arguments']['name']) {
117
            throw new \RuntimeException('Expected name to be "divine".');
118
        }
119
120
        return $handler->handle($req);
121
    }));
122
123
    $response = $router->process(new ServerRequest(Router::METHOD_GET, '/hello'), $h = new RouteHandler(new Psr17Factory()));
124
    t\assertSame('Hello divine', (string) $response->getBody());
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 ignore-call  annotation

124
    /** @scrutinizer ignore-call */ 
125
    t\assertSame('Hello divine', (string) $response->getBody());
Loading history...
125
    $router->process(new ServerRequest(Router::METHOD_GET, '/frank'), $h);
126
})->throws(\RuntimeException::class, 'Expected name to be "divine".');
127
128
test('if route cannot be found', function (): void {
129
    $handler = new RouteHandler(new Psr17Factory());
130
    $handler->handle(new ServerRequest(Router::METHOD_GET, '/hello'));
131
})->throws(
132
    RouteNotFoundException::class,
133
    'Unable to find the controller for path "/hello". The route is wrongly configured.'
134
);
135
136
test('if route not found exception can be overridden', function (): void {
137
    $handler = new RouteHandler($f = new Psr17Factory());
138
    $router = new Router();
139
    $router->pipe(middleware(
140
        function (ServerRequestInterface $req, RequestHandlerInterface $handler) use ($f) {
141
            if (null === $req->getAttribute(Router::class)) {
142
                t\assertInstanceOf(Next::class, $handler);
0 ignored issues
show
The function assertInstanceOf 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 ignore-call  annotation

142
                /** @scrutinizer ignore-call */ 
143
                t\assertInstanceOf(Next::class, $handler);
Loading history...
143
                $response = $f->createResponse('OPTIONS' === $req->getMethod() ? 200 : 204);
144
145
                if (false === $h = $req->getAttribute('override')) {
146
                    return $response;
147
                } // This will break the middleware chain.
148
                $req = $req->withAttribute(RouteHandler::OVERRIDE_NULL_ROUTE, $h ?? $response);
149
            }
150
151
            return $handler->handle($req);
152
        }
153
    ));
154
155
    $req = new ServerRequest(Router::METHOD_GET, '/hello');
156
    $res1 = $router->process($req->withAttribute('override', false), $handler);
157
    $res2 = $router->process($req->withAttribute('override', false)->withMethod('OPTIONS'), $handler);
158
    $res3 = $router->process($req->withAttribute('override', true), $handler);
159
    t\assertSame([204, 200, 200], [$res1->getStatusCode(), $res2->getStatusCode(), $res3->getStatusCode()]);
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 ignore-call  annotation

159
    /** @scrutinizer ignore-call */ 
160
    t\assertSame([204, 200, 200], [$res1->getStatusCode(), $res2->getStatusCode(), $res3->getStatusCode()]);
Loading history...
160
});
161
162
test('if router export method can work with closures, __set_state and resource handler', function (): void {
163
    t\assertSame(
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 ignore-call  annotation

163
    /** @scrutinizer ignore-call */ 
164
    t\assertSame(
Loading history...
164
        "[[], unserialize('O:11:\"ArrayObject\":4:{i:0;i:0;i:1;a:3:{i:0;i:1;i:1;i:2;i:2;i:3;}i:2;a:0:{}i:3;N;}'), ".
165
        "Flight\Routing\Handlers\ResourceHandler(['ShopHandler', 'User']), ".
166
        "Flight\Routing\Tests\Fixtures\BlankRequestHandler::__set_state(['isDone' => false, 'attributes' => ['a' => [1, 2, 3]]]]",
167
        Router::export([[], new \ArrayObject([1, 2, 3]), new ResourceHandler('ShopHandler', 'user'), new BlankRequestHandler(['a' => [1, 2, 3]])])
168
    );
169
170
    Router::export(\Closure::bind(fn () => 'Failed', null));
171
    $this->fail('Expected an exception to be thrown as closure cannot be serialized');
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $this seems to be never defined.
Loading history...
172
})->throws(\Exception::class, "Serialization of 'Closure' is not allowed");
173
174
test('if router can be resolvable', function (int $cache): void {
175
    $file = __DIR__."/../tests/Fixtures/cached/{$cache}/compiled.php";
176
    $collection = static function (RouteCollection $mergedCollection): void {
177
        $demoCollection = new RouteCollection();
178
        $demoCollection->add('/admin/post/', [Router::METHOD_POST]);
179
        $demoCollection->add('/admin/post/new', [Router::METHOD_POST]);
180
        $demoCollection->add('/admin/post/{id}', [Router::METHOD_POST])->placeholder('id', '\d+');
181
        $demoCollection->add('/admin/post/{id}/edit', [Router::METHOD_PATCH])->placeholder('id', '\d+');
182
        $demoCollection->add('/admin/post/{id}/delete', [Router::METHOD_DELETE])->placeholder('id', '\d+');
183
        $demoCollection->add('/blog/', [Router::METHOD_GET]);
184
        $demoCollection->add('/blog/rss.xml', [Router::METHOD_GET]);
185
        $demoCollection->add('/blog/page/{page}', [Router::METHOD_GET])->placeholder('id', '\d+');
186
        $demoCollection->add('/blog/posts/{page}', [Router::METHOD_GET])->placeholder('id', '\d+');
187
        $demoCollection->add('/blog/comments/{id}/new', [Router::METHOD_GET])->placeholder('id', '\d+');
188
        $demoCollection->add('/blog/search', [Router::METHOD_GET]);
189
        $demoCollection->add('/login', [Router::METHOD_POST]);
190
        $demoCollection->add('/logout', [Router::METHOD_POST]);
191
        $demoCollection->add('/', [Router::METHOD_GET]);
192
        $demoCollection->prototype(true)->prefix('/{_locale}/');
193
        $demoCollection->method(Router::METHOD_CONNECT);
194
        $mergedCollection->group('demo.', $demoCollection)->default('_locale', 'en')->placeholder('_locale', 'en|fr');
195
196
        $chunkedCollection = new RouteCollection();
197
        $chunkedCollection->domain('http://localhost')->scheme('https', 'http');
198
199
        for ($i = 0; $i < 100; ++$i) {
200
            $chunkedCollection->get('/chuck'.$i.'/{a}/{b}/{c}/')->bind('_'.$i);
201
        }
202
        $mergedCollection->group('chuck_', $chunkedCollection);
203
204
        $groupOptimisedCollection = new RouteCollection();
205
        $groupOptimisedCollection->add('/a/11', [Router::METHOD_GET])->bind('a_first');
206
        $groupOptimisedCollection->add('/a/22', [Router::METHOD_GET])->bind('a_second');
207
        $groupOptimisedCollection->add('/a/333', [Router::METHOD_GET])->bind('a_third');
208
        $groupOptimisedCollection->add('/a/333/', [Router::METHOD_POST], (object) [2, 4])->bind('a_third_1');
209
        // $groupOptimisedCollection->add('/{param}', [Router::METHOD_GET])->bind('a_wildcard');
210
        $groupOptimisedCollection->add('/a/44/', [Router::METHOD_GET])->bind('a_fourth');
211
        $groupOptimisedCollection->add('/a/55/', [Router::METHOD_GET])->bind('a_fifth');
212
        $groupOptimisedCollection->add('/nested/{param}', [Router::METHOD_GET])->bind('nested_wildcard');
213
        $groupOptimisedCollection->add('/nested/group/a/', [Router::METHOD_GET])->bind('nested_a');
214
        $groupOptimisedCollection->add('/nested/group/b/', [Router::METHOD_GET])->bind('nested_b');
215
        $groupOptimisedCollection->add('/nested/group/c/', [Router::METHOD_GET])->bind('nested_c');
216
        $groupOptimisedCollection->add('/a/66/', [Router::METHOD_GET], 'phpinfo');
217
218
        $groupOptimisedCollection->add('/slashed/group/', [Router::METHOD_GET])->bind('slashed_a');
219
        $groupOptimisedCollection->add('/slashed/group/b/', [Router::METHOD_GET])->bind('slashed_b');
220
        $groupOptimisedCollection->add('/slashed/group/c/', [Router::METHOD_GET])->bind('slashed_c');
221
222
        $mergedCollection->group('', $groupOptimisedCollection);
223
        $mergedCollection->sort();
224
    };
225
226
    if ($cache <= 1) {
227
        if (\file_exists($dir = __DIR__.'/../tests/Fixtures/cached/') && 0 === $cache) {
228
            foreach ([
229
                $dir.'1/compiled.php',
230
                $dir.'3/compiled.php',
231
                $dir.'1',
232
                $dir.'3',
233
                $dir,
234
            ] as $cached) {
235
                \is_dir($cached) ? @\rmdir($cached) : @\unlink($cached);
236
            }
237
        }
238
        $router = new Router(cache: 1 === $cache ? $file : null);
239
        $router->setCollection($collection);
240
    } else {
241
        $collection($collection = new RouteCollection());
242
        $router = Router::withCollection($collection);
243
        $router->setCompiler(new RouteCompiler());
244
245
        if (3 === $cache) {
246
            $router->setCache($file);
247
        }
248
    }
249
250
    try {
251
        $router->match(Router::METHOD_DELETE, new Uri('/a/22'));
252
    } catch (MethodNotAllowedException $e) {
253
        t\assertSame('Route with "/a/22" path requires request method(s) [GET], "DELETE" is invalid.', $e->getMessage());
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 ignore-call  annotation

253
        /** @scrutinizer ignore-call */ 
254
        t\assertSame('Route with "/a/22" path requires request method(s) [GET], "DELETE" is invalid.', $e->getMessage());
Loading history...
254
    }
255
256
    $route1 = $router->match(Router::METHOD_GET, new Uri('/fr/blog'));
257
    $route2 = $router->matchRequest(new ServerRequest(Router::METHOD_GET, 'http://localhost/chuck12/hello/1/2'));
258
    $route3 = $router->matchRequest(new ServerRequest(Router::METHOD_GET, '/a/333'));
259
    $route4 = $router->matchRequest(new ServerRequest(Router::METHOD_POST, '/a/333'));
260
    $genRoute = $router->generateUri('chuck__12', ['a', 'b', 'c'])->withQuery(['h', 'a' => 'b']);
261
262
    t\assertCount(128, $router->getCollection());
0 ignored issues
show
The function assertCount 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 ignore-call  annotation

262
    /** @scrutinizer ignore-call */ 
263
    t\assertCount(128, $router->getCollection());
Loading history...
263
    t\assertSame($router->match('GET', new Uri('/a/66/')), $router->match('GET', new Uri('/a/66')));
264
    t\assertSame('/chuck12/a/b/c/?0=h&a=b#yes', (string) $genRoute->withFragment('yes'));
265
    t\assertSame('//example.com:8080/a/11', (string) $router->generateUri('a_first', [], GeneratedUri::ABSOLUTE_URL)->withPort(8080));
266
    t\assertNull($router->match('GET', new Uri('/None')));
0 ignored issues
show
The function assertNull 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 ignore-call  annotation

266
    /** @scrutinizer ignore-call */ 
267
    t\assertNull($router->match('GET', new Uri('/None')));
Loading history...
267
    t\assertEquals(<<<'EOT'
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 ignore-call  annotation

267
    /** @scrutinizer ignore-call */ 
268
    t\assertEquals(<<<'EOT'
Loading history...
268
    [
269
        [
270
            'handler' => null,
271
            'prefix' => null,
272
            'path' => '/{_locale}/blog/',
273
            'methods' => [
274
                'GET' => true,
275
                'CONNECT' => true,
276
            ],
277
            'defaults' => [
278
                '_locale' => 'en',
279
            ],
280
            'placeholders' => [
281
                '_locale' => 'en|fr',
282
            ],
283
            'name' => 'demo.GET_CONNECT_locale_blog_',
284
            'arguments' => [
285
                '_locale' => 'fr',
286
            ],
287
        ],
288
        [
289
            'handler' => null,
290
            'prefix' => '/chuck12',
291
            'path' => '/chuck12/{a}/{b}/{c}/',
292
            'schemes' => [
293
                'https' => true,
294
                'http' => true,
295
            ],
296
            'hosts' => [
297
                'localhost' => true,
298
            ],
299
            'methods' => [
300
                'GET' => true,
301
                'HEAD' => true,
302
            ],
303
            'name' => 'chuck__12',
304
            'arguments' => [
305
                'a' => 'hello',
306
                'b' => '1',
307
                'c' => '2',
308
            ],
309
        ],
310
        [
311
            'handler' => null,
312
            'prefix' => '/a/333',
313
            'path' => '/a/333',
314
            'methods' => [
315
                'GET' => true,
316
            ],
317
            'name' => 'a_third',
318
        ],
319
        [
320
            'handler' => (object) [
321
                2,
322
                4,
323
            ],
324
            'prefix' => '/a/333',
325
            'path' => '/a/333/',
326
            'methods' => [
327
                'POST' => true,
328
            ],
329
            'name' => 'a_third_1',
330
        ],
331
    ]
332
    EOT, debugFormat([$route1, $route2, $route3, $route4]));
333
})->with([0, 1, 1, 2, 3, 3]);
334