diff --git a/CHANGELOG.md b/CHANGELOG.md index 711d424..e0535c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## [1.4.0] - 2023-04-20 + +- Support general permission endpoints. Examples [here](examples/general/permissions.php) + ## [1.3.0] - 2023-04-13 - Support sandbox project endpoints. Examples [here](examples/sandbox/projects.php) diff --git a/examples/general/permissions.php b/examples/general/permissions.php new file mode 100644 index 0000000..b90dcc5 --- /dev/null +++ b/examples/general/permissions.php @@ -0,0 +1,62 @@ +general()->permissions()->getResources($accountId); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} + + +/** + * Manage user or token permissions + * + * If you send a combination of resource_type and resource_id that already exists, the permission is updated. + * If the combination doesn’t exist, the permission is created. + * + * PUT https://mailtrap.io/api/accounts/{account_id}/account_accesses/{account_access_id}/permissions/bulk + */ +try { + $accountId = getenv('MAILTRAP_ACCOUNT_ID'); + $accountAccessId = getenv('MAILTRAP_ACCOUNT_ACCESS_ID'); + + // resource IDs + $projectResourceId = getenv('MAILTRAP_NEW_PROJECT_RESOURCE_ID'); + $inboxResourceId = getenv('MAILTRAP_INBOX_RESOURCE_ID'); + $destroyProjectResourceId = getenv('MAILTRAP_OLD_PROJECT_RESOURCE_ID'); + + $permissions = new Permissions( + new CreateOrUpdatePermission($projectResourceId, PermissionInterface::TYPE_PROJECT, 10), // viewer = 10 + new CreateOrUpdatePermission($inboxResourceId, PermissionInterface::TYPE_INBOX, 100), // admin = 100 + new DestroyPermission($destroyProjectResourceId, PermissionInterface::TYPE_PROJECT), + ); + + $response = $mailtrap->general()->permissions()->update($accountId, $accountAccessId, $permissions); + + // print the response body (array) + var_dump(ResponseHelper::toArray($response)); +} catch (Exception $e) { + echo 'Caught exception: ', $e->getMessage(), "\n"; +} diff --git a/src/Api/General/Permission.php b/src/Api/General/Permission.php new file mode 100644 index 0000000..270f421 --- /dev/null +++ b/src/Api/General/Permission.php @@ -0,0 +1,64 @@ +handleResponse($this->httpGet( + sprintf('%s/api/accounts/%s/permissions/resources', $this->getHost(), $accountId) + )); + } + + /** + * Manage user or token permissions. + * If you send a combination of resource_type and resource_id that already exists, the permission is updated. + * If the combination doesn’t exist, the permission is created. + * + * @param int $accountId + * @param int $accountAccessId + * @param Permissions $permissions + * + * @return ResponseInterface + */ + public function update(int $accountId, int $accountAccessId, Permissions $permissions): ResponseInterface + { + return $this->handleResponse($this->httpPut( + sprintf('%s/api/accounts/%s/account_accesses/%s/permissions/bulk', $this->getHost(), $accountId, $accountAccessId), + [], + ['permissions' => $this->getPayload($permissions)] + )); + } + + private function getPayload(Permissions $permissions): array + { + $payload = []; + foreach ($permissions->getAll() as $permission) { + $payload[] = $permission->toArray(); + } + + if (count($payload) === 0) { + throw new RuntimeException('At least one "permission" object should be added to manage user or token'); + } + + return $payload; + } +} diff --git a/src/DTO/Request/Permission/CreateOrUpdatePermission.php b/src/DTO/Request/Permission/CreateOrUpdatePermission.php new file mode 100644 index 0000000..89f9bcd --- /dev/null +++ b/src/DTO/Request/Permission/CreateOrUpdatePermission.php @@ -0,0 +1,51 @@ +resourceId = (string) $resourceId; + $this->resourceType = $resourceType; + $this->accessLevel = (string) $accessLevel; + } + + public function getResourceId(): string + { + return $this->resourceId; + } + + public function getResourceType(): string + { + return $this->resourceType; + } + + public function getAccessLevel(): string + { + return $this->accessLevel; + } + + public function toArray(): array + { + return [ + 'resource_id' => $this->getResourceId(), + 'resource_type' => $this->getResourceType(), + 'access_level' => $this->getAccessLevel(), + ]; + } +} diff --git a/src/DTO/Request/Permission/DestroyPermission.php b/src/DTO/Request/Permission/DestroyPermission.php new file mode 100644 index 0000000..b992a5c --- /dev/null +++ b/src/DTO/Request/Permission/DestroyPermission.php @@ -0,0 +1,43 @@ +resourceId = (string) $resourceId; + $this->resourceType = $resourceType; + } + + public function getResourceId(): string + { + return $this->resourceId; + } + + public function getResourceType(): string + { + return $this->resourceType; + } + + public function toArray(): array + { + return [ + 'resource_id' => $this->getResourceId(), + 'resource_type' => $this->getResourceType(), + '_destroy' => true, + ]; + } +} diff --git a/src/DTO/Request/Permission/PermissionInterface.php b/src/DTO/Request/Permission/PermissionInterface.php new file mode 100644 index 0000000..c66cdb8 --- /dev/null +++ b/src/DTO/Request/Permission/PermissionInterface.php @@ -0,0 +1,33 @@ +add($permission); + } + } + + public function add(PermissionInterface $permission): Permissions + { + $this->permissions[] = $permission; + + return $this; + } + + /** + * @return PermissionInterface[] + */ + public function getAll(): array + { + return $this->permissions; + } +} diff --git a/src/MailtrapGeneralClient.php b/src/MailtrapGeneralClient.php index 13af9dd..4460a60 100644 --- a/src/MailtrapGeneralClient.php +++ b/src/MailtrapGeneralClient.php @@ -7,8 +7,9 @@ use Mailtrap\Api; /** - * @method Api\General\Account accounts - * @method Api\General\User users + * @method Api\General\Account accounts + * @method Api\General\User users + * @method Api\General\Permission permissions * * Class MailtrapGeneralClient */ @@ -17,5 +18,6 @@ final class MailtrapGeneralClient extends AbstractMailtrapClient public const API_MAPPING = [ 'accounts' => Api\General\Account::class, 'users' => Api\General\User::class, + 'permissions' => Api\General\Permission::class ]; } diff --git a/tests/Api/General/PermissionTest.php b/tests/Api/General/PermissionTest.php new file mode 100644 index 0000000..fd9b976 --- /dev/null +++ b/tests/Api/General/PermissionTest.php @@ -0,0 +1,227 @@ +permission = $this->getMockBuilder(Permission::class) + ->onlyMethods(['httpGet', 'httpPut']) + ->setConstructorArgs([$this->getConfigMock()]) + ->getMock() + ; + } + + protected function tearDown(): void + { + $this->permission = null; + + parent::tearDown(); + } + + public function testValidGetResources(): void + { + $this->permission->expects($this->once()) + ->method('httpGet') + ->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/permissions/resources') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode($this->getExpectedData()))); + + $response = $this->permission->getResources(self::FAKE_ACCOUNT_ID); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertCount(2, $responseData); + $this->assertArrayHasKey('resources', array_shift($responseData)); + } + + public function test401InvalidGetResources(): void + { + $this->permission->expects($this->once()) + ->method('httpGet') + ->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/permissions/resources') + ->willReturn(new Response(401, ['Content-Type' => 'application/json'], json_encode(['error' => 'Incorrect API token']))); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage( + 'Unauthorized. Make sure you are sending correct credentials with the request before retrying. Errors: Incorrect API token.' + ); + + $this->permission->getResources(self::FAKE_ACCOUNT_ID); + } + + public function test403InvalidGetResources(): void + { + $this->permission->expects($this->once()) + ->method('httpGet') + ->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/permissions/resources') + ->willReturn(new Response(403, ['Content-Type' => 'application/json'], json_encode(['errors' => 'Access forbidden']))); + + $this->expectException(HttpClientException::class); + $this->expectExceptionMessage( + 'Forbidden. Make sure domain verification process is completed or check your permissions. Errors: Access forbidden.' + ); + + $this->permission->getResources(self::FAKE_ACCOUNT_ID); + } + + /** + * @dataProvider validUpdateDataProvider + */ + public function testValidUpdate($permissions): void + { + $this->permission->expects($this->once()) + ->method('httpPut') + ->with(AbstractApi::DEFAULT_HOST . '/api/accounts/' . self::FAKE_ACCOUNT_ID . '/account_accesses/' . self::FAKE_ACCOUNT_ACCESS_ID . '/permissions/bulk') + ->willReturn(new Response(200, ['Content-Type' => 'application/json'], json_encode(['message' => 'Permissions have been updated!']))); + + $response = $this->permission->update(self::FAKE_ACCOUNT_ID, self::FAKE_ACCOUNT_ACCESS_ID, $permissions); + $responseData = ResponseHelper::toArray($response); + + $this->assertInstanceOf(Response::class, $response); + $this->assertArrayHasKey('message', $responseData); + $this->assertEquals('Permissions have been updated!', $responseData['message']); + } + + public function testInvalidUpdate(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage( + 'At least one "permission" object should be added to manage user or token' + ); + + $emptyPermissions = new Permissions(); + $this->permission->update(self::FAKE_ACCOUNT_ID, self::FAKE_ACCOUNT_ACCESS_ID, $emptyPermissions); + } + + /** + * @dataProvider validUpdateDataProvider + */ + public function testValidGetPayload($permissions, $expectedResult): void + { + $method = new \ReflectionMethod(Permission::class, 'getPayload'); + $method->setAccessible(true); + $payload = $method->invoke(new Permission($this->getConfigMock()), $permissions); + + $this->assertEquals($expectedResult, $payload); + } + + public function validUpdateDataProvider(): iterable + { + // create/update + yield [ + new Permissions( + new CreateOrUpdatePermission(1000001, PermissionInterface::TYPE_PROJECT, 10), + ), + [ + [ + "resource_id" => "1000001", + "resource_type" => "project", + "access_level" => "10" + ] + ] + ]; + + // destroy + yield [ + new Permissions( + new DestroyPermission(1000009, PermissionInterface::TYPE_PROJECT), + ), + [ + [ + "resource_id" => "1000009", + "resource_type" => "project", + "_destroy" => true + ] + ] + ]; + + // create/update and destroy together + yield [ + new Permissions( + new CreateOrUpdatePermission(1000001, PermissionInterface::TYPE_PROJECT, 10), + new CreateOrUpdatePermission(2000002, PermissionInterface::TYPE_INBOX, 100), + new DestroyPermission(1000009, PermissionInterface::TYPE_PROJECT), + ), + [ + [ + "resource_id" => "1000001", + "resource_type" => "project", + "access_level" => "10" + ], + [ + "resource_id" => "2000002", + "resource_type" => "inbox", + "access_level" => "100" + ], + [ + "resource_id" => "1000009", + "resource_type" => "project", + "_destroy" => true + ] + ] + ]; + } + + private function getExpectedData() + { + return [ + [ + "id" => 4001, + "name" => "My First Project", + "type" => "project", + "access_level" => 1, + "resources" => [ + [ + "id" => 3816, + "name" => "My First Inbox", + "type" => "inbox", + "access_level" => 100, + "resources" => [ + ] + ] + ] + ], + [ + "id" => 4002, + "name" => "My Second Project", + "type" => "project", + "access_level" => 1, + "resources" => [ + [ + "id" => 3820, + "name" => "My Second Inbox", + "type" => "inbox", + "access_level" => 100, + "resources" => [ + ] + ] + ] + ] + ]; + } +}