Skip to content

Commit e7eca82

Browse files
committed
Add presenter fixtures and application error handling simulation test
- Add Front:Homepage and Front:Article presenters that throw errors - Add Admin:Dashboard and Admin:User presenters that throw errors - Add ApplicationErrorHandlingTest that simulates real application run: - Presenters throw RuntimeException/BadRequestException - ModuleErrorPresenterLocator routes to correct error presenter - Validates error presenter response contains expected error message - Tests full error handling flow with request parameters preserved
1 parent be8fc85 commit e7eca82

5 files changed

Lines changed: 423 additions & 0 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Cases\ErrorPresenter;
4+
5+
use Contributte\Application\ErrorPresenter\ModuleErrorPresenterLocator;
6+
use Nette\Application\IPresenter;
7+
use Nette\Application\IPresenterFactory;
8+
use Nette\Application\Request;
9+
use Nette\Application\Response;
10+
use Nette\Application\Responses\TextResponse;
11+
use Tester\Assert;
12+
use Tester\TestCase;
13+
use Tests\Fixtures\Presenters\Admin\DashboardPresenter as AdminDashboardPresenter;
14+
use Tests\Fixtures\Presenters\Admin\ErrorPresenter as AdminErrorPresenter;
15+
use Tests\Fixtures\Presenters\Admin\UserPresenter as AdminUserPresenter;
16+
use Tests\Fixtures\Presenters\DefaultErrorPresenter;
17+
use Tests\Fixtures\Presenters\Front\ArticlePresenter as FrontArticlePresenter;
18+
use Tests\Fixtures\Presenters\Front\ErrorPresenter as FrontErrorPresenter;
19+
use Tests\Fixtures\Presenters\Front\HomepagePresenter as FrontHomepagePresenter;
20+
21+
require_once __DIR__ . '/../../bootstrap.php';
22+
23+
/**
24+
* Test simulating real application error handling flow.
25+
* Presenters throw exceptions, and errors are routed to module-specific error presenters.
26+
*
27+
* @testCase
28+
*/
29+
final class ApplicationErrorHandlingTest extends TestCase
30+
{
31+
32+
private ModuleErrorPresenterLocator $errorLocator;
33+
34+
/** @var array<string, class-string<IPresenter>> */
35+
private array $presenterMap;
36+
37+
protected function setUp(): void
38+
{
39+
// Configure error presenter routing
40+
$this->errorLocator = new ModuleErrorPresenterLocator([
41+
'Front:*' => FrontErrorPresenter::PRESENTER_NAME,
42+
'Admin:*' => AdminErrorPresenter::PRESENTER_NAME,
43+
], DefaultErrorPresenter::PRESENTER_NAME);
44+
45+
// Map presenter names to classes (simulating PresenterFactory)
46+
$this->presenterMap = [
47+
'Front:Homepage' => FrontHomepagePresenter::class,
48+
'Front:Article' => FrontArticlePresenter::class,
49+
'Admin:Dashboard' => AdminDashboardPresenter::class,
50+
'Admin:User' => AdminUserPresenter::class,
51+
'Front:Error' => FrontErrorPresenter::class,
52+
'Admin:Error' => AdminErrorPresenter::class,
53+
'Error' => DefaultErrorPresenter::class,
54+
];
55+
}
56+
57+
/**
58+
* Simulates application run: execute presenter, catch error, route to error presenter.
59+
*/
60+
private function simulateApplicationRun(Request $request): Response
61+
{
62+
$presenterName = $request->getPresenterName();
63+
$presenter = $this->createPresenter($presenterName);
64+
65+
try {
66+
// Try to run the presenter (will throw exception)
67+
return $presenter->run($request);
68+
} catch (\Throwable $e) {
69+
// Error occurred - locate the appropriate error presenter
70+
$errorPresenterName = $this->errorLocator->locate($request);
71+
72+
if ($errorPresenterName === null) {
73+
throw $e; // No error presenter configured
74+
}
75+
76+
// Create and run the error presenter
77+
$errorPresenter = $this->createPresenter($errorPresenterName);
78+
$errorRequest = new Request(
79+
$errorPresenterName,
80+
Request::FORWARD,
81+
[
82+
'exception' => $e,
83+
'request' => $request,
84+
]
85+
);
86+
87+
return $errorPresenter->run($errorRequest);
88+
}
89+
}
90+
91+
private function createPresenter(string $name): IPresenter
92+
{
93+
if (!isset($this->presenterMap[$name])) {
94+
throw new \InvalidArgumentException("Presenter '$name' not found in map");
95+
}
96+
97+
$class = $this->presenterMap[$name];
98+
99+
return new $class();
100+
}
101+
102+
/**
103+
* Test: Front:Homepage throws error -> Front:Error handles it
104+
*/
105+
public function testFrontHomepageErrorRoutesToFrontError(): void
106+
{
107+
$request = new Request('Front:Homepage', 'GET', ['action' => 'default']);
108+
109+
$response = $this->simulateApplicationRun($request);
110+
111+
Assert::type(TextResponse::class, $response);
112+
Assert::contains('Front Error:', (string) $response->getSource());
113+
Assert::contains('Homepage error occurred', (string) $response->getSource());
114+
}
115+
116+
/**
117+
* Test: Front:Article throws error -> Front:Error handles it
118+
*/
119+
public function testFrontArticleErrorRoutesToFrontError(): void
120+
{
121+
$request = new Request('Front:Article', 'GET', ['action' => 'detail', 'id' => 123]);
122+
123+
$response = $this->simulateApplicationRun($request);
124+
125+
Assert::type(TextResponse::class, $response);
126+
Assert::contains('Front Error:', (string) $response->getSource());
127+
Assert::contains('Article detail error', (string) $response->getSource());
128+
}
129+
130+
/**
131+
* Test: Front:Article with invalid ID throws BadRequestException -> Front:Error handles it
132+
*/
133+
public function testFrontArticleNotFoundRoutesToFrontError(): void
134+
{
135+
$request = new Request('Front:Article', 'GET', ['action' => 'detail', 'id' => -1]);
136+
137+
$response = $this->simulateApplicationRun($request);
138+
139+
Assert::type(TextResponse::class, $response);
140+
Assert::contains('Front Error:', (string) $response->getSource());
141+
Assert::contains('Article not found', (string) $response->getSource());
142+
}
143+
144+
/**
145+
* Test: Admin:Dashboard throws error -> Admin:Error handles it
146+
*/
147+
public function testAdminDashboardErrorRoutesToAdminError(): void
148+
{
149+
$request = new Request('Admin:Dashboard', 'GET', ['action' => 'default']);
150+
151+
$response = $this->simulateApplicationRun($request);
152+
153+
Assert::type(TextResponse::class, $response);
154+
Assert::contains('Admin Error:', (string) $response->getSource());
155+
Assert::contains('Admin dashboard error occurred', (string) $response->getSource());
156+
}
157+
158+
/**
159+
* Test: Admin:User:list throws error -> Admin:Error handles it
160+
*/
161+
public function testAdminUserListErrorRoutesToAdminError(): void
162+
{
163+
$request = new Request('Admin:User', 'GET', ['action' => 'list']);
164+
165+
$response = $this->simulateApplicationRun($request);
166+
167+
Assert::type(TextResponse::class, $response);
168+
Assert::contains('Admin Error:', (string) $response->getSource());
169+
Assert::contains('User list error', (string) $response->getSource());
170+
}
171+
172+
/**
173+
* Test: Admin:User:edit throws BadRequestException -> Admin:Error handles it
174+
*/
175+
public function testAdminUserNotFoundRoutesToAdminError(): void
176+
{
177+
$request = new Request('Admin:User', 'GET', ['action' => 'edit', 'id' => 0]);
178+
179+
$response = $this->simulateApplicationRun($request);
180+
181+
Assert::type(TextResponse::class, $response);
182+
Assert::contains('Admin Error:', (string) $response->getSource());
183+
Assert::contains('User not found', (string) $response->getSource());
184+
}
185+
186+
/**
187+
* Test: Verify Front and Admin errors go to different error presenters
188+
*/
189+
public function testDifferentModulesRouteToDifferentErrorPresenters(): void
190+
{
191+
// Front module error
192+
$frontRequest = new Request('Front:Homepage', 'GET', ['action' => 'default']);
193+
$frontResponse = $this->simulateApplicationRun($frontRequest);
194+
195+
// Admin module error
196+
$adminRequest = new Request('Admin:Dashboard', 'GET', ['action' => 'default']);
197+
$adminResponse = $this->simulateApplicationRun($adminRequest);
198+
199+
// Verify different error presenters handled the errors
200+
Assert::contains('Front Error:', (string) $frontResponse->getSource());
201+
Assert::contains('Admin Error:', (string) $adminResponse->getSource());
202+
203+
// Verify the error messages are from different presenters
204+
Assert::notContains('Admin Error:', (string) $frontResponse->getSource());
205+
Assert::notContains('Front Error:', (string) $adminResponse->getSource());
206+
}
207+
208+
/**
209+
* Test: Error presenter receives original request information
210+
*/
211+
public function testErrorPresenterReceivesOriginalRequest(): void
212+
{
213+
$originalRequest = new Request('Front:Homepage', 'GET', [
214+
'action' => 'default',
215+
'foo' => 'bar',
216+
]);
217+
218+
// Create error presenter and track request
219+
$errorPresenter = new FrontErrorPresenter();
220+
221+
try {
222+
$presenter = $this->createPresenter('Front:Homepage');
223+
$presenter->run($originalRequest);
224+
} catch (\Throwable $e) {
225+
$errorRequest = new Request(
226+
'Front:Error',
227+
Request::FORWARD,
228+
[
229+
'exception' => $e,
230+
'request' => $originalRequest,
231+
]
232+
);
233+
234+
$errorPresenter->run($errorRequest);
235+
236+
// Verify error presenter received the exception and original request
237+
$receivedRequest = $errorPresenter->getRequest();
238+
Assert::notNull($receivedRequest);
239+
Assert::type(\Throwable::class, $receivedRequest->getParameter('exception'));
240+
Assert::same($originalRequest, $receivedRequest->getParameter('request'));
241+
}
242+
}
243+
244+
/**
245+
* Test: Complete error handling flow with request parameters preserved
246+
*/
247+
public function testCompleteErrorFlowWithParameters(): void
248+
{
249+
// Simulate a POST request with data
250+
$request = new Request('Admin:User', 'POST', [
251+
'action' => 'edit',
252+
'id' => -5,
253+
'name' => 'John Doe',
254+
'email' => 'john@example.com',
255+
]);
256+
257+
$response = $this->simulateApplicationRun($request);
258+
259+
// Error should be handled by Admin:Error
260+
Assert::type(TextResponse::class, $response);
261+
Assert::contains('Admin Error:', (string) $response->getSource());
262+
Assert::contains('User not found', (string) $response->getSource());
263+
}
264+
265+
/**
266+
* Test: Sequential errors in same module go to same error presenter
267+
*/
268+
public function testSequentialErrorsInSameModule(): void
269+
{
270+
$requests = [
271+
new Request('Front:Homepage', 'GET', ['action' => 'default']),
272+
new Request('Front:Homepage', 'GET', ['action' => 'about']),
273+
new Request('Front:Article', 'GET', ['action' => 'detail', 'id' => 1]),
274+
];
275+
276+
foreach ($requests as $request) {
277+
$response = $this->simulateApplicationRun($request);
278+
Assert::type(TextResponse::class, $response);
279+
Assert::contains('Front Error:', (string) $response->getSource());
280+
}
281+
}
282+
283+
/**
284+
* Test: Mixed module errors route correctly
285+
*/
286+
public function testMixedModuleErrorsRouteCorrectly(): void
287+
{
288+
$testCases = [
289+
['Front:Homepage', 'Front Error:'],
290+
['Admin:Dashboard', 'Admin Error:'],
291+
['Front:Article', 'Front Error:'],
292+
['Admin:User', 'Admin Error:'],
293+
];
294+
295+
foreach ($testCases as [$presenterName, $expectedPrefix]) {
296+
$request = new Request($presenterName, 'GET', ['action' => 'default']);
297+
$response = $this->simulateApplicationRun($request);
298+
299+
Assert::contains(
300+
$expectedPrefix,
301+
(string) $response->getSource(),
302+
"Expected '$expectedPrefix' for presenter '$presenterName'"
303+
);
304+
}
305+
}
306+
307+
}
308+
309+
(new ApplicationErrorHandlingTest())->run();
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Fixtures\Presenters\Admin;
4+
5+
use Nette\Application\IPresenter;
6+
use Nette\Application\Request;
7+
use Nette\Application\Response;
8+
9+
/**
10+
* Fixture: Admin:Dashboard presenter that throws an error
11+
*/
12+
class DashboardPresenter implements IPresenter
13+
{
14+
15+
public function run(Request $request): Response
16+
{
17+
throw new \RuntimeException('Admin dashboard error occurred');
18+
}
19+
20+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace Tests\Fixtures\Presenters\Admin;
4+
5+
use Nette\Application\BadRequestException;
6+
use Nette\Application\IPresenter;
7+
use Nette\Application\Request;
8+
use Nette\Application\Response;
9+
10+
/**
11+
* Fixture: Admin:User presenter that throws errors
12+
*/
13+
class UserPresenter implements IPresenter
14+
{
15+
16+
public function run(Request $request): Response
17+
{
18+
$action = $request->getParameter('action') ?? 'default';
19+
$id = $request->getParameter('id');
20+
21+
if ($action === 'list') {
22+
throw new \RuntimeException('User list error');
23+
}
24+
25+
if ($action === 'edit') {
26+
if ($id !== null && $id <= 0) {
27+
throw new BadRequestException('User not found', 404);
28+
}
29+
30+
throw new \RuntimeException('User edit error');
31+
}
32+
33+
throw new \RuntimeException('Unknown action: ' . $action);
34+
}
35+
36+
}

0 commit comments

Comments
 (0)