31 - Expose Scenario via API with ROLE_USER gating
This commit is contained in:
parent
a2663be938
commit
e4ae6dabf6
3 changed files with 272 additions and 1 deletions
|
|
@ -2,6 +2,8 @@ api_platform:
|
||||||
title: Hello API Platform
|
title: Hello API Platform
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
defaults:
|
defaults:
|
||||||
stateless: true
|
stateless: false
|
||||||
cache_headers:
|
cache_headers:
|
||||||
vary: ['Content-Type', 'Authorization', 'Origin']
|
vary: ['Content-Type', 'Authorization', 'Origin']
|
||||||
|
formats:
|
||||||
|
jsonld: ['application/ld+json']
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,22 @@
|
||||||
|
|
||||||
namespace App\Entity;
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
use App\Enum\DistributionMode;
|
use App\Enum\DistributionMode;
|
||||||
use App\Repository\ScenarioRepository;
|
use App\Repository\ScenarioRepository;
|
||||||
use Doctrine\DBAL\Types\Types;
|
use Doctrine\DBAL\Types\Types;
|
||||||
use Doctrine\ORM\Mapping as ORM;
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
use Symfony\Component\Uid\UuidV7;
|
use Symfony\Component\Uid\UuidV7;
|
||||||
|
use Symfony\Component\Validator\Constraints as Assert;
|
||||||
|
|
||||||
#[ORM\Entity(repositoryClass: ScenarioRepository::class)]
|
#[ORM\Entity(repositoryClass: ScenarioRepository::class)]
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [new GetCollection(), new Get(), new Post()],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
)]
|
||||||
class Scenario
|
class Scenario
|
||||||
{
|
{
|
||||||
#[ORM\Id]
|
#[ORM\Id]
|
||||||
|
|
@ -19,6 +28,7 @@ class Scenario
|
||||||
private DistributionMode $distributionMode = DistributionMode::EVEN;
|
private DistributionMode $distributionMode = DistributionMode::EVEN;
|
||||||
|
|
||||||
#[ORM\Column(length: 255)]
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Assert\NotBlank]
|
||||||
private string $name;
|
private string $name;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
#[ORM\Column(type: Types::TEXT, nullable: true)]
|
||||||
|
|
|
||||||
259
tests/Functional/ScenarioApiTest.php
Normal file
259
tests/Functional/ScenarioApiTest.php
Normal file
|
|
@ -0,0 +1,259 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Functional;
|
||||||
|
|
||||||
|
use App\Entity\Scenario;
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\KernelBrowser;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\NativePasswordHasher;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactory;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||||
|
|
||||||
|
final class ScenarioApiTest extends WebTestCase
|
||||||
|
{
|
||||||
|
private const EMAIL = 'scenario-api-test@example.com';
|
||||||
|
private const PASSWORD = 'correct-horse-battery-staple';
|
||||||
|
|
||||||
|
private KernelBrowser $client;
|
||||||
|
private EntityManagerInterface $em;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
$this->client = static::createClient();
|
||||||
|
|
||||||
|
/** @var EntityManagerInterface $em */
|
||||||
|
$em = static::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
$this->em = $em;
|
||||||
|
|
||||||
|
$factory = new PasswordHasherFactory([
|
||||||
|
User::class => new NativePasswordHasher(cost: 4),
|
||||||
|
]);
|
||||||
|
$hasher = new UserPasswordHasher($factory);
|
||||||
|
|
||||||
|
$user = new User();
|
||||||
|
$user->setEmail(self::EMAIL);
|
||||||
|
$user->setPassword($hasher->hashPassword($user, self::PASSWORD));
|
||||||
|
|
||||||
|
$this->em->persist($user);
|
||||||
|
$this->em->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function login(): void
|
||||||
|
{
|
||||||
|
$this->client->jsonRequest('POST', '/api/login', [
|
||||||
|
'username' => self::EMAIL,
|
||||||
|
'password' => self::PASSWORD,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertLessThan(
|
||||||
|
300,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'Test setup precondition: login must succeed before exercising the Scenario API.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function persistScenario(string $name): Scenario
|
||||||
|
{
|
||||||
|
$scenario = new Scenario();
|
||||||
|
$scenario->setName($name);
|
||||||
|
|
||||||
|
$this->em->persist($scenario);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return $scenario;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCollectionIsRejectedWhenUnauthenticated(): void
|
||||||
|
{
|
||||||
|
$this->client->request('GET', '/api/scenarios', server: [
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
401,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'GET /api/scenarios must require authentication.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetItemIsRejectedWhenUnauthenticated(): void
|
||||||
|
{
|
||||||
|
$scenario = $this->persistScenario('Emergency Fund');
|
||||||
|
|
||||||
|
$this->client->request('GET', '/api/scenarios/'.$scenario->getId(), server: [
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
401,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'GET /api/scenarios/{id} must require authentication.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostIsRejectedWhenUnauthenticated(): void
|
||||||
|
{
|
||||||
|
$this->client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/scenarios',
|
||||||
|
server: [
|
||||||
|
'CONTENT_TYPE' => 'application/ld+json',
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
],
|
||||||
|
content: json_encode(['name' => 'Holiday Fund']),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
401,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'POST /api/scenarios must require authentication.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetCollectionReturnsPersistedScenariosWhenAuthenticated(): void
|
||||||
|
{
|
||||||
|
$this->persistScenario('Household Budget');
|
||||||
|
$this->persistScenario('Travel Fund');
|
||||||
|
|
||||||
|
$this->login();
|
||||||
|
|
||||||
|
$this->client->request('GET', '/api/scenarios', server: [
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->getResponse();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
200,
|
||||||
|
$response->getStatusCode(),
|
||||||
|
'GET /api/scenarios must return 200 for an authenticated user.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$body = json_decode((string) $response->getContent(), true);
|
||||||
|
|
||||||
|
self::assertIsArray($body, 'The collection response must be a JSON-LD/Hydra object.');
|
||||||
|
self::assertArrayHasKey(
|
||||||
|
'member',
|
||||||
|
$body,
|
||||||
|
'A Hydra collection response must expose its items under the member key.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$names = array_column($body['member'], 'name');
|
||||||
|
|
||||||
|
self::assertContains('Household Budget', $names);
|
||||||
|
self::assertContains('Travel Fund', $names);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testGetItemReturnsTheScenarioWhenAuthenticated(): void
|
||||||
|
{
|
||||||
|
$scenario = $this->persistScenario('Emergency Fund');
|
||||||
|
|
||||||
|
$this->login();
|
||||||
|
|
||||||
|
$this->client->request('GET', '/api/scenarios/'.$scenario->getId(), server: [
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->client->getResponse();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
200,
|
||||||
|
$response->getStatusCode(),
|
||||||
|
'GET /api/scenarios/{id} must return 200 for an authenticated user.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$body = json_decode((string) $response->getContent(), true);
|
||||||
|
|
||||||
|
self::assertIsArray($body, 'The item response must be a JSON-LD object.');
|
||||||
|
self::assertArrayHasKey('@id', $body, 'A JSON-LD item must expose its IRI under @id.');
|
||||||
|
self::assertSame('Scenario', $body['@type'] ?? null, 'The item @type must be Scenario.');
|
||||||
|
self::assertSame('Emergency Fund', $body['name'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostCreatesAScenarioWhenAuthenticated(): void
|
||||||
|
{
|
||||||
|
$this->login();
|
||||||
|
|
||||||
|
$this->client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/scenarios',
|
||||||
|
server: [
|
||||||
|
'CONTENT_TYPE' => 'application/ld+json',
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
],
|
||||||
|
content: json_encode(['name' => 'Holiday Fund']),
|
||||||
|
);
|
||||||
|
|
||||||
|
$response = $this->client->getResponse();
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
201,
|
||||||
|
$response->getStatusCode(),
|
||||||
|
'POST /api/scenarios with a valid body must return 201 Created.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$body = json_decode((string) $response->getContent(), true);
|
||||||
|
|
||||||
|
self::assertIsArray($body, 'The created resource response must be a JSON-LD object.');
|
||||||
|
self::assertArrayHasKey('@id', $body, 'The created resource must expose its IRI under @id.');
|
||||||
|
self::assertSame('Holiday Fund', $body['name'] ?? null);
|
||||||
|
|
||||||
|
$this->em->clear();
|
||||||
|
|
||||||
|
$persisted = $this->em->getRepository(Scenario::class)->findOneBy(['name' => 'Holiday Fund']);
|
||||||
|
|
||||||
|
self::assertNotNull(
|
||||||
|
$persisted,
|
||||||
|
'A successful POST /api/scenarios must persist the new Scenario to the database.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostWithMissingNameIsRejectedAsUnprocessable(): void
|
||||||
|
{
|
||||||
|
$this->login();
|
||||||
|
|
||||||
|
$this->client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/scenarios',
|
||||||
|
server: [
|
||||||
|
'CONTENT_TYPE' => 'application/ld+json',
|
||||||
|
'HTTP_ACCEPT' => 'application/ld+json',
|
||||||
|
],
|
||||||
|
content: json_encode([]),
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
422,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'POST /api/scenarios without a name must return 422 Unprocessable Entity (validator-rejected, not a 500).',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPostWithNonJsonContentTypeIsRejected(): void
|
||||||
|
{
|
||||||
|
$this->login();
|
||||||
|
|
||||||
|
$this->client->request(
|
||||||
|
'POST',
|
||||||
|
'/api/scenarios',
|
||||||
|
['name' => 'Holiday Fund'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
415,
|
||||||
|
$this->client->getResponse()->getStatusCode(),
|
||||||
|
'POST /api/scenarios without a JSON Content-Type must be rejected with 415 Unsupported Media Type.',
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->em->clear();
|
||||||
|
|
||||||
|
$persisted = $this->em->getRepository(Scenario::class)->findOneBy(['name' => 'Holiday Fund']);
|
||||||
|
|
||||||
|
self::assertNull(
|
||||||
|
$persisted,
|
||||||
|
'A rejected non-JSON POST must not persist a Scenario.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue