1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace GacelaTest\Feature\Router; |
6
|
|
|
|
7
|
|
|
use Exception; |
8
|
|
|
use Gacela\Router\Entities\Request; |
9
|
|
|
use Gacela\Router\Exceptions\NotFound404Exception; |
10
|
|
|
use Gacela\Router\Exceptions\UnsupportedParamTypeException; |
11
|
|
|
use Gacela\Router\Exceptions\UnsupportedResponseTypeException; |
12
|
|
|
use Gacela\Router\Handlers; |
13
|
|
|
use Gacela\Router\Router; |
14
|
|
|
use Gacela\Router\Routes; |
15
|
|
|
use GacelaTest\Feature\HeaderTestCase; |
16
|
|
|
use GacelaTest\Feature\Router\Fixtures\FakeController; |
17
|
|
|
use GacelaTest\Feature\Router\Fixtures\FakeControllerWithUnhandledException; |
18
|
|
|
use GacelaTest\Feature\Router\Fixtures\UnhandledException; |
19
|
|
|
use Generator; |
20
|
|
|
use stdClass; |
21
|
|
|
|
22
|
|
|
final class ErrorHandlingTest extends HeaderTestCase |
23
|
|
|
{ |
24
|
|
|
public function test_respond_404_status_when_uri_does_not_match(): void |
25
|
|
|
{ |
26
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/optional/uri'; |
27
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_OPTIONS; |
28
|
|
|
|
29
|
|
|
$router = new Router(static function (): void { |
30
|
|
|
}); |
31
|
|
|
$router->run(); |
32
|
|
|
|
33
|
|
|
self::assertSame([ |
34
|
|
|
[ |
35
|
|
|
'header' => 'HTTP/1.0 404 Not Found', |
36
|
|
|
'replace' => true, |
37
|
|
|
'response_code' => 0, |
38
|
|
|
], |
39
|
|
|
], $this->headers()); |
40
|
|
|
} |
41
|
|
|
|
42
|
|
|
public function test_respond_404_status_when_method_does_not_match(): void |
43
|
|
|
{ |
44
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
45
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
46
|
|
|
|
47
|
|
|
$router = new Router(static function (Routes $routes): void { |
48
|
|
|
$routes->post('expected/uri', FakeController::class, 'basicAction'); |
49
|
|
|
}); |
50
|
|
|
$router->run(); |
51
|
|
|
|
52
|
|
|
self::assertSame([ |
53
|
|
|
[ |
54
|
|
|
'header' => 'HTTP/1.0 404 Not Found', |
55
|
|
|
'replace' => true, |
56
|
|
|
'response_code' => 0, |
57
|
|
|
], |
58
|
|
|
], $this->headers()); |
59
|
|
|
} |
60
|
|
|
|
61
|
|
|
/** |
62
|
|
|
* @dataProvider notMatchesMethodsProvider |
63
|
|
|
*/ |
64
|
|
|
public function test_respond_404_status_when_not_matches_match_methods(string $testMethod, array $givenMethods): void |
65
|
|
|
{ |
66
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
67
|
|
|
$_SERVER['REQUEST_METHOD'] = $testMethod; |
68
|
|
|
|
69
|
|
|
$router = new Router(static function (Routes $routes) use ($givenMethods): void { |
70
|
|
|
$routes->match($givenMethods, 'expected/uri', FakeController::class, 'basicAction'); |
71
|
|
|
}); |
72
|
|
|
$router->run(); |
73
|
|
|
|
74
|
|
|
self::assertSame([ |
75
|
|
|
[ |
76
|
|
|
'header' => 'HTTP/1.0 404 Not Found', |
77
|
|
|
'replace' => true, |
78
|
|
|
'response_code' => 0, |
79
|
|
|
], |
80
|
|
|
], $this->headers()); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
public function notMatchesMethodsProvider(): Generator |
84
|
|
|
{ |
85
|
|
|
yield [Request::METHOD_PUT, [Request::METHOD_GET, Request::METHOD_POST]]; |
86
|
|
|
yield [Request::METHOD_OPTIONS, [Request::METHOD_GET, Request::METHOD_POST]]; |
87
|
|
|
yield [Request::METHOD_GET, [Request::METHOD_PATCH, Request::METHOD_PUT, Request::METHOD_DELETE, Request::METHOD_POST]]; |
88
|
|
|
yield [Request::METHOD_CONNECT, [ |
89
|
|
|
Request::METHOD_GET, Request::METHOD_DELETE, Request::METHOD_HEAD, Request::METHOD_OPTIONS, |
90
|
|
|
Request::METHOD_PATCH, Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_TRACE, |
91
|
|
|
]]; |
92
|
|
|
} |
93
|
|
|
|
94
|
|
|
public function test_respond_500_status_when_unhandled_exception(): void |
95
|
|
|
{ |
96
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
97
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
98
|
|
|
|
99
|
|
|
$router = new Router(static function (Routes $routes): void { |
100
|
|
|
$routes->get('expected/uri', FakeControllerWithUnhandledException::class); |
101
|
|
|
}); |
102
|
|
|
$router->run(); |
103
|
|
|
|
104
|
|
|
self::assertSame([ |
105
|
|
|
[ |
106
|
|
|
'header' => 'HTTP/1.1 500 Internal Server Error', |
107
|
|
|
'replace' => true, |
108
|
|
|
'response_code' => 0, |
109
|
|
|
], |
110
|
|
|
], $this->headers()); |
111
|
|
|
} |
112
|
|
|
|
113
|
|
|
public function test_handle_handled_exception_with_anonymous_function(): void |
114
|
|
|
{ |
115
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
116
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
117
|
|
|
|
118
|
|
|
$router = new Router(static function (Routes $routes, Handlers $handlers): void { |
119
|
|
|
$routes->get('expected/uri', FakeControllerWithUnhandledException::class); |
120
|
|
|
|
121
|
|
|
$handlers->handle(UnhandledException::class, static function (): string { |
122
|
|
|
\Gacela\Router\header('HTTP/1.1 418 I\'m a teapot'); |
123
|
|
|
return 'Handled!'; |
124
|
|
|
}); |
125
|
|
|
}); |
126
|
|
|
$router->run(); |
127
|
|
|
|
128
|
|
|
$this->expectOutputString('Handled!'); |
129
|
|
|
self::assertSame([ |
130
|
|
|
[ |
131
|
|
|
'header' => 'HTTP/1.1 418 I\'m a teapot', |
132
|
|
|
'replace' => true, |
133
|
|
|
'response_code' => 0, |
134
|
|
|
], |
135
|
|
|
], $this->headers()); |
136
|
|
|
} |
137
|
|
|
|
138
|
|
|
public function test_custom_404_handler(): void |
139
|
|
|
{ |
140
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
141
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
142
|
|
|
|
143
|
|
|
$router = new Router(static function (Handlers $handlers): void { |
144
|
|
|
$handlers->handle(NotFound404Exception::class, static function (NotFound404Exception $exception): string { |
145
|
|
|
\Gacela\Router\header('HTTP/1.1 418 I\'m a teapot'); |
146
|
|
|
return "'{$exception->getMessage()}' Handled!"; |
147
|
|
|
}); |
148
|
|
|
}); |
149
|
|
|
$router->run(); |
150
|
|
|
|
151
|
|
|
$this->expectOutputString("'Error 404 - Not Found' Handled!"); |
152
|
|
|
self::assertSame([ |
153
|
|
|
[ |
154
|
|
|
'header' => 'HTTP/1.1 418 I\'m a teapot', |
155
|
|
|
'replace' => true, |
156
|
|
|
'response_code' => 0, |
157
|
|
|
], |
158
|
|
|
], $this->headers()); |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
public function test_custom_fallback_handler(): void |
162
|
|
|
{ |
163
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
164
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
165
|
|
|
|
166
|
|
|
$router = new Router(static function (Handlers $handlers, Routes $routes): void { |
167
|
|
|
$routes->get('expected/uri', FakeControllerWithUnhandledException::class); |
168
|
|
|
|
169
|
|
|
$handlers->handle(Exception::class, static function (): string { |
170
|
|
|
\Gacela\Router\header('HTTP/1.1 418 I\'m a teapot'); |
171
|
|
|
return 'Handled!'; |
172
|
|
|
}); |
173
|
|
|
}); |
174
|
|
|
$router->run(); |
175
|
|
|
|
176
|
|
|
$this->expectOutputString('Handled!'); |
177
|
|
|
self::assertSame([ |
178
|
|
|
[ |
179
|
|
|
'header' => 'HTTP/1.1 418 I\'m a teapot', |
180
|
|
|
'replace' => true, |
181
|
|
|
'response_code' => 0, |
182
|
|
|
], |
183
|
|
|
], $this->headers()); |
184
|
|
|
} |
185
|
|
|
|
186
|
|
|
public function test_handle_handled_exception_with_anonymous_class(): void |
187
|
|
|
{ |
188
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
189
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
190
|
|
|
|
191
|
|
|
$router = new Router(static function (Handlers $handlers, Routes $routes): void { |
192
|
|
|
$routes->get('expected/uri', FakeControllerWithUnhandledException::class); |
193
|
|
|
|
194
|
|
|
$handlers->handle(UnhandledException::class, new class() { |
195
|
|
|
public function __invoke(): string |
196
|
|
|
{ |
197
|
|
|
\Gacela\Router\header('HTTP/1.1 418 I\'m a teapot'); |
198
|
|
|
return 'Handled!'; |
199
|
|
|
} |
200
|
|
|
}); |
201
|
|
|
}); |
202
|
|
|
$router->run(); |
203
|
|
|
|
204
|
|
|
$this->expectOutputString('Handled!'); |
205
|
|
|
self::assertSame([ |
206
|
|
|
[ |
207
|
|
|
'header' => 'HTTP/1.1 418 I\'m a teapot', |
208
|
|
|
'replace' => true, |
209
|
|
|
'response_code' => 0, |
210
|
|
|
], |
211
|
|
|
], $this->headers()); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* @dataProvider nonStringProvider |
216
|
|
|
* |
217
|
|
|
* @param mixed $given |
218
|
|
|
* @param mixed $type |
219
|
|
|
*/ |
220
|
|
|
public function test_throws_exception_if_response_is_not_a_string_or_stringable($given, $type): void |
221
|
|
|
{ |
222
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
223
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
224
|
|
|
|
225
|
|
|
$router = new Router(static function (Handlers $handlers, Routes $routes) use ($given): void { |
226
|
|
|
$routes->get('expected/uri', static fn () => $given); |
227
|
|
|
|
228
|
|
|
$handlers->handle( |
229
|
|
|
UnsupportedResponseTypeException::class, |
230
|
|
|
static fn (UnsupportedResponseTypeException $exception): string => $exception->getMessage(), |
231
|
|
|
); |
232
|
|
|
}); |
233
|
|
|
$router->run(); |
234
|
|
|
|
235
|
|
|
$this->expectOutputString("Unsupported response type '{$type}'. Must be a string or implement Stringable interface."); |
236
|
|
|
} |
237
|
|
|
|
238
|
|
|
public function nonStringProvider(): Generator |
239
|
|
|
{ |
240
|
|
|
yield [42, 'integer']; |
241
|
|
|
yield [false, 'boolean']; |
242
|
|
|
yield [[], 'array']; |
243
|
|
|
yield [new stdClass(), 'stdClass']; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
public function test_throws_exception_when_param_has_no_type(): void |
247
|
|
|
{ |
248
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/param/is/any'; |
249
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
250
|
|
|
|
251
|
|
|
$router = new Router(static function (Routes $routes, Handlers $handlers): void { |
252
|
|
|
$routes->get('expected/param/is/{param}', FakeController::class, 'nonTypedParam'); |
253
|
|
|
|
254
|
|
|
$handlers->handle( |
255
|
|
|
UnsupportedParamTypeException::class, |
256
|
|
|
static fn (UnsupportedParamTypeException $exception): string => $exception->getMessage(), |
257
|
|
|
); |
258
|
|
|
}); |
259
|
|
|
$router->run(); |
260
|
|
|
|
261
|
|
|
$this->expectOutputString('Unsupported non-typed param. Must be a scalar.'); |
262
|
|
|
} |
263
|
|
|
|
264
|
|
|
public function test_throws_exception_when_param_is_no_scalar(): void |
265
|
|
|
{ |
266
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/param/is/array'; |
267
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
268
|
|
|
|
269
|
|
|
$router = new Router(static function (Routes $routes, Handlers $handlers): void { |
270
|
|
|
$routes->get('expected/param/is/{param}', FakeController::class, 'nonScalarParam'); |
271
|
|
|
|
272
|
|
|
$handlers->handle( |
273
|
|
|
UnsupportedParamTypeException::class, |
274
|
|
|
static fn (UnsupportedParamTypeException $exception): string => $exception->getMessage(), |
275
|
|
|
); |
276
|
|
|
}); |
277
|
|
|
$router->run(); |
278
|
|
|
|
279
|
|
|
$this->expectOutputString("Unsupported param type 'array'. Must be a scalar."); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
public function test_configure_throws_unsupported_closure_param(): void |
283
|
|
|
{ |
284
|
|
|
$this->expectExceptionMessage("'unrecognised' parameter in configuration Closure for Router must be from types Routes, Bindings or Handlers."); |
285
|
|
|
|
286
|
|
|
new Router(static function ($unrecognised): void {}); |
|
|
|
|
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
public function test_configure_non_callable_handler(): void |
290
|
|
|
{ |
291
|
|
|
$_SERVER['REQUEST_URI'] = 'https://example.org/expected/uri'; |
292
|
|
|
$_SERVER['REQUEST_METHOD'] = Request::METHOD_GET; |
293
|
|
|
|
294
|
|
|
$this->expectExceptionMessage('Handler assigned to \'GacelaTest\Feature\Router\Fixtures\UnhandledException\' exception cannot be called.'); |
295
|
|
|
|
296
|
|
|
$router = new Router(static function (Handlers $handlers, Routes $routes): void { |
297
|
|
|
$routes->get('expected/uri', FakeControllerWithUnhandledException::class); |
298
|
|
|
|
299
|
|
|
$handlers->handle(UnhandledException::class, 'non-callable'); |
300
|
|
|
}); |
301
|
|
|
$router->run(); |
302
|
|
|
} |
303
|
|
|
} |
304
|
|
|
|
This check looks for parameters that have been defined for a function or method, but which are not used in the method body.