diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index eb2530e2..7215c075 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - php: ['7.4', '8.0', '8.1', '8.2', '8.3'] + php: ['8.1', '8.2', '8.3'] steps: - name: Checkout Code diff --git a/composer.json b/composer.json index 96efc599..6e8337d0 100644 --- a/composer.json +++ b/composer.json @@ -26,7 +26,7 @@ } ], "require": { - "php": "^7.4.15 || ^8.0.2", + "php": "^8.1", "ext-json": "*", "ext-xml": "*", "php-http/cache-plugin": "^1.8.1", diff --git a/src/Api/AbstractApi.php b/src/Api/AbstractApi.php index c7026135..2f8c8dcb 100644 --- a/src/Api/AbstractApi.php +++ b/src/Api/AbstractApi.php @@ -253,6 +253,17 @@ protected function getProjectPath($id, string $uri): string return 'projects/'.self::encodePath($id).'/'.$uri; } + /** + * @param string|int $id + * @param string $uri + * + * @return string + */ + protected function getGroupPath(string|int $id, string $uri): string + { + return 'groups/'.self::encodePath($id).'/'.$uri; + } + /** * Create a new OptionsResolver with page and per_page options. * diff --git a/src/Api/Groups.php b/src/Api/Groups.php index 5a97e0cf..d8dcc03c 100644 --- a/src/Api/Groups.php +++ b/src/Api/Groups.php @@ -982,6 +982,98 @@ public function deleteDeployToken($group_id, int $token_id) return $this->delete('groups/'.self::encodePath($group_id).'/deploy_tokens/'.self::encodePath($token_id)); } + /** + * @param string|int $group_id + * + * @return mixed + */ + public function groupAccessTokens(string|int $group_id): mixed + { + return $this->get($this->getGroupPath($group_id, 'access_tokens')); + } + + /** + * @param string|int $group_id + * @param string|int $token_id + * + * @return mixed + */ + public function groupAccessToken(string|int $group_id, string|int $token_id): mixed + { + return $this->get($this->getGroupPath($group_id, 'access_tokens/'.self::encodePath($token_id))); + } + + /** + * @param string|int $group_id + * @param array $parameters + * + * @return mixed + */ + public function createGroupAccessToken(string|int $group_id, array $parameters = []): mixed + { + $resolver = $this->createOptionsResolver(); + $datetimeNormalizer = function (Options $resolver, \DateTimeInterface $value): string { + return $value->format('Y-m-d'); + }; + + $resolver->define('name') + ->required(); + + $resolver->define('scopes') + ->required() + ->allowedTypes('array') + ->allowedValues(function ($scopes) { + $allowed = ['api', 'read_api', 'read_registry', 'write_registry', 'read_repository', 'write_repository']; + foreach ($scopes as $scope) { + if (!\in_array($scope, $allowed, true)) { + return false; + } + } + + return true; + }); + + $resolver->setDefined('access_level') + ->setAllowedTypes('access_level', 'int') + ->setAllowedValues('access_level', [10, 20, 30, 40, 50]); + + $resolver->setDefined('expires_at') + ->setAllowedTypes('expires_at', \DateTimeInterface::class) + ->setNormalizer('expires_at', $datetimeNormalizer); + + return $this->post($this->getGroupPath($group_id, 'access_tokens'), $resolver->resolve($parameters)); + } + + /** + * @param string|int $group_id + * @param string|int $token_id + * + * @return mixed + */ + public function deleteGroupAccessToken(string|int $group_id, string|int $token_id): mixed + { + return $this->delete($this->getGroupPath($group_id, 'access_tokens/'.self::encodePath($token_id))); + } + + /** + * @param string|int $group_id + * @param string|int $token_id + * @param string $expiry + * + * @return mixed + */ + public function rotateGroupAccessToken(string|int $group_id, string|int $token_id, string $expiry = ''): mixed + { + $regex = '/(?:19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[01])/'; + if ('' !== $expiry && false !== \preg_match($regex, $expiry)) { + $uri = 'access_tokens/'.self::encodePath($token_id).'/rotate?expires_at='.$expiry; + + return $this->post($this->getGroupPath($group_id, $uri)); + } + + return $this->post($this->getGroupPath($group_id, 'access_tokens/'.self::encodePath($token_id).'/rotate')); + } + /** * @param int|string $id * @param array $parameters { diff --git a/src/Api/Projects.php b/src/Api/Projects.php index d0c66833..0bac0ca6 100644 --- a/src/Api/Projects.php +++ b/src/Api/Projects.php @@ -1715,6 +1715,25 @@ public function deleteProjectAccessToken($project_id, $token_id) return $this->delete($this->getProjectPath($project_id, 'access_tokens/'.$token_id)); } + /** + * @param string|int $project_id + * @param string|int $token_id + * @param string $expiry + * + * @return mixed + */ + public function rotateProjectAccessToken(string|int $project_id, string|int $token_id, string $expiry = ''): mixed + { + $regex = '/(?:19|20)\d{2}-(0[1-9]|1[0-2])-(0[1-9]|[1-2]\d|3[01])/'; + if ('' !== $expiry && false !== \preg_match($regex, $expiry)) { + $uri = 'access_tokens/'.self::encodePath($token_id).'/rotate?expires_at='.$expiry; + + return $this->post($this->getProjectPath($project_id, $uri)); + } + + return $this->post($this->getProjectPath($project_id, 'access_tokens/'.self::encodePath($token_id).'/rotate')); + } + /** * @param int|string $project_id * diff --git a/src/HttpClient/Plugin/Authentication.php b/src/HttpClient/Plugin/Authentication.php index 2160f5cd..923c981f 100644 --- a/src/HttpClient/Plugin/Authentication.php +++ b/src/HttpClient/Plugin/Authentication.php @@ -19,7 +19,6 @@ use Http\Client\Common\Plugin; use Http\Promise\Promise; use Psr\Http\Message\RequestInterface; -use Psr\Http\Message\ResponseInterface; /** * Add authentication to the request. @@ -55,7 +54,7 @@ public function __construct(string $method, string $token, string $sudo = null) * @param callable $next * @param callable $first * - * @return Promise + * @return Promise */ public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { diff --git a/src/HttpClient/Plugin/ExceptionThrower.php b/src/HttpClient/Plugin/ExceptionThrower.php index 207ecaaa..33bc2cf0 100644 --- a/src/HttpClient/Plugin/ExceptionThrower.php +++ b/src/HttpClient/Plugin/ExceptionThrower.php @@ -39,10 +39,13 @@ final class ExceptionThrower implements Plugin * Handle the request and return the response coming from the next callable. * * @param RequestInterface $request - * @param callable $next - * @param callable $first + * @param callable $next + * @param callable $first * - * @return Promise + * @throws ErrorException + * @throws ExceptionInterface + * + * @return Promise */ public function handleRequest(RequestInterface $request, callable $next, callable $first): Promise { diff --git a/tests/Api/GroupsTest.php b/tests/Api/GroupsTest.php index 19209bfd..38058b35 100644 --- a/tests/Api/GroupsTest.php +++ b/tests/Api/GroupsTest.php @@ -941,6 +941,153 @@ public function shouldDeleteDeployToken(): void $this->assertEquals($expectedBool, $api->deleteDeployToken(1, 2)); } + /** + * @test + */ + public function shouldGetGroupAccessTokens(): void + { + $expectedArray = [ + [ + 'user_id' => 141, + 'scopes' => [ + 'api', + ], + 'name' => 'token', + 'expires_at' => '2021-01-31', + 'id' => 42, + 'active' => true, + 'created_at' => '2021-01-20T22:11:48.151Z', + 'revoked' => false, + ], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/access_tokens') + ->will($this->returnValue($expectedArray)); + + $this->assertEquals($expectedArray, $api->groupAccessTokens(1)); + } + + /** + * @test + */ + public function shouldGetGroupAccessToken(): void + { + $expectedArray = [ + 'user_id' => 141, + 'scopes' => [ + 'api', + ], + 'name' => 'token', + 'expires_at' => '2021-01-31', + 'id' => 42, + 'active' => true, + 'created_at' => '2021-01-20T22:11:48.151Z', + 'revoked' => false, + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/access_tokens/42') + ->will($this->returnValue($expectedArray)); + + $this->assertEquals($expectedArray, $api->groupAccessToken(1, 42)); + } + + /** + * @test + */ + public function shouldCreateGroupAccessToken(): void + { + $expectedArray = [ + 'scopes' => [ + 'api', + 'read_repository', + ], + 'active' => true, + 'name' => 'test', + 'revoked' => false, + 'created_at' => '2021-01-21T19:35:37.921Z', + 'user_id' => 166, + 'id' => 58, + 'expires_at' => '2021-01-31', + 'token' => 'D4y...Wzr', + ]; + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with( + 'groups/1/access_tokens', + [ + 'name' => 'test_token', + 'scopes' => [ + 'api', + 'read_repository', + ], + 'access_level' => 30, + 'expires_at' => '2021-01-31', + ] + ) + ->will($this->returnValue($expectedArray)); + + $this->assertEquals($expectedArray, $api->createGroupAccessToken(1, [ + 'name' => 'test_token', + 'scopes' => [ + 'api', + 'read_repository', + ], + 'access_level' => 30, + 'expires_at' => new DateTime('2021-01-31'), + ])); + } + + /** + * @test + */ + public function shouldDeleteGroupAccessToken(): void + { + $expectedBool = true; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('delete') + ->with('groups/1/access_tokens/2') + ->will($this->returnValue($expectedBool)); + + $this->assertEquals($expectedBool, $api->deleteGroupAccessToken(1, 2)); + } + + /** + * @test + */ + public function shouldRotateGroupAccessToken(): void + { + $expectedArray = [ + 'scopes' => [ + 'api', + 'read_repository', + ], + 'active' => true, + 'name' => 'test', + 'revoked' => false, + 'created_at' => '2021-01-21T19:35:37.921Z', + 'user_id' => 166, + 'id' => 58, + 'expires_at' => '2021-01-31', + 'token' => 'D4y...Wzr', + ]; + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('groups/1/access_tokens/2/rotate?expires_at=2021-01-31') + ->will($this->returnValue($expectedArray)); + + $this->assertEquals($expectedArray, $api->rotateGroupAccessToken(1, 2, '2021-01-31')); + } + /** * @test */ diff --git a/tests/Api/ProjectsTest.php b/tests/Api/ProjectsTest.php index c3296d62..13495574 100644 --- a/tests/Api/ProjectsTest.php +++ b/tests/Api/ProjectsTest.php @@ -2952,6 +2952,35 @@ public function shouldDeleteProjectAccessToken(): void $this->assertEquals($expectedBool, $api->deleteProjectAccessToken(1, 2)); } + /** + * @test + */ + public function shouldRotateProjectAccessToken(): void + { + $expectedArray = [ + 'scopes' => [ + 'api', + 'read_repository', + ], + 'active' => true, + 'name' => 'test', + 'revoked' => false, + 'created_at' => '2021-01-21T19:35:37.921Z', + 'user_id' => 166, + 'id' => 58, + 'expires_at' => '2021-01-31', + 'token' => 'D4y...Wzr', + ]; + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with( + 'projects/1/access_tokens/2/rotate?expires_at=2021-01-31') + ->will($this->returnValue($expectedArray)); + + $this->assertEquals($expectedArray, $api->rotateProjectAccessToken(1, 2, '2021-01-31')); + } + /** * @test */