diff --git a/app/Exceptions/BadRequestException.php b/app/Exceptions/BadRequestException.php new file mode 100644 index 0000000..ebf6385 --- /dev/null +++ b/app/Exceptions/BadRequestException.php @@ -0,0 +1,5 @@ +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'); @@ -85,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( @@ -112,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'); diff --git a/app/Node/UseCases/BulkCreateNodes.php b/app/Node/UseCases/BulkCreateNodes.php index 1ca322d..f2c39ad 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; @@ -17,10 +18,31 @@ class BulkCreateNodes /** * @return Node[] + * @throws BadRequestException * @throws DomainException */ 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'); + } + + 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"); 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, ) {} } diff --git a/app/Node/UseCases/CreateNode.php b/app/Node/UseCases/CreateNode.php index dcb8afa..5819205 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; @@ -16,10 +17,19 @@ class CreateNode ) {} /** + * @throws BadRequestException * @throws DomainException */ 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) { 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, ) {} } diff --git a/app/Plan/UseCases/CreatePlan.php b/app/Plan/UseCases/CreatePlan.php index cf9a3d2..edc65d2 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; @@ -24,10 +25,23 @@ class CreatePlan ) {} /** + * @throws BadRequestException * @throws DomainException */ 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) { 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, ) {} } diff --git a/app/ScheduledNode/UseCases/CreateScheduledNode.php b/app/ScheduledNode/UseCases/CreateScheduledNode.php index a279f62..9a82b10 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; @@ -15,9 +16,21 @@ class CreateScheduledNode private PlanRepository $planRepo, ) {} + /** + * @throws BadRequestException + * @throws DomainException + */ 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) { 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, ) {} } 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'); } } diff --git a/app/Text/UseCases/CreateText.php b/app/Text/UseCases/CreateText.php index bf4715b..a976997 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; @@ -15,8 +16,15 @@ class CreateText private NodeRepository $nodeRepo, ) {} + /** + * @throws BadRequestException + */ 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, )); 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, ) {} } diff --git a/app/User/UseCases/CreateUser.php b/app/User/UseCases/CreateUser.php index b59966f..cc5ffc0 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; @@ -11,8 +12,15 @@ class CreateUser private UserRepository $userRepo, ) {} + /** + * @throws BadRequestException + */ 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), )); 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, ) {} } diff --git a/tests/Unit/Node/UseCases/BulkCreateNodesTest.php b/tests/Unit/Node/UseCases/BulkCreateNodesTest.php index b3f6240..3fd3452 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,69 @@ 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, + )); + } + + 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, + )); + } } 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, + )); + } } 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, + )); + } } 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, + ) + ); + } } 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, + )); + } } 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, + )); + } } 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); + } }