From 6c1ecc8b38a32b68c5776c78f1e3cbd3f0b3d7f2 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:09:08 +0300 Subject: [PATCH 01/32] add BadRequestException class --- app/Exceptions/BadRequestException.php | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 app/Exceptions/BadRequestException.php diff --git a/app/Exceptions/BadRequestException.php b/app/Exceptions/BadRequestException.php new file mode 100644 index 0000000..1aea2a2 --- /dev/null +++ b/app/Exceptions/BadRequestException.php @@ -0,0 +1,7 @@ + Date: Sun, 19 Apr 2026 23:09:40 +0300 Subject: [PATCH 02/32] test bulk create nodes validates null fields --- .../Node/UseCases/BulkCreateNodesTest.php | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/tests/Unit/Node/UseCases/BulkCreateNodesTest.php b/tests/Unit/Node/UseCases/BulkCreateNodesTest.php index b3f6240..13072db 100644 --- a/tests/Unit/Node/UseCases/BulkCreateNodesTest.php +++ b/tests/Unit/Node/UseCases/BulkCreateNodesTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Node\UseCases; +use App\Exceptions\BadRequestException; use App\Node\CreateNodeDto; use App\Node\Node; use App\Node\UseCases\BulkCreateNodes; @@ -131,4 +132,56 @@ class BulkCreateNodesTest extends TestCase count: 5, )); } + + public function test_throws_if_text_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('textId is required'); + + $this->useCase->execute(new BulkCreateNodesRequest( + textId: null, + parentNodeId: $this->parentNode->getId(), + titlePrefix: 'Page', + count: 5, + )); + } + + public function test_throws_if_parent_node_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('parentNodeId is required'); + + $this->useCase->execute(new BulkCreateNodesRequest( + textId: 0, + parentNodeId: null, + titlePrefix: 'Page', + count: 5, + )); + } + + public function test_throws_if_title_prefix_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('titlePrefix is required'); + + $this->useCase->execute(new BulkCreateNodesRequest( + textId: 0, + parentNodeId: $this->parentNode->getId(), + titlePrefix: null, + count: 5, + )); + } + + public function test_throws_if_count_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('count is required'); + + $this->useCase->execute(new BulkCreateNodesRequest( + textId: 0, + parentNodeId: $this->parentNode->getId(), + titlePrefix: 'Page', + count: null, + )); + } } From 337017fc5298e6c38021142870f09ac37b71a128 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:09:52 +0300 Subject: [PATCH 03/32] make BulkCreateNodesRequest properties nullable --- app/Node/UseCases/BulkCreateNodesRequest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Node/UseCases/BulkCreateNodesRequest.php b/app/Node/UseCases/BulkCreateNodesRequest.php index 45b94be..1682faf 100644 --- a/app/Node/UseCases/BulkCreateNodesRequest.php +++ b/app/Node/UseCases/BulkCreateNodesRequest.php @@ -5,9 +5,9 @@ namespace App\Node\UseCases; class BulkCreateNodesRequest { public function __construct( - public int $textId, - public int $parentNodeId, - public string $titlePrefix, - public int $count, + public ?int $textId, + public ?int $parentNodeId, + public ?string $titlePrefix, + public ?int $count, ) {} } From 1f76fc08b69b3be753e4939df3dc947c2fb95a92 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:10:10 +0300 Subject: [PATCH 04/32] add null guards in bulk create nodes use case --- app/Node/UseCases/BulkCreateNodes.php | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/Node/UseCases/BulkCreateNodes.php b/app/Node/UseCases/BulkCreateNodes.php index 1ca322d..d4b124b 100644 --- a/app/Node/UseCases/BulkCreateNodes.php +++ b/app/Node/UseCases/BulkCreateNodes.php @@ -2,6 +2,7 @@ namespace App\Node\UseCases; +use App\Exceptions\BadRequestException; use App\Node\CreateNodeDto; use App\Node\Node; use App\Node\NodeRepository; @@ -21,6 +22,22 @@ class BulkCreateNodes */ public function execute(BulkCreateNodesRequest $request): array { + if ($request->textId === null) { + throw new BadRequestException('textId is required'); + } + + if ($request->parentNodeId === null) { + throw new BadRequestException('parentNodeId is required'); + } + + if ($request->titlePrefix === null) { + throw new BadRequestException('titlePrefix is required'); + } + + if ($request->count === null) { + throw new BadRequestException('count is required'); + } + $text = $this->textRepo->find($request->textId); if ($text === null) { throw new DomainException("Text with id: {$request->textId} doesnt exist"); From a429b647ccb9e8408c73289283a1c83edd704fdd Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:10:46 +0300 Subject: [PATCH 05/32] test create node use case validates null fields --- tests/Unit/Node/UseCases/CreateNodeTest.php | 25 +++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Unit/Node/UseCases/CreateNodeTest.php b/tests/Unit/Node/UseCases/CreateNodeTest.php index 68d4dfa..5b9226e 100644 --- a/tests/Unit/Node/UseCases/CreateNodeTest.php +++ b/tests/Unit/Node/UseCases/CreateNodeTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Node\UseCases; +use App\Exceptions\BadRequestException; use App\Node\Node; use App\Node\NodeRepository; use App\Node\UseCases\CreateNode; @@ -111,4 +112,28 @@ class CreateNodeTest extends TestCase parentNodeId: null, )); } + + public function test_throws_if_text_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('textId is required'); + + $this->useCase->execute(new CreateNodeRequest( + textId: null, + title: 'test', + parentNodeId: null, + )); + } + + public function test_throws_if_title_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('title is required'); + + $this->useCase->execute(new CreateNodeRequest( + textId: 0, + title: null, + parentNodeId: null, + )); + } } From 23f4e70e57fcb411783750e38a8735e4630d86a2 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:10:58 +0300 Subject: [PATCH 06/32] make CreateNodeRequest properties nullable --- app/Node/UseCases/CreateNodeRequest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Node/UseCases/CreateNodeRequest.php b/app/Node/UseCases/CreateNodeRequest.php index 700a824..521fbbf 100644 --- a/app/Node/UseCases/CreateNodeRequest.php +++ b/app/Node/UseCases/CreateNodeRequest.php @@ -5,8 +5,8 @@ namespace App\Node\UseCases; class CreateNodeRequest { public function __construct( - public int $textId, - public string $title, + public ?int $textId, + public ?string $title, public ?int $parentNodeId, ) {} } From ff721d91375bf7788cec4b6106a2fc11ce588e4c Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:11:15 +0300 Subject: [PATCH 07/32] add null guards in create node use case --- app/Node/UseCases/CreateNode.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/Node/UseCases/CreateNode.php b/app/Node/UseCases/CreateNode.php index dcb8afa..87d8ead 100644 --- a/app/Node/UseCases/CreateNode.php +++ b/app/Node/UseCases/CreateNode.php @@ -2,6 +2,7 @@ namespace App\Node\UseCases; +use App\Exceptions\BadRequestException; use App\Node\Node; use App\Node\CreateNodeDto; use App\Node\NodeRepository; @@ -20,6 +21,14 @@ class CreateNode */ public function execute(CreateNodeRequest $request): Node { + if ($request->textId === null) { + throw new BadRequestException('textId is required'); + } + + if ($request->title === null) { + throw new BadRequestException('title is required'); + } + $textId = $request->textId; $text = $this->textRepo->find($textId); if ($text === null) { From f6ec4a2550081f1ceb8b80e16be5d9c56d08dd2f Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:11:43 +0300 Subject: [PATCH 08/32] test create text use case validates null name --- tests/Unit/Text/UseCases/CreateTextTest.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/Unit/Text/UseCases/CreateTextTest.php b/tests/Unit/Text/UseCases/CreateTextTest.php index 8b9cb56..279d6ab 100644 --- a/tests/Unit/Text/UseCases/CreateTextTest.php +++ b/tests/Unit/Text/UseCases/CreateTextTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Text\UseCases; +use App\Exceptions\BadRequestException; use App\Text\Text; use App\Text\TextRepository; use App\Text\UseCases\CreateText; @@ -51,4 +52,14 @@ class CreateTextTest extends TestCase $this->assertEquals('my text', $rootNode->getTitle()); $this->assertNull($rootNode->getParentNode()); } + + public function test_throws_if_name_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('name is required'); + + $this->useCase->execute(new CreateTextRequest( + name: null, + )); + } } From ab023315a98fd3dbfb6f6082285ae9031aff65f7 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:11:52 +0300 Subject: [PATCH 09/32] make CreateTextRequest name nullable --- app/Text/UseCases/CreateTextRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Text/UseCases/CreateTextRequest.php b/app/Text/UseCases/CreateTextRequest.php index 9ccb2c6..3a6c6a4 100644 --- a/app/Text/UseCases/CreateTextRequest.php +++ b/app/Text/UseCases/CreateTextRequest.php @@ -5,6 +5,6 @@ namespace App\Text\UseCases; class CreateTextRequest { public function __construct( - public string $name, + public ?string $name, ) {} } From f77101e4e9dcc0fd8e0209279a0fe1e9647d0aa2 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:12:07 +0300 Subject: [PATCH 10/32] add null guard in create text use case --- app/Text/UseCases/CreateText.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/Text/UseCases/CreateText.php b/app/Text/UseCases/CreateText.php index bf4715b..79e2667 100644 --- a/app/Text/UseCases/CreateText.php +++ b/app/Text/UseCases/CreateText.php @@ -2,6 +2,7 @@ namespace App\Text\UseCases; +use App\Exceptions\BadRequestException; use App\Text\Text; use App\Text\CreateTextDto; use App\Text\TextRepository; @@ -17,6 +18,10 @@ class CreateText public function execute(CreateTextRequest $request): Text { + if ($request->name === null) { + throw new BadRequestException('name is required'); + } + $text = $this->textRepo->create(new CreateTextDto( name: $request->name, )); From 86052efbcb1d18b5120dec5eb84c1a3f205e0656 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:12:41 +0300 Subject: [PATCH 11/32] test create plan use case validates null fields --- tests/Unit/Plan/UseCases/CreatePlanTest.php | 37 +++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/Unit/Plan/UseCases/CreatePlanTest.php b/tests/Unit/Plan/UseCases/CreatePlanTest.php index 32f9162..26af21d 100644 --- a/tests/Unit/Plan/UseCases/CreatePlanTest.php +++ b/tests/Unit/Plan/UseCases/CreatePlanTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\Plan\UseCases; +use App\Exceptions\BadRequestException; use App\Node\CreateNodeDto; use App\Plan\UseCases\CreatePlan; use App\Plan\UseCases\CreatePlanRequest; @@ -125,4 +126,40 @@ class CreatePlanTest extends TestCase $this->scheduledNodeRepo->getNumberOfTimesCreateCalled() ); } + + public function test_throws_if_user_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('userId is required'); + + $this->useCase->execute(new CreatePlanRequest( + userId: null, + name: 'testPlan', + textId: 0, + )); + } + + public function test_throws_if_text_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('textId is required'); + + $this->useCase->execute(new CreatePlanRequest( + userId: 0, + name: 'testPlan', + textId: null, + )); + } + + public function test_throws_if_name_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('name is required'); + + $this->useCase->execute(new CreatePlanRequest( + userId: 0, + name: null, + textId: 0, + )); + } } From 02244b6ed91ce8972074eb1a02df0e219bc5b689 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:12:52 +0300 Subject: [PATCH 12/32] make CreatePlanRequest properties nullable --- app/Plan/UseCases/CreatePlanRequest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Plan/UseCases/CreatePlanRequest.php b/app/Plan/UseCases/CreatePlanRequest.php index 2af3e1e..d9e0669 100644 --- a/app/Plan/UseCases/CreatePlanRequest.php +++ b/app/Plan/UseCases/CreatePlanRequest.php @@ -5,8 +5,8 @@ namespace App\Plan\UseCases; class CreatePlanRequest { public function __construct( - public int $userId, - public int $textId, - public string $name, + public ?int $userId, + public ?int $textId, + public ?string $name, ) {} } From 3775607503f89d42089e0e9025fed00245e12b46 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:13:08 +0300 Subject: [PATCH 13/32] add null guards in create plan use case --- app/Plan/UseCases/CreatePlan.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/Plan/UseCases/CreatePlan.php b/app/Plan/UseCases/CreatePlan.php index cf9a3d2..c499a8b 100644 --- a/app/Plan/UseCases/CreatePlan.php +++ b/app/Plan/UseCases/CreatePlan.php @@ -2,6 +2,7 @@ namespace App\Plan\UseCases; +use App\Exceptions\BadRequestException; use App\Node\NodeRepository; use App\Plan\CreatePlanDto; use App\Plan\Plan; @@ -28,6 +29,18 @@ class CreatePlan */ public function execute(CreatePlanRequest $request): Plan { + if ($request->userId === null) { + throw new BadRequestException('userId is required'); + } + + if ($request->textId === null) { + throw new BadRequestException('textId is required'); + } + + if ($request->name === null) { + throw new BadRequestException('name is required'); + } + $userId = $request->userId; $user = $this->userRepo->find($userId); if ($user === null) { From 1ce379904446aed50eab7d8f45cfee40590bc9fa Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:13:34 +0300 Subject: [PATCH 14/32] test create user use case validates null email --- tests/Unit/User/UseCases/CreateUserTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Unit/User/UseCases/CreateUserTest.php b/tests/Unit/User/UseCases/CreateUserTest.php index 0867537..180046f 100644 --- a/tests/Unit/User/UseCases/CreateUserTest.php +++ b/tests/Unit/User/UseCases/CreateUserTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\User\UseCases; +use App\Exceptions\BadRequestException; use App\User\User; use App\User\UseCases\CreateUser; use App\User\UseCases\CreateUserRequest; @@ -21,4 +22,17 @@ class CreateUserTest extends TestCase $this->assertInstanceOf(User::class, $user); $this->assertEquals('test@test.com', $user->getEmail()); } + + public function test_throws_if_email_is_null(): void + { + $userRepo = new FakeUserRepository(); + $useCase = new CreateUser($userRepo); + + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('email is required'); + + $useCase->execute(new CreateUserRequest( + email: null, + )); + } } From e2c220e0eee3382bd869cab80be8fe1a7ee6e28a Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:13:42 +0300 Subject: [PATCH 15/32] make CreateUserRequest email nullable --- app/User/UseCases/CreateUserRequest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/User/UseCases/CreateUserRequest.php b/app/User/UseCases/CreateUserRequest.php index 93a3102..7c48913 100644 --- a/app/User/UseCases/CreateUserRequest.php +++ b/app/User/UseCases/CreateUserRequest.php @@ -5,6 +5,6 @@ namespace App\User\UseCases; class CreateUserRequest { public function __construct( - public string $email, + public ?string $email, ) {} } From 90a9002df09a8b29e6874cfbd3311f7ece9d5ecc Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:13:57 +0300 Subject: [PATCH 16/32] add null guard in create user use case --- app/User/UseCases/CreateUser.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/User/UseCases/CreateUser.php b/app/User/UseCases/CreateUser.php index b59966f..1241647 100644 --- a/app/User/UseCases/CreateUser.php +++ b/app/User/UseCases/CreateUser.php @@ -2,6 +2,7 @@ namespace App\User\UseCases; +use App\Exceptions\BadRequestException; use App\User\UserRepository; use App\ValueObjects\EmailAddress; @@ -13,6 +14,10 @@ class CreateUser public function execute(CreateUserRequest $dto): void { + if ($dto->email === null) { + throw new BadRequestException('email is required'); + } + $this->userRepo->create(new CreateUserDto( email: new EmailAddress($dto->email), )); From 1a4144c2783f22de4f0108754f4aecae518a3a47 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:14:25 +0300 Subject: [PATCH 17/32] test create scheduled node validates null fields --- .../UseCases/CreateScheduledNodeTest.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php b/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php index 43617bd..3350a81 100644 --- a/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php +++ b/tests/Unit/ScheduledNode/UseCases/CreateScheduledNodeTest.php @@ -2,6 +2,7 @@ namespace Tests\Unit\ScheduledNode\UseCases; +use App\Exceptions\BadRequestException; use App\Plan\CreatePlanDto; use App\Plan\Plan; use App\ScheduledNode\ScheduledNode; @@ -75,4 +76,30 @@ class CreateScheduledNodeTest extends TestCase ) ); } + + public function test_throws_if_date_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('date is required'); + + $this->useCase->execute( + new CreateScheduledNodeRequest( + date: null, + planId: 0, + ) + ); + } + + public function test_throws_if_plan_id_is_null(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('planId is required'); + + $this->useCase->execute( + new CreateScheduledNodeRequest( + date: new DateTimeImmutable('now'), + planId: null, + ) + ); + } } From 8cd11e98c244018b5a8a28433ecd4aea6a4dbad5 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:14:36 +0300 Subject: [PATCH 18/32] make CreateScheduledNodeRequest properties nullable --- app/ScheduledNode/UseCases/CreateScheduledNodeRequest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/ScheduledNode/UseCases/CreateScheduledNodeRequest.php b/app/ScheduledNode/UseCases/CreateScheduledNodeRequest.php index 153dddc..1fb9c80 100644 --- a/app/ScheduledNode/UseCases/CreateScheduledNodeRequest.php +++ b/app/ScheduledNode/UseCases/CreateScheduledNodeRequest.php @@ -7,7 +7,7 @@ use DateTimeImmutable; class CreateScheduledNodeRequest { public function __construct( - public DateTimeImmutable $date, - public int $planId, + public ?DateTimeImmutable $date, + public ?int $planId, ) {} } From 374eaeb4ecbfcc4007159107a512f8d924eaf1f2 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:14:52 +0300 Subject: [PATCH 19/32] add null guards in create scheduled node use case --- app/ScheduledNode/UseCases/CreateScheduledNode.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/ScheduledNode/UseCases/CreateScheduledNode.php b/app/ScheduledNode/UseCases/CreateScheduledNode.php index a279f62..7450e6d 100644 --- a/app/ScheduledNode/UseCases/CreateScheduledNode.php +++ b/app/ScheduledNode/UseCases/CreateScheduledNode.php @@ -2,6 +2,7 @@ namespace App\ScheduledNode\UseCases; +use App\Exceptions\BadRequestException; use App\Plan\PlanRepository; use App\ScheduledNode\ScheduledNode; use App\ScheduledNode\CreateScheduledNodeDto; @@ -18,6 +19,14 @@ class CreateScheduledNode public function execute( CreateScheduledNodeRequest $request ): ScheduledNode { + if ($request->date === null) { + throw new BadRequestException('date is required'); + } + + if ($request->planId === null) { + throw new BadRequestException('planId is required'); + } + $id = $request->planId; $plan = $this->planRepo->find($id); if ($plan === null) { From ace727dae093f35a8e5359d3b86c71e75cf88a2b Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:19:05 +0300 Subject: [PATCH 20/32] php cs fix --- app/Exceptions/BadRequestException.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/app/Exceptions/BadRequestException.php b/app/Exceptions/BadRequestException.php index 1aea2a2..ebf6385 100644 --- a/app/Exceptions/BadRequestException.php +++ b/app/Exceptions/BadRequestException.php @@ -2,6 +2,4 @@ namespace App\Exceptions; -class BadRequestException extends \RuntimeException -{ -} +class BadRequestException extends \RuntimeException {} From 1199fcbff36a2dad44b7214becb1305a13431b39 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:22:56 +0300 Subject: [PATCH 21/32] add @throws BadRequestException to bulk create nodes --- app/Node/UseCases/BulkCreateNodes.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Node/UseCases/BulkCreateNodes.php b/app/Node/UseCases/BulkCreateNodes.php index d4b124b..966e704 100644 --- a/app/Node/UseCases/BulkCreateNodes.php +++ b/app/Node/UseCases/BulkCreateNodes.php @@ -18,6 +18,7 @@ class BulkCreateNodes /** * @return Node[] + * @throws BadRequestException * @throws DomainException */ public function execute(BulkCreateNodesRequest $request): array From 99c320b28fda519bc104ee838a4dd1c3cfcefa32 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:23:03 +0300 Subject: [PATCH 22/32] add @throws BadRequestException to create node --- app/Node/UseCases/CreateNode.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Node/UseCases/CreateNode.php b/app/Node/UseCases/CreateNode.php index 87d8ead..5819205 100644 --- a/app/Node/UseCases/CreateNode.php +++ b/app/Node/UseCases/CreateNode.php @@ -17,6 +17,7 @@ class CreateNode ) {} /** + * @throws BadRequestException * @throws DomainException */ public function execute(CreateNodeRequest $request): Node From 920f8ad76824430259e8bff631242452c8e413c7 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:23:11 +0300 Subject: [PATCH 23/32] add @throws BadRequestException to create text --- app/Text/UseCases/CreateText.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/Text/UseCases/CreateText.php b/app/Text/UseCases/CreateText.php index 79e2667..a976997 100644 --- a/app/Text/UseCases/CreateText.php +++ b/app/Text/UseCases/CreateText.php @@ -16,6 +16,9 @@ class CreateText private NodeRepository $nodeRepo, ) {} + /** + * @throws BadRequestException + */ public function execute(CreateTextRequest $request): Text { if ($request->name === null) { From a8bc84f2462fc8e580ac7aec895c7b4569f28aa3 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:23:18 +0300 Subject: [PATCH 24/32] add @throws BadRequestException to create plan --- app/Plan/UseCases/CreatePlan.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Plan/UseCases/CreatePlan.php b/app/Plan/UseCases/CreatePlan.php index c499a8b..edc65d2 100644 --- a/app/Plan/UseCases/CreatePlan.php +++ b/app/Plan/UseCases/CreatePlan.php @@ -25,6 +25,7 @@ class CreatePlan ) {} /** + * @throws BadRequestException * @throws DomainException */ public function execute(CreatePlanRequest $request): Plan From 03e47817994f9adfd3b0395c6f6b27d1bb4c39fd Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:23:26 +0300 Subject: [PATCH 25/32] add @throws BadRequestException to create user --- app/User/UseCases/CreateUser.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/User/UseCases/CreateUser.php b/app/User/UseCases/CreateUser.php index 1241647..cc5ffc0 100644 --- a/app/User/UseCases/CreateUser.php +++ b/app/User/UseCases/CreateUser.php @@ -12,6 +12,9 @@ class CreateUser private UserRepository $userRepo, ) {} + /** + * @throws BadRequestException + */ public function execute(CreateUserRequest $dto): void { if ($dto->email === null) { From 2d19265c2403c99689ce07bd88da7d620c664b4a Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:23:33 +0300 Subject: [PATCH 26/32] add @throws BadRequestException to create scheduled node --- app/ScheduledNode/UseCases/CreateScheduledNode.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/ScheduledNode/UseCases/CreateScheduledNode.php b/app/ScheduledNode/UseCases/CreateScheduledNode.php index 7450e6d..9a82b10 100644 --- a/app/ScheduledNode/UseCases/CreateScheduledNode.php +++ b/app/ScheduledNode/UseCases/CreateScheduledNode.php @@ -16,6 +16,10 @@ class CreateScheduledNode private PlanRepository $planRepo, ) {} + /** + * @throws BadRequestException + * @throws DomainException + */ public function execute( CreateScheduledNodeRequest $request ): ScheduledNode { From 32bf96dd99bf014501a94c0be398a6e072665f3a Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:36:18 +0300 Subject: [PATCH 27/32] test bulk create nodes throws if count is less than one --- tests/Unit/Node/UseCases/BulkCreateNodesTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Unit/Node/UseCases/BulkCreateNodesTest.php b/tests/Unit/Node/UseCases/BulkCreateNodesTest.php index 13072db..3fd3452 100644 --- a/tests/Unit/Node/UseCases/BulkCreateNodesTest.php +++ b/tests/Unit/Node/UseCases/BulkCreateNodesTest.php @@ -184,4 +184,17 @@ class BulkCreateNodesTest extends TestCase count: null, )); } + + public function test_throws_if_count_is_less_than_one(): void + { + $this->expectException(BadRequestException::class); + $this->expectExceptionMessage('count must be at least 1'); + + $this->useCase->execute(new BulkCreateNodesRequest( + textId: 0, + parentNodeId: $this->parentNode->getId(), + titlePrefix: 'Page', + count: 0, + )); + } } From 38b7a0adb8ba9554f2a437862430e1f9f0763a38 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:36:37 +0300 Subject: [PATCH 28/32] add count validation in bulk create nodes use case --- app/Node/UseCases/BulkCreateNodes.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/Node/UseCases/BulkCreateNodes.php b/app/Node/UseCases/BulkCreateNodes.php index 966e704..f2c39ad 100644 --- a/app/Node/UseCases/BulkCreateNodes.php +++ b/app/Node/UseCases/BulkCreateNodes.php @@ -39,6 +39,10 @@ class BulkCreateNodes throw new BadRequestException('count is required'); } + if ($request->count < 1) { + throw new BadRequestException('count must be at least 1'); + } + $text = $this->textRepo->find($request->textId); if ($text === null) { throw new DomainException("Text with id: {$request->textId} doesnt exist"); From 1761bfad7f0188cb9f7a44f680b0c38b31b4c0fc Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:37:12 +0300 Subject: [PATCH 29/32] refactor create node controller to catch BadRequestException --- app/Node/NodeController.php | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/app/Node/NodeController.php b/app/Node/NodeController.php index 20d534f..fee4d13 100644 --- a/app/Node/NodeController.php +++ b/app/Node/NodeController.php @@ -2,6 +2,7 @@ namespace App\Node; +use App\Exceptions\BadRequestException; use App\Node\UseCases\BulkCreateNodesRequest; use App\Node\NodeRepository; use App\Node\UseCases\BulkCreateNodes; @@ -47,17 +48,10 @@ class NodeController CreateNode $createNodeUseCase, ): Response { $data = json_decode((string) $request->getBody(), true) ?? []; - $title = $data['title'] ?? ''; - if (empty($title)) { - $response->getBody()->write(json_encode(['error' => 'Title is required'])); - return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); - } - - $textId = (int) ($data['textId'] ?? 0); - $parentNodeId = isset($data['parentNodeId']) && $data['parentNodeId'] !== null - ? (int) $data['parentNodeId'] - : null; + $textId = isset($data['textId']) ? (int) $data['textId'] : null; + $title = $data['title'] ?? null; + $parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null; try { $node = $createNodeUseCase->execute(new CreateNodeRequest( @@ -65,6 +59,9 @@ class NodeController title: $title, parentNodeId: $parentNodeId, )); + } catch (BadRequestException $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } catch (DomainException $e) { $response->getBody()->write(json_encode(['error' => $e->getMessage()])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); From 8a90c5bab41b277bcae3e20b4caab579cc1b5312 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:37:40 +0300 Subject: [PATCH 30/32] refactor bulk create nodes controller to catch BadRequestException --- app/Node/NodeController.php | 26 +++++++------------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/app/Node/NodeController.php b/app/Node/NodeController.php index fee4d13..bb3aa30 100644 --- a/app/Node/NodeController.php +++ b/app/Node/NodeController.php @@ -82,25 +82,10 @@ class NodeController ): Response { $data = json_decode((string) $request->getBody(), true) ?? []; - $titlePrefix = trim($data['titlePrefix'] ?? ''); - if ($titlePrefix === '') { - $response->getBody()->write(json_encode(['error' => 'Title prefix is required'])); - return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); - } - - $count = isset($data['count']) ? (int) $data['count'] : 0; - if ($count < 1) { - $response->getBody()->write(json_encode(['error' => 'Count must be at least 1'])); - return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); - } - - if (!isset($data['parentNodeId']) || $data['parentNodeId'] === null) { - $response->getBody()->write(json_encode(['error' => 'parentNodeId is required'])); - return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); - } - - $textId = (int) ($data['textId'] ?? 0); - $parentNodeId = (int) $data['parentNodeId']; + $textId = isset($data['textId']) ? (int) $data['textId'] : null; + $parentNodeId = isset($data['parentNodeId']) ? (int) $data['parentNodeId'] : null; + $titlePrefix = isset($data['titlePrefix']) ? (string) $data['titlePrefix'] : null; + $count = isset($data['count']) ? (int) $data['count'] : null; try { $nodes = $bulkCreateNodesUseCase->execute(new BulkCreateNodesRequest( @@ -109,6 +94,9 @@ class NodeController titlePrefix: $titlePrefix, count: $count, )); + } catch (BadRequestException $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } catch (DomainException $e) { $response->getBody()->write(json_encode(['error' => $e->getMessage()])); return $response->withStatus(404)->withHeader('Content-Type', 'application/json'); From 82dab3b90f04095abce9d737e6893f06ff984bee Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:38:02 +0300 Subject: [PATCH 31/32] test create text controller returns 400 when name missing --- tests/e2e/Controllers/TextControllerTest.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/e2e/Controllers/TextControllerTest.php b/tests/e2e/Controllers/TextControllerTest.php index 3289598..ec5af99 100644 --- a/tests/e2e/Controllers/TextControllerTest.php +++ b/tests/e2e/Controllers/TextControllerTest.php @@ -84,4 +84,24 @@ class TextControllerTest extends TestCase $response->getBody() ); } + + public function test_create_text_returns_400_when_name_missing(): void + { + $request = new ServerRequestFactory() + ->createServerRequest('POST', 'http://localhost/texts') + ->withParsedBody([]); + + $response = $this->controller->createText( + $request, + new Response(), + new CreateText( + $this->textRepo, + new FakeNodeRepository(), + ), + ); + + $this->assertEquals(400, $response->getStatusCode()); + $body = json_decode($response->getBody(), true); + $this->assertArrayHasKey('error', $body); + } } From 6009fb7dddbec053038523d3570d2da7619e8215 Mon Sep 17 00:00:00 2001 From: Yisroel Baum Date: Sun, 19 Apr 2026 23:38:23 +0300 Subject: [PATCH 32/32] refactor create text controller to catch BadRequestException --- app/Text/TextController.php | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/app/Text/TextController.php b/app/Text/TextController.php index c608018..3834f32 100644 --- a/app/Text/TextController.php +++ b/app/Text/TextController.php @@ -2,6 +2,7 @@ namespace App\Text; +use App\Exceptions\BadRequestException; use App\Text\TextRepository; use App\Text\UseCases\CreateText; use App\Text\UseCases\CreateTextRequest; @@ -50,21 +51,21 @@ class TextController CreateText $createTextUseCase, ): Response { $data = $request->getParsedBody(); - $name = $data['name'] ?? ''; + $name = $data['name'] ?? null; - if (!empty($name)) { + try { $text = $createTextUseCase->execute(new CreateTextRequest( name: $name, )); - - $response->getBody()->write(json_encode([ - 'id' => $text->getId(), - 'name' => $text->getName(), - ])); - return $response->withHeader('Content-Type', 'application/json'); + } catch (BadRequestException $e) { + $response->getBody()->write(json_encode(['error' => $e->getMessage()])); + return $response->withStatus(400)->withHeader('Content-Type', 'application/json'); } - $response->getBody()->write(json_encode(['error' => 'Name is required'])); - return $response->withStatus(400); + $response->getBody()->write(json_encode([ + 'id' => $text->getId(), + 'name' => $text->getName(), + ])); + return $response->withHeader('Content-Type', 'application/json'); } }