From 5b4248bfda73ae4e96a381eeb751467f38790756 Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Mon, 27 May 2024 15:55:41 +0200 Subject: [PATCH] refs #377. Test UserGroups --- config/api_platform/User.yaml | 1 + config/api_platform/UserGroup.yaml | 14 ++- config/services.yaml | 6 + config/services/api_platform.yaml | 12 ++ migrations/Version20240527103936.php | 33 +++++ src/Command/LoadDefaultUserGroupsCommand.php | 58 +++++++++ src/DataFixtures/AppFixtures.php | 23 ++++ src/Dto/Input/UserGroupInput.php | 25 +++- src/Dto/Input/UserInput.php | 6 +- src/Dto/Output/AbstractOutput.php | 7 +- src/Dto/Output/UserGroupOutput.php | 38 ++++++ src/Entity/UserGroup.php | 13 +- src/Factory/UserGroupFactory.php | 45 +++++++ src/Repository/UserGroupRepository.php | 27 +--- src/State/Processor/UserGroupProcessor.php | 69 ++++++++++ src/State/Provider/UserGroupProvider.php | 71 +++++++++++ tests/Functional/UserGroupTest.php | 125 +++++++++++++++++++ tests/Functional/UserTest.php | 14 +-- 18 files changed, 538 insertions(+), 49 deletions(-) create mode 100644 migrations/Version20240527103936.php create mode 100644 src/Command/LoadDefaultUserGroupsCommand.php create mode 100644 src/Dto/Output/UserGroupOutput.php create mode 100644 src/Factory/UserGroupFactory.php create mode 100644 src/State/Processor/UserGroupProcessor.php create mode 100644 src/State/Provider/UserGroupProvider.php create mode 100644 tests/Functional/UserGroupTest.php diff --git a/config/api_platform/User.yaml b/config/api_platform/User.yaml index f71e14b..8a75827 100644 --- a/config/api_platform/User.yaml +++ b/config/api_platform/User.yaml @@ -13,6 +13,7 @@ resources: filters: - 'api_platform.filter.user.order' - 'api_platform.filter.user.search' + - 'api_platform.filter.user.boolean' ApiPlatform\Metadata\Get: provider: App\State\Provider\UserProvider ApiPlatform\Metadata\Put: diff --git a/config/api_platform/UserGroup.yaml b/config/api_platform/UserGroup.yaml index 9602c01..0e25cb8 100644 --- a/config/api_platform/UserGroup.yaml +++ b/config/api_platform/UserGroup.yaml @@ -1,17 +1,25 @@ resources: App\Entity\UserGroup: + processor: App\State\Processor\UserGroupProcessor + input: App\Dto\Input\UserGroupInput + output: App\Dto\Output\UserGroupOutput normalization_context: groups: ['default', 'user-group:read'] denormalization_context: groups: ['user-group:write'] operations: ApiPlatform\Metadata\GetCollection: + provider: App\State\Provider\UserGroupProvider filters: - 'api_platform.filter.user_group.order' - 'api_platform.filter.user_group.search' - ApiPlatform\Metadata\Get: ~ - ApiPlatform\Metadata\Put: ~ - ApiPlatform\Metadata\Patch: ~ + - 'api_platform.filter.user_group.boolean' + ApiPlatform\Metadata\Get: + provider: App\State\Provider\UserGroupProvider + ApiPlatform\Metadata\Put: + provider: App\State\Provider\UserGroupProvider + ApiPlatform\Metadata\Patch: + provider: App\State\Provider\UserGroupProvider ApiPlatform\Metadata\Post: ~ ApiPlatform\Metadata\Delete: ~ diff --git a/config/services.yaml b/config/services.yaml index e5006be..af7a6a0 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -29,3 +29,9 @@ services: bind: $collectionProvider: '@api_platform.doctrine.orm.state.collection_provider' $itemProvider: '@api_platform.doctrine.orm.state.item_provider' + + App\State\Provider\UserGroupProvider: + bind: + $collectionProvider: '@api_platform.doctrine.orm.state.collection_provider' + $itemProvider: '@api_platform.doctrine.orm.state.item_provider' + diff --git a/config/services/api_platform.yaml b/config/services/api_platform.yaml index f735df2..8049096 100644 --- a/config/services/api_platform.yaml +++ b/config/services/api_platform.yaml @@ -13,6 +13,12 @@ services: tags: - ['api_platform.filter' ] + api_platform.filter.user.boolean: + parent: 'api_platform.doctrine.orm.boolean_filter' + arguments: [ { 'enabled': ~ } ] + tags: + - [ 'api_platform.filter' ] + api_platform.filter.user_group.order: parent: 'api_platform.doctrine.orm.order_filter' arguments: @@ -24,5 +30,11 @@ services: api_platform.filter.user_group.search: parent: 'api_platform.doctrine.orm.search_filter' arguments: [ { 'id': 'exact', 'name': 'partial' } ] + tags: + - [ 'api_platform.filter' ] + + api_platform.filter.user_group.boolean: + parent: 'api_platform.doctrine.orm.boolean_filter' + arguments: [ { 'enabled': ~ } ] tags: - [ 'api_platform.filter' ] \ No newline at end of file diff --git a/migrations/Version20240527103936.php b/migrations/Version20240527103936.php new file mode 100644 index 0000000..802e375 --- /dev/null +++ b/migrations/Version20240527103936.php @@ -0,0 +1,33 @@ +addSql('ALTER TABLE user_group CHANGE roles permissions JSON NOT NULL COMMENT \'(DC2Type:json)\''); + $this->addSql('CREATE UNIQUE INDEX UNIQ_IDENTIFIER_NAME ON user_group (name)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('DROP INDEX UNIQ_IDENTIFIER_NAME ON user_group'); + $this->addSql('ALTER TABLE user_group CHANGE permissions roles JSON NOT NULL COMMENT \'(DC2Type:json)\''); + } +} diff --git a/src/Command/LoadDefaultUserGroupsCommand.php b/src/Command/LoadDefaultUserGroupsCommand.php new file mode 100644 index 0000000..a4bdda2 --- /dev/null +++ b/src/Command/LoadDefaultUserGroupsCommand.php @@ -0,0 +1,58 @@ + 'Super Admin', + 'permissions' => ['ROLE_SUPER_ADMIN'], + 'enabled' => true + ], + [ + 'name' => 'Administrador de aulas', + 'permissions' => ['ROLE_ORGANIZATIONAL_UNIT_ADMIN'], + 'enabled' => true + ], + [ + 'name' => 'Operador de aulas', + 'permissions' => ['ROLE_ORGANIZATIONAL_UNIT_OPERATOR'], + 'enabled' => true + ], + [ + 'name' => 'Usuario', + 'permissions' => ['ROLE_USER'], + 'enabled' => true + ], + ]; + + foreach ($userGroups as $userGroup) { + $entity = new UserGroup(); + $entity->setName($userGroup['name']); + $entity->setPermissions($userGroup['permissions']); + $entity->setEnabled($userGroup['enabled']); + + $this->entityManager->persist($entity); + } + + $this->entityManager->flush(); + + return 1; + } +} \ No newline at end of file diff --git a/src/DataFixtures/AppFixtures.php b/src/DataFixtures/AppFixtures.php index 8a74d9b..ded5655 100644 --- a/src/DataFixtures/AppFixtures.php +++ b/src/DataFixtures/AppFixtures.php @@ -5,13 +5,36 @@ namespace App\DataFixtures; use App\Factory\UserFactory; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\HttpKernel\KernelInterface; class AppFixtures extends Fixture { CONST ADMIN_USER = 'ogadmin'; + + public function __construct( + private readonly KernelInterface $kernel, + ) + { + } + + /** + * @throws \Exception + */ public function load(ObjectManager $manager): void { UserFactory::createOne(['username' => self::ADMIN_USER]); + + $application = new Application($this->kernel); + + $input = new ArrayInput([ + 'command' => 'app:load-default-user-groups' + ]); + + $output = new BufferedOutput(); + $application->run($input, $output); } } diff --git a/src/Dto/Input/UserGroupInput.php b/src/Dto/Input/UserGroupInput.php index 8902d95..d26ef2a 100644 --- a/src/Dto/Input/UserGroupInput.php +++ b/src/Dto/Input/UserGroupInput.php @@ -13,20 +13,33 @@ final class UserGroupInput public ?string $name = null; #[Groups(['user-group:write'])] - public array $roles = []; + public array $permissions = []; #[Assert\NotNull] #[Groups(['user-group:write'])] public ?bool $enabled = false; - public function __construct(?UserGroup $userGroup = null) + public function __construct(?UserGroup $userGroupGroup = null) { - if (!$userGroup) { + if (!$userGroupGroup) { return; } - $this->name = $userGroup->getName(); - $this->roles = $userGroup->getRoles(); - $this->enabled= $userGroup->isEnabled(); + $this->name = $userGroupGroup->getName(); + $this->permissions = $userGroupGroup->getPermissions(); + $this->enabled= $userGroupGroup->isEnabled(); + } + + public function createOrUpdateEntity(?UserGroup $userGroup = null): UserGroup + { + if (!$userGroup) { + $userGroup = new UserGroup(); + } + + $userGroup->setName($this->name); + $userGroup->setPermissions($this->permissions); + $userGroup->setEnabled($this->enabled); + + return $userGroup; } } \ No newline at end of file diff --git a/src/Dto/Input/UserInput.php b/src/Dto/Input/UserInput.php index 6b7c2af..ce9e9a4 100644 --- a/src/Dto/Input/UserInput.php +++ b/src/Dto/Input/UserInput.php @@ -60,7 +60,11 @@ final class UserInput $user->setUsername($this->username); $user->setRoles($this->roles); $user->setEnabled($this->enabled); - $user->setUserGroups($this->userGroups); + + foreach ($this->userGroups as $userGroup) { + $userGroupsToAdd[] = $userGroup->getEntity(); + } + $user->setUserGroups( $userGroupsToAdd ?? [] ); if ($this->password !== null) { $user->setPassword($this->password); diff --git a/src/Dto/Output/AbstractOutput.php b/src/Dto/Output/AbstractOutput.php index b5b0c56..2ca73e0 100644 --- a/src/Dto/Output/AbstractOutput.php +++ b/src/Dto/Output/AbstractOutput.php @@ -17,9 +17,14 @@ abstract class AbstractOutput #[Groups(['default'])] public int $id; - public function __construct(AbstractEntity $entity) + public function __construct(private readonly AbstractEntity $entity) { $this->uuid = $entity->getUuid(); $this->id = $entity->getId(); } + + public function getEntity(): AbstractEntity + { + return $this->entity; + } } \ No newline at end of file diff --git a/src/Dto/Output/UserGroupOutput.php b/src/Dto/Output/UserGroupOutput.php new file mode 100644 index 0000000..1b1308d --- /dev/null +++ b/src/Dto/Output/UserGroupOutput.php @@ -0,0 +1,38 @@ +name = $userGroup->getName(); + $this->permissions = $userGroup->getPermissions(); + $this->enabled = $userGroup->isEnabled(); + $this->createAt = $userGroup->getCreatedAt(); + $this->createBy = $userGroup->getCreatedBy(); + } +} \ No newline at end of file diff --git a/src/Entity/UserGroup.php b/src/Entity/UserGroup.php index b6c47c3..32c8ce4 100644 --- a/src/Entity/UserGroup.php +++ b/src/Entity/UserGroup.php @@ -7,8 +7,11 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; +use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; #[ORM\Entity(repositoryClass: UserGroupRepository::class)] +#[ORM\UniqueConstraint(name: 'UNIQ_IDENTIFIER_NAME', fields: ['name'])] +#[UniqueEntity(fields: ['name'], message: 'There is already an role with this name')] class UserGroup extends AbstractEntity { use ToggleableTrait; @@ -17,7 +20,7 @@ class UserGroup extends AbstractEntity private ?string $name = null; #[ORM\Column(type: Types::JSON)] - private array $roles = []; + private array $permissions = []; /** * @var Collection @@ -44,14 +47,14 @@ class UserGroup extends AbstractEntity return $this; } - public function getRoles(): array + public function getPermissions(): array { - return $this->roles; + return $this->permissions; } - public function setRoles(array $roles): static + public function setPermissions(array $permissions): static { - $this->roles = $roles; + $this->permissions = $permissions; return $this; } diff --git a/src/Factory/UserGroupFactory.php b/src/Factory/UserGroupFactory.php new file mode 100644 index 0000000..30df222 --- /dev/null +++ b/src/Factory/UserGroupFactory.php @@ -0,0 +1,45 @@ + + */ +final class UserGroupFactory extends ModelFactory +{ + public function __construct() + { + parent::__construct(); + } + + + protected function getDefaults(): array + { + return [ + 'createdAt' => self::faker()->dateTime(), + 'permissions' => [], + 'updatedAt' => self::faker()->dateTime(), + ]; + } + + /** + * @see https://symfony.com/bundles/ZenstruckFoundryBundle/current/index.html#initialization + */ + protected function initialize(): self + { + return $this + // ->afterInstantiate(function(UserGroup $userGroup): void {}) + ; + } + + protected static function getClass(): string + { + return UserGroup::class; + } +} diff --git a/src/Repository/UserGroupRepository.php b/src/Repository/UserGroupRepository.php index 33ef735..64fec99 100644 --- a/src/Repository/UserGroupRepository.php +++ b/src/Repository/UserGroupRepository.php @@ -9,35 +9,10 @@ use Doctrine\Persistence\ManagerRegistry; /** * @extends ServiceEntityRepository */ -class UserGroupRepository extends ServiceEntityRepository +class UserGroupRepository extends AbstractRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, UserGroup::class); } - - // /** - // * @return UserGroup[] Returns an array of UserGroup objects - // */ - // public function findByExampleField($value): array - // { - // return $this->createQueryBuilder('u') - // ->andWhere('u.exampleField = :val') - // ->setParameter('val', $value) - // ->orderBy('u.id', 'ASC') - // ->setMaxResults(10) - // ->getQuery() - // ->getResult() - // ; - // } - - // public function findOneBySomeField($value): ?UserGroup - // { - // return $this->createQueryBuilder('u') - // ->andWhere('u.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } } diff --git a/src/State/Processor/UserGroupProcessor.php b/src/State/Processor/UserGroupProcessor.php new file mode 100644 index 0000000..0dfdaea --- /dev/null +++ b/src/State/Processor/UserGroupProcessor.php @@ -0,0 +1,69 @@ +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 = []): UserGroupOutput + { + if (!($data instanceof UserGroupInput)) { + throw new \Exception(sprintf('data is not instance of %s', UserGroupInput::class)); + } + + $entity = null; + if (isset($uriVariables['uuid'])) { + $entity = $this->userGroupRepository->findOneByUuid($uriVariables['uuid']); + } + + $userGroup = $data->createOrUpdateEntity($entity); + $this->validator->validate($userGroup); + $this->userGroupRepository->save($userGroup); + + return new UserGroupOutput($userGroup); + } + + private function processDelete($data, Operation $operation, array $uriVariables = [], array $context = []): null + { + $user = $this->userGroupRepository->findOneByUuid($uriVariables['uuid']); + $this->userGroupRepository->delete($user); + + return null; + } +} diff --git a/src/State/Provider/UserGroupProvider.php b/src/State/Provider/UserGroupProvider.php new file mode 100644 index 0000000..2bf73a9 --- /dev/null +++ b/src/State/Provider/UserGroupProvider.php @@ -0,0 +1,71 @@ +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|array|null + { + $paginator = $this->collectionProvider->provide($operation, $uriVariables, $context); + + $items = new \ArrayObject(); + foreach ($paginator->getIterator() as $item){ + $items[] = new UserGroupOutput($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('User Group not found'); + } + + return new UserGroupOutput($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 UserGroupInput($item) : null; + } + + return new UserGroupInput(); + } +} diff --git a/tests/Functional/UserGroupTest.php b/tests/Functional/UserGroupTest.php new file mode 100644 index 0000000..5806634 --- /dev/null +++ b/tests/Functional/UserGroupTest.php @@ -0,0 +1,125 @@ + self::USER_ADMIN]); + + UserGroupFactory::createOne(['name' => 'Super Admin', 'permissions' => ['ROLE_SUPER_ADMIN'], 'enabled' => true]); + UserGroupFactory::createOne(['name' => 'Administrador de aulas', 'permissions' => ['ROLE_ORGANIZATIONAL_UNIT_ADMIN'], 'enabled' => true]); + UserGroupFactory::createOne(['name' => 'Operador de aulas', 'permissions' => ['ROLE_ORGANIZATIONAL_UNIT_OPERATOR'], 'enabled' => true]); + UserGroupFactory::createOne(['name' => 'Usuario', 'permissions' => ['ROLE_USER'], 'enabled' => true]); + + $this->createClientWithCredentials()->request('GET', '/api/user-groups'); + $this->assertResponseStatusCodeSame(Response::HTTP_OK); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + $this->assertJsonContains([ + '@context' => '/api/contexts/UserGroup', + '@id' => '/api/user-groups', + '@type' => 'hydra:Collection', + 'hydra:totalItems' => 4, + ]); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testCreateUserGroup(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN]); + $this->createClientWithCredentials()->request('POST', '/api/user-groups',['json' => [ + 'name' => self::USER_GROUP_CREATE, + 'enabled' => true, + ]]); + + $this->assertResponseStatusCodeSame(201); + $this->assertResponseHeaderSame('content-type', 'application/ld+json; charset=utf-8'); + $this->assertJsonContains([ + '@context' => '/api/contexts/UserGroupOutput', + '@type' => 'UserGroup', + 'name' => self::USER_GROUP_CREATE, + 'enabled' => true, + ]); + } + + /** + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + */ + public function testUpdateUserGroup(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN]); + + UserGroupFactory::createOne(['name' => self::USER_GROUP_UPDATE]); + $iri = $this->findIriBy(UserGroup::class, ['name' => self::USER_GROUP_UPDATE]); + + $this->createClientWithCredentials()->request('PATCH', $iri, ['json' => [ + 'name' => self::USER_GROUP_UPDATE, + 'enabled' => false + ]]); + + $this->assertResponseIsSuccessful(); + $this->assertJsonContains([ + '@id' => $iri, + 'name' => self::USER_GROUP_UPDATE, + 'enabled' => false + ]); + } + + + /** + * @throws TransportExceptionInterface + * @throws ServerExceptionInterface + * @throws RedirectionExceptionInterface + * @throws DecodingExceptionInterface + * @throws ClientExceptionInterface + */ + public function testDeleteUser(): void + { + UserFactory::createOne(['username' => self::USER_ADMIN]); + UserGroupFactory::createOne(['name' => self::USER_GROUP_DELETE]); + + $iri = $this->findIriBy(UserGroup::class, ['name' => self::USER_GROUP_DELETE]); + + $this->createClientWithCredentials()->request('DELETE', $iri); + $this->assertResponseStatusCodeSame(204); + $this->assertNull( + static::getContainer()->get('doctrine')->getRepository(UserGroup::class)->findOneBy(['name' => self::USER_GROUP_DELETE]) + ); + } +} \ No newline at end of file diff --git a/tests/Functional/UserTest.php b/tests/Functional/UserTest.php index 8b83430..b0e9cc4 100644 --- a/tests/Functional/UserTest.php +++ b/tests/Functional/UserTest.php @@ -14,9 +14,9 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; class UserTest extends AbstractTest { CONST USER_ADMIN = 'ogadmin'; - CONST USER_CREATE = 'test-create'; - CONST USER_UPDATE = 'test-update'; - CONST USER_DELETE = 'test-delete'; + CONST USER_CREATE = 'test-user-create'; + CONST USER_UPDATE = 'test-user-update'; + CONST USER_DELETE = 'test-user-delete'; /** * @throws RedirectionExceptionInterface @@ -25,7 +25,7 @@ class UserTest extends AbstractTest * @throws TransportExceptionInterface * @throws ServerExceptionInterface */ - public function testGetCollection(): void + public function testGetCollectionUser(): void { UserFactory::createOne(['username' => self::USER_ADMIN]); UserFactory::createMany(10); @@ -48,7 +48,7 @@ class UserTest extends AbstractTest * @throws TransportExceptionInterface * @throws ServerExceptionInterface */ - public function testCreate(): void + public function testCreateUser(): void { UserFactory::createOne(['username' => self::USER_ADMIN]); $this->createClientWithCredentials()->request('POST', '/api/users',['json' => [ @@ -74,7 +74,7 @@ class UserTest extends AbstractTest * @throws TransportExceptionInterface * @throws ServerExceptionInterface */ - public function testUpdateBook(): void + public function testUpdateUser(): void { UserFactory::createOne(['username' => self::USER_ADMIN]); UserFactory::createOne(['username' => self::USER_UPDATE]); @@ -99,7 +99,7 @@ class UserTest extends AbstractTest * @throws DecodingExceptionInterface * @throws ClientExceptionInterface */ - public function testDeleteBook(): void + public function testDeleteUser(): void { UserFactory::createOne(['username' => self::USER_ADMIN]); UserFactory::createOne(['username' => self::USER_DELETE]);