diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ab09057..b288d4ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Add support for `visibility` in `Groups::all` * Add support for personal access tokens * Add support for project integrations endpoints +* Add support for group hook endpoints * Add support for `job_inputs` and `job_variables_attributes` in `Jobs::play` * Add support for filters in `Projects::projectAccessTokens` * Add support for `Projects::rotateProjectAccessToken` diff --git a/src/Api/GroupsHooks.php b/src/Api/GroupsHooks.php new file mode 100644 index 00000000..d388bd1e --- /dev/null +++ b/src/Api/GroupsHooks.php @@ -0,0 +1,115 @@ + + * (c) Graham Campbell + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gitlab\Api; + +use Symfony\Component\OptionsResolver\OptionsResolver; + +class GroupsHooks extends AbstractApi +{ + public function all(int|string $group_id): mixed + { + return $this->get('groups/'.self::encodePath($group_id).'/hooks'); + } + + public function show(int|string $group_id, int $hook_id): mixed + { + return $this->get('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id)); + } + + /** + * Create a group hook. + * + * Hook parameters vary across GitLab versions and are passed through. + * + * @see https://docs.gitlab.com/api/group_webhooks/#create-a-group-hook + * + * @param array $parameters + */ + public function create(int|string $group_id, string $url, array $parameters = []): mixed + { + $parameters['url'] = $url; + + return $this->post('groups/'.self::encodePath($group_id).'/hooks', $parameters); + } + + /** + * Update a group hook. + * + * Hook parameters vary across GitLab versions and are passed through. + * + * @see https://docs.gitlab.com/api/group_webhooks/#update-a-group-hook + * + * @param array $parameters + */ + public function update(int|string $group_id, int $hook_id, array $parameters): mixed + { + return $this->put('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id), $parameters); + } + + public function remove(int|string $group_id, int $hook_id): mixed + { + return $this->delete('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id)); + } + + /** + * @param array $parameters { + * + * @var int|string $status response status code or status category + * } + */ + public function events(int|string $group_id, int $hook_id, array $parameters = []): mixed + { + $resolver = new OptionsResolver(); + $resolver->setDefined('status') + ->setAllowedTypes('status', ['int', 'string']) + ; + + return $this->get('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/events', $resolver->resolve($parameters)); + } + + public function resendEvent(int|string $group_id, int $hook_id, int $hook_event_id): mixed + { + return $this->post('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/events/'.self::encodePath($hook_event_id).'/resend'); + } + + public function test(int|string $group_id, int $hook_id, string $trigger): mixed + { + return $this->post('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/test/'.self::encodePath($trigger)); + } + + public function setCustomHeader(int|string $group_id, int $hook_id, string $key, string $value): mixed + { + return $this->put('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/custom_headers/'.self::encodePath($key), [ + 'value' => $value, + ]); + } + + public function deleteCustomHeader(int|string $group_id, int $hook_id, string $key): mixed + { + return $this->delete('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/custom_headers/'.self::encodePath($key)); + } + + public function setUrlVariable(int|string $group_id, int $hook_id, string $key, string $value): mixed + { + return $this->put('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/url_variables/'.self::encodePath($key), [ + 'value' => $value, + ]); + } + + public function deleteUrlVariable(int|string $group_id, int $hook_id, string $key): mixed + { + return $this->delete('groups/'.self::encodePath($group_id).'/hooks/'.self::encodePath($hook_id).'/url_variables/'.self::encodePath($key)); + } +} diff --git a/src/Client.php b/src/Client.php index bcca7dbd..c4acc103 100644 --- a/src/Client.php +++ b/src/Client.php @@ -21,6 +21,7 @@ use Gitlab\Api\Groups; use Gitlab\Api\GroupsBoards; use Gitlab\Api\GroupsEpics; +use Gitlab\Api\GroupsHooks; use Gitlab\Api\GroupsMilestones; use Gitlab\Api\Integrations; use Gitlab\Api\IssueBoards; @@ -168,6 +169,11 @@ public function groupsEpics(): GroupsEpics return new GroupsEpics($this); } + public function groupsHooks(): GroupsHooks + { + return new GroupsHooks($this); + } + public function groupsMilestones(): GroupsMilestones { return new GroupsMilestones($this); diff --git a/tests/Api/GroupsHooksTest.php b/tests/Api/GroupsHooksTest.php new file mode 100644 index 00000000..d0a2e6f7 --- /dev/null +++ b/tests/Api/GroupsHooksTest.php @@ -0,0 +1,287 @@ + + * (c) Graham Campbell + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Gitlab\Tests\Api; + +use Gitlab\Api\GroupsHooks; +use PHPUnit\Framework\Attributes\Test; + +class GroupsHooksTest extends TestCase +{ + #[Test] + public function shouldGetAllHooks(): void + { + $expectedArray = [ + ['id' => 1, 'url' => 'https://example.com/webhook-trigger/1'], + ['id' => 2, 'url' => 'https://example.com/webhook-trigger/2'], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/hooks') + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->all(1)); + } + + #[Test] + public function shouldGetAllHooksForStringGroupPath(): void + { + $expectedArray = [ + ['id' => 1, 'url' => 'https://example.com/webhook-trigger/1'], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/foo%2Fbar/hooks') + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->all('foo/bar')); + } + + #[Test] + public function shouldShowHook(): void + { + $expectedArray = ['id' => 2, 'url' => 'https://example.com/webhook-trigger/2']; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/hooks/2') + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->show(1, 2)); + } + + #[Test] + public function shouldCreateHook(): void + { + $expectedArray = ['id' => 3, 'url' => 'https://example.com/webhook-trigger/3']; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('groups/1/hooks', ['push_events' => true, 'url' => $expectedArray['url']]) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->create(1, $expectedArray['url'], ['push_events' => true])); + } + + #[Test] + public function shouldCreateHookWithOnlyUrl(): void + { + $expectedArray = ['id' => 3, 'url' => 'https://example.com/webhook-trigger/3']; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('groups/1/hooks', ['url' => $expectedArray['url']]) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->create(1, $expectedArray['url'])); + } + + #[Test] + public function shouldUpdateHook(): void + { + $expectedArray = ['id' => 2, 'url' => 'https://example.com/webhook-trigger-rename/2']; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('put') + ->with('groups/1/hooks/2', ['url' => $expectedArray['url'], 'name' => 'Test Webhook']) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->update(1, 2, ['url' => $expectedArray['url'], 'name' => 'Test Webhook'])); + } + + #[Test] + public function shouldRemoveHook(): void + { + $expectedBool = true; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('delete') + ->with('groups/1/hooks/2') + ->willReturn($expectedBool) + ; + + $this->assertEquals($expectedBool, $api->remove(1, 2)); + } + + #[Test] + public function shouldGetEvents(): void + { + $expectedArray = [ + [ + 'id' => 1, + 'url' => 'https://example.com/webhook-trigger/2', + 'trigger' => 'push_hooks', + ], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/hooks/2/events', []) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->events(1, 2)); + } + + #[Test] + public function shouldGetEventsWithStringStatus(): void + { + $expectedArray = [ + ['id' => 1, 'response_status' => '500'], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/hooks/2/events', ['status' => 'server_failure']) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->events(1, 2, [ + 'status' => 'server_failure', + ])); + } + + #[Test] + public function shouldGetEventsWithIntegerStatus(): void + { + $expectedArray = [ + ['id' => 1, 'response_status' => '200'], + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('get') + ->with('groups/1/hooks/2/events', ['status' => 200]) + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->events(1, 2, ['status' => 200])); + } + + #[Test] + public function shouldResendEvent(): void + { + $expectedArray = [ + 'response_status' => 200, + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('groups/1/hooks/2/events/3/resend') + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->resendEvent(1, 2, 3)); + } + + #[Test] + public function shouldTestHook(): void + { + $expectedArray = [ + 'message' => '201 Created', + ]; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('post') + ->with('groups/1/hooks/2/test/push_events') + ->willReturn($expectedArray) + ; + + $this->assertEquals($expectedArray, $api->test(1, 2, 'push_events')); + } + + #[Test] + public function shouldSetCustomHeader(): void + { + $expected = ''; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('put') + ->with('groups/1/hooks/2/custom_headers/X-Webhook-Token', ['value' => 'secret']) + ->willReturn($expected) + ; + + $this->assertEquals($expected, $api->setCustomHeader(1, 2, 'X-Webhook-Token', 'secret')); + } + + #[Test] + public function shouldDeleteCustomHeader(): void + { + $expectedBool = true; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('delete') + ->with('groups/1/hooks/2/custom_headers/X-Webhook-Token') + ->willReturn($expectedBool) + ; + + $this->assertEquals($expectedBool, $api->deleteCustomHeader(1, 2, 'X-Webhook-Token')); + } + + #[Test] + public function shouldSetUrlVariable(): void + { + $expected = ''; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('put') + ->with('groups/1/hooks/2/url_variables/environment', ['value' => 'staging']) + ->willReturn($expected) + ; + + $this->assertEquals($expected, $api->setUrlVariable(1, 2, 'environment', 'staging')); + } + + #[Test] + public function shouldDeleteUrlVariable(): void + { + $expectedBool = true; + + $api = $this->getApiMock(); + $api->expects($this->once()) + ->method('delete') + ->with('groups/1/hooks/2/url_variables/environment') + ->willReturn($expectedBool) + ; + + $this->assertEquals($expectedBool, $api->deleteUrlVariable(1, 2, 'environment')); + } + + protected function getApiClass(): string + { + return GroupsHooks::class; + } +}