From bebc0427b97e6c6a1e6c5f2d2a57b7cf33c321b5 Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sat, 20 Jun 2026 10:15:31 +0200 Subject: [PATCH] 31 - Add required-field validation --- config/packages/api_platform.yaml | 2 +- src/Entity/Bucket.php | 1 + src/Entity/Stream.php | 1 + src/Repository/StreamRepository.php | 25 -------- tests/Functional/BucketApiTest.php | 72 ++++++++++++++++++++++ tests/Functional/StreamApiTest.php | 96 +++++++++++++++++++++++++++++ 6 files changed, 171 insertions(+), 26 deletions(-) diff --git a/config/packages/api_platform.yaml b/config/packages/api_platform.yaml index f3060c5..afc1612 100644 --- a/config/packages/api_platform.yaml +++ b/config/packages/api_platform.yaml @@ -1,5 +1,5 @@ api_platform: - title: Hello API Platform + title: Buckets API Platform version: 1.0.0 defaults: stateless: false diff --git a/src/Entity/Bucket.php b/src/Entity/Bucket.php index bda8e03..5329b2b 100644 --- a/src/Entity/Bucket.php +++ b/src/Entity/Bucket.php @@ -44,6 +44,7 @@ class Bucket private BucketType $type = BucketType::NEED; #[ORM\Column] + #[Assert\NotNull] private int $priority; #[ORM\Column(options: ['default' => 0])] diff --git a/src/Entity/Stream.php b/src/Entity/Stream.php index 2cebbbd..9600b8f 100644 --- a/src/Entity/Stream.php +++ b/src/Entity/Stream.php @@ -35,6 +35,7 @@ class Stream private Scenario $scenario; #[ORM\Column(length: 255)] + #[Assert\NotBlank] private string $name; #[ORM\Column] diff --git a/src/Repository/StreamRepository.php b/src/Repository/StreamRepository.php index 65f38ae..54107a7 100644 --- a/src/Repository/StreamRepository.php +++ b/src/Repository/StreamRepository.php @@ -15,29 +15,4 @@ class StreamRepository extends ServiceEntityRepository { parent::__construct($registry, Stream::class); } - - // /** - // * @return Stream[] Returns an array of Stream objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('s') - // ->andWhere('s.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('s.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?Stream - // { - // return $this->createQueryBuilder('s') - // ->andWhere('s.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } } diff --git a/tests/Functional/BucketApiTest.php b/tests/Functional/BucketApiTest.php index 4469ba1..2eed148 100644 --- a/tests/Functional/BucketApiTest.php +++ b/tests/Functional/BucketApiTest.php @@ -396,4 +396,76 @@ final class BucketApiTest extends WebTestCase 'POST /api/buckets without a name must return 422 Unprocessable Entity (validator-rejected, not a 500).', ); } + + public function testPostWithMissingPriorityIsRejectedAsUnprocessable(): void + { + $scenario = $this->persistScenario('Household Budget'); + + $this->login(); + + $this->client->request( + 'POST', + '/api/buckets', + server: [ + 'CONTENT_TYPE' => 'application/ld+json', + 'HTTP_ACCEPT' => 'application/ld+json', + ], + content: json_encode([ + 'scenario' => $this->scenarioIri($scenario), + 'name' => 'Groceries', + ]), + ); + + $response = $this->client->getResponse(); + + self::assertSame( + 422, + $response->getStatusCode(), + 'POST /api/buckets without a priority must return 422 Unprocessable Entity (validator-rejected, not a 500).', + ); + + $this->assertHasViolationAt('priority', $response); + } + + public function testGetItemIsRejectedWhenUnauthenticated(): void + { + $scenario = $this->persistScenario('Household Budget'); + $bucket = $this->persistBucket($scenario, 'Rent', 1); + + $this->client->request('GET', '/api/buckets/'.$bucket->getId(), server: [ + 'HTTP_ACCEPT' => 'application/ld+json', + ]); + + self::assertSame( + 401, + $this->client->getResponse()->getStatusCode(), + 'GET /api/buckets/{id} must require authentication.', + ); + } + + public function testPostWithNonJsonContentTypeIsRejected(): void + { + $this->login(); + + $this->client->request( + 'POST', + '/api/buckets', + ['name' => 'Groceries'], + ); + + self::assertSame( + 415, + $this->client->getResponse()->getStatusCode(), + 'POST /api/buckets without a JSON Content-Type must be rejected with 415 Unsupported Media Type.', + ); + + $this->em->clear(); + + $persisted = $this->em->getRepository(Bucket::class)->findOneBy(['name' => 'Groceries']); + + self::assertNull( + $persisted, + 'A rejected non-JSON POST must not persist a Bucket.', + ); + } } diff --git a/tests/Functional/StreamApiTest.php b/tests/Functional/StreamApiTest.php index e0682bb..f145fcd 100644 --- a/tests/Functional/StreamApiTest.php +++ b/tests/Functional/StreamApiTest.php @@ -284,4 +284,100 @@ final class StreamApiTest extends WebTestCase 'POST /api/streams without a startDate must return 422 Unprocessable Entity (validator-rejected, not a 500).', ); } + + public function testPostWithMissingNameIsRejectedAsUnprocessable(): void + { + $scenario = $this->persistScenario('Household Budget'); + + $this->login(); + + $this->client->request( + 'POST', + '/api/streams', + server: [ + 'CONTENT_TYPE' => 'application/ld+json', + 'HTTP_ACCEPT' => 'application/ld+json', + ], + content: json_encode([ + 'scenario' => $this->scenarioIri($scenario), + 'amount' => 300000, + 'type' => StreamType::INCOME->value, + 'frequency' => StreamFrequency::MONTHLY->value, + 'startDate' => '2026-01-01', + ]), + ); + + self::assertSame( + 422, + $this->client->getResponse()->getStatusCode(), + 'POST /api/streams without a name must return 422 Unprocessable Entity (validator-rejected, not a 500).', + ); + } + + public function testGetItemIsRejectedWhenUnauthenticated(): void + { + $scenario = $this->persistScenario('Household Budget'); + $stream = $this->persistStream($scenario, 'Salary'); + + $this->client->request('GET', '/api/streams/'.$stream->getId(), server: [ + 'HTTP_ACCEPT' => 'application/ld+json', + ]); + + self::assertSame( + 401, + $this->client->getResponse()->getStatusCode(), + 'GET /api/streams/{id} must require authentication.', + ); + } + + public function testPostIsRejectedWhenUnauthenticated(): void + { + $this->client->request( + 'POST', + '/api/streams', + server: [ + 'CONTENT_TYPE' => 'application/ld+json', + 'HTTP_ACCEPT' => 'application/ld+json', + ], + content: json_encode([ + 'name' => 'Salary', + 'amount' => 300000, + 'type' => StreamType::INCOME->value, + 'frequency' => StreamFrequency::MONTHLY->value, + 'startDate' => '2026-01-01', + ]), + ); + + self::assertSame( + 401, + $this->client->getResponse()->getStatusCode(), + 'POST /api/streams must require authentication.', + ); + } + + public function testPostWithNonJsonContentTypeIsRejected(): void + { + $this->login(); + + $this->client->request( + 'POST', + '/api/streams', + ['name' => 'Salary'], + ); + + self::assertSame( + 415, + $this->client->getResponse()->getStatusCode(), + 'POST /api/streams without a JSON Content-Type must be rejected with 415 Unsupported Media Type.', + ); + + $this->em->clear(); + + $persisted = $this->em->getRepository(Stream::class)->findOneBy(['name' => 'Salary']); + + self::assertNull( + $persisted, + 'A rejected non-JSON POST must not persist a Stream.', + ); + } }