diff --git a/config/api_platform/OgLive.yaml b/config/api_platform/OgLive.yaml index e69de29..5a52f02 100644 --- a/config/api_platform/OgLive.yaml +++ b/config/api_platform/OgLive.yaml @@ -0,0 +1,31 @@ +resources: + App\Entity\OgLive: + processor: App\State\Processor\OgLiveProcessor + input: App\Dto\Input\OgLiveInput + output: App\Dto\Output\OgLiveOutput + normalizationContext: + groups: ['default', 'og-live:read'] + denormalizationContext: + groups: ['og-live:write'] + operations: + ApiPlatform\Metadata\GetCollection: + provider: App\State\Provider\OgLiveProvider + filters: + - 'api_platform.filter.og-live.order' + - 'api_platform.filter.og-live.search' + + ApiPlatform\Metadata\Get: + provider: App\State\Provider\OgLiveProvider + ApiPlatform\Metadata\Put: + provider: App\State\Provider\OgLiveProvider + ApiPlatform\Metadata\Patch: + provider: App\State\Provider\OgLiveProvider + ApiPlatform\Metadata\Post: ~ + ApiPlatform\Metadata\Delete: ~ + +properties: + App\Entity\OgLive: + id: + identifier: false + uuid: + identifier: true \ No newline at end of file diff --git a/config/services.yaml b/config/services.yaml index beee276..c3267cb 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -101,3 +101,8 @@ services: bind: $collectionProvider: '@api_platform.doctrine.orm.state.collection_provider' $itemProvider: '@api_platform.doctrine.orm.state.item_provider' + + App\State\Provider\OgLiveProvider: + bind: + $collectionProvider: '@api_platform.doctrine.orm.state.collection_provider' + $itemProvider: '@api_platform.doctrine.orm.state.item_provider' diff --git a/src/Dto/Input/OgLiveInput.php b/src/Dto/Input/OgLiveInput.php index 4f8d788..d9e86df 100644 --- a/src/Dto/Input/OgLiveInput.php +++ b/src/Dto/Input/OgLiveInput.php @@ -2,7 +2,41 @@ namespace App\Dto\Input; -class OgLiveInput -{ +use ApiPlatform\Metadata\ApiProperty; +use App\Entity\OgLive; +use Symfony\Component\Serializer\Annotation\Groups; +use Symfony\Component\Validator\Constraints as Assert; +final class OgLiveInput +{ + #[Assert\NotBlank(message: 'validators.hardware.name.not_blank')] + #[Groups(['og-live:write'])] + #[ApiProperty(description: 'The name of the ogLive', example: "OgLive 1")] + public ?string $name = null; + + #[Groups(['og-live:write'])] + #[ApiProperty(description: 'The download url of the ogLive', example: "http://example.com/oglive1.iso")] + public ?string $downloadUrl = null; + + public function __construct(?OgLive $ogLive = null) + { + if (!$ogLive) { + return; + } + + $this->name = $ogLive->getName(); + $this->downloadUrl = $ogLive->getDownloadUrl(); + } + + public function createOrUpdateEntity(?OgLive $ogLive = null): OgLive + { + if (!$ogLive) { + $ogLive = new OgLive(); + } + + $ogLive->setName($this->name); + $ogLive->setDownloadUrl($this->downloadUrl); + + return $ogLive; + } } \ No newline at end of file diff --git a/src/Dto/Output/OgLiveOutput.php b/src/Dto/Output/OgLiveOutput.php index 7f55a7a..1b15405 100644 --- a/src/Dto/Output/OgLiveOutput.php +++ b/src/Dto/Output/OgLiveOutput.php @@ -2,7 +2,33 @@ namespace App\Dto\Output; -class OgLiveOutput -{ +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use App\Entity\OgLive; +use Symfony\Component\Serializer\Annotation\Groups; +#[Get(shortName: 'OgLive')] +final class OgLiveOutput extends AbstractOutput +{ + #[Groups(['og-live:read'])] + public string $name; + + #[Groups(['og-live:read'])] + public ?string $downloadUrl = ''; + + #[Groups(['og-live:read'])] + public \DateTime $createdAt; + + #[Groups(['og-live:read'])] + public ?string $createdBy = null; + + public function __construct(OgLive $ogLive) + { + parent::__construct($ogLive); + + $this->name = $ogLive->getName(); + $this->downloadUrl = $ogLive->getDownloadUrl(); + $this->createdAt = $ogLive->getCreatedAt(); + $this->createdBy = $ogLive->getCreatedBy(); + } } \ No newline at end of file diff --git a/src/State/Processor/OgLiveProcessor.php b/src/State/Processor/OgLiveProcessor.php index dff06bf..0cc9bc3 100644 --- a/src/State/Processor/OgLiveProcessor.php +++ b/src/State/Processor/OgLiveProcessor.php @@ -2,7 +2,67 @@ namespace App\State\Processor; -class OgLiveProcessor -{ +use ApiPlatform\Metadata\Delete; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Post; +use ApiPlatform\Metadata\Put; +use ApiPlatform\State\ProcessorInterface; +use ApiPlatform\Validator\ValidatorInterface; +use App\Dto\Input\OgLiveInput; +use App\Dto\Output\OgLiveOutput; +use App\Repository\OgLiveRepository; -} \ No newline at end of file +readonly class OgLiveProcessor implements ProcessorInterface +{ + public function __construct( + private OgLiveRepository $ogLiveRepository, + private ValidatorInterface $validator + ) + { + } + + /** + * @throws \Exception + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): OgLiveOutput|null + { + switch ($operation){ + case $operation instanceof Post: + case $operation instanceof Put: + case $operation instanceof Patch: + return $this->processCreateOrUpdate($data, $operation, $uriVariables, $context); + case $operation instanceof Delete: + return $this->processDelete($data, $operation, $uriVariables, $context); + } + } + + /** + * @throws \Exception + */ + private function processCreateOrUpdate($data, Operation $operation, array $uriVariables = [], array $context = []): OgLiveOutput + { + if (!($data instanceof OgLiveInput)) { + throw new \Exception(sprintf('data is not instance of %s', OgLiveInput::class)); + } + + $entity = null; + if (isset($uriVariables['uuid'])) { + $entity = $this->ogLiveRepository->findOneByUuid($uriVariables['uuid']); + } + + $ogLive = $data->createOrUpdateEntity($entity); + $this->validator->validate($ogLive); + $this->ogLiveRepository->save($ogLive); + + return new OgLiveOutput($ogLive); + } + + private function processDelete($data, Operation $operation, array $uriVariables = [], array $context = []): null + { + $ogLive = $this->ogLiveRepository->findOneByUuid($uriVariables['uuid']); + $this->ogLiveRepository->delete($ogLive); + + return null; + } +} diff --git a/src/State/Provider/OgLiveProvider.php b/src/State/Provider/OgLiveProvider.php index 23f51e0..cf1bcc5 100644 --- a/src/State/Provider/OgLiveProvider.php +++ b/src/State/Provider/OgLiveProvider.php @@ -2,7 +2,70 @@ namespace App\State\Provider; -class OgLiveProvider -{ +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operation; +use ApiPlatform\Metadata\Patch; +use ApiPlatform\Metadata\Put; +use ApiPlatform\State\Pagination\TraversablePaginator; +use ApiPlatform\State\ProviderInterface; +use App\Dto\Input\OgLiveInput; +use App\Dto\Output\OgLiveOutput; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -} \ No newline at end of file +readonly class OgLiveProvider implements ProviderInterface +{ + public function __construct( + private ProviderInterface $collectionProvider, + private ProviderInterface $itemProvider + ) + { + } + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + switch ($operation){ + case $operation instanceof GetCollection: + return $this->provideCollection($operation, $uriVariables, $context); + case $operation instanceof Patch: + case $operation instanceof Put: + return $this->provideInput($operation, $uriVariables, $context); + case $operation instanceof Get: + return $this->provideItem($operation, $uriVariables, $context); + } + } + + private function provideCollection(Operation $operation, array $uriVariables = [], array $context = []): object + { + $paginator = $this->collectionProvider->provide($operation, $uriVariables, $context); + + $items = new \ArrayObject(); + foreach ($paginator->getIterator() as $item){ + $items[] = new OgLiveOutput($item); + } + + return new TraversablePaginator($items, $paginator->getCurrentPage(), $paginator->getItemsPerPage(), $paginator->getTotalItems()); + } + + public function provideItem(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + $item = $this->itemProvider->provide($operation, $uriVariables, $context); + + if (!$item) { + throw new NotFoundHttpException('OgLive not found'); + } + + return new OgLiveOutput($item); + } + + public function provideInput(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + { + if (isset($uriVariables['uuid'])) { + $item = $this->itemProvider->provide($operation, $uriVariables, $context); + + return $item !== null ? new OgLiveInput($item) : null; + } + + return new OgLiveInput(); + } +} diff --git a/tests/Functional/OgLiveTest.php b/tests/Functional/OgLiveTest.php index e3dc1d5..bb6c58a 100644 --- a/tests/Functional/OgLiveTest.php +++ b/tests/Functional/OgLiveTest.php @@ -2,7 +2,123 @@ namespace Functional; -class OgLiveTest -{ +use App\Entity\Client; +use App\Entity\HardwareProfile; +use App\Entity\OgLive; +use App\Entity\User; +use App\Factory\HardwareProfileFactory; +use App\Factory\OgLiveFactory; +use App\Factory\UserFactory; +use App\Model\UserGroupPermissions; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; +use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; +class OgLiveTest extends AbstractTest +{ + CONST string USER_ADMIN = 'ogadmin'; + CONST string OGLIVE_CREATE = 'test-oglive-create'; + CONST string OGLIVE_UPDATE = 'test-oglive-update'; + CONST string OGLIVE_DELETE = 'test-oglive-delete'; + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testGetCollectionOgLives(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]); + + OgLiveFactory::createMany(10); + + $this->createClientWithCredentials()->request('GET', '/og-lives'); + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + $this->assertJsonContains([ + '@context' => '/contexts/OgLive', + '@id' => '/og-lives', + '@type' => 'hydra:Collection', + 'hydra:totalItems' => 10, + ]); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testCreateOgLive(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]); + + $this->createClientWithCredentials()->request('POST', '/og-lives',['json' => [ + 'name' => self::OGLIVE_CREATE, + 'downloadUrl' => 'http://example.com' + ]]); + + $this->assertResponseStatusCodeSame(201); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + $this->assertJsonContains([ + '@context' => '/contexts/OgLiveOutput', + '@type' => 'OgLive', + 'name' => self::OGLIVE_CREATE, + 'downloadUrl' => 'http://example.com' + ]); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testUpdateOgLive(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]); + + OgLiveFactory::createOne(['name' => self::OGLIVE_CREATE, 'downloadUrl' => 'http://example.com']); + $iri = $this->findIriBy(OgLive::class, ['name' => self::OGLIVE_CREATE]); + + $this->createClientWithCredentials()->request('PUT', $iri, ['json' => [ + 'name' => self::OGLIVE_UPDATE, + 'downloadUrl' => 'http://example-2.com', + ]]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + '@id' => $iri, + 'name' => self::OGLIVE_UPDATE, + 'downloadUrl' => 'http://example-2.com', + ]); + } + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + */ + public function testDeleteOgLive(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN, 'roles'=> [UserGroupPermissions::ROLE_SUPER_ADMIN]]); + + OgLiveFactory::createOne(['name' => self::OGLIVE_CREATE, 'downloadUrl' => 'http://example.com']); + $iri = $this->findIriBy(OgLive::class, ['name' => self::OGLIVE_CREATE]); + + $this->createClientWithCredentials()->request('DELETE', $iri); + $this->assertResponseStatusCodeSame(204); + $this->assertNull( + static::getContainer()->get('doctrine')->getRepository(OgLive::class)->findOneBy(['name' => self::OGLIVE_CREATE]) + ); + } } \ No newline at end of file diff --git a/translations/validators.en.yaml b/translations/validators.en.yaml index e5b6e69..ad9b5aa 100644 --- a/translations/validators.en.yaml +++ b/translations/validators.en.yaml @@ -36,4 +36,9 @@ validators: operative_system: name: - not_blank: 'The name should not be blank.' \ No newline at end of file + not_blank: 'The name should not be blank.' + + og_live: + name: + not_blank: 'The name should not be blank.' + unique: 'The name should be unique.'