31 - Add required-field validation

This commit is contained in:
myrmidex 2026-06-20 10:15:31 +02:00
parent 1a90a18b49
commit bebc0427b9
6 changed files with 171 additions and 26 deletions

View file

@ -1,5 +1,5 @@
api_platform:
title: Hello API Platform
title: Buckets API Platform
version: 1.0.0
defaults:
stateless: false

View file

@ -44,6 +44,7 @@ class Bucket
private BucketType $type = BucketType::NEED;
#[ORM\Column]
#[Assert\NotNull]
private int $priority;
#[ORM\Column(options: ['default' => 0])]

View file

@ -35,6 +35,7 @@ class Stream
private Scenario $scenario;
#[ORM\Column(length: 255)]
#[Assert\NotBlank]
private string $name;
#[ORM\Column]

View file

@ -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()
// ;
// }
}

View file

@ -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.',
);
}
}

View file

@ -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.',
);
}
}