From 1e1204fe40418e8786dd4a31edbb1c4c5d431d07 Mon Sep 17 00:00:00 2001 From: Koen Hendriks Date: Mon, 25 Sep 2023 10:11:01 +0200 Subject: [PATCH] Gitlab Self-managed Support (#56) * Update UI to ask for URL for Gitlab as provider. * Create state for each source control in factory. * Save url for SourceControl when given. * Make Gitlab dynamically use correct URL (custom or default) * Update SourceControlsTest to check for custom url when given. * Style fixes. --------- Co-authored-by: Saeed Vaziry <61919774+saeedvaziry@users.noreply.github.com> --- .../SourceControl/ConnectSourceControl.php | 7 ++ app/SourceControlProviders/Gitlab.php | 29 +++++-- database/factories/SourceControlFactory.php | 36 ++++++++ .../views/components/input-help.blade.php | 1 + .../source-controls/connect.blade.php | 11 +++ tests/Feature/Http/SourceControlsTest.php | 20 +++-- .../SourceControlProviders/GitlabTest.php | 86 +++++++++++++++++++ 7 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 resources/views/components/input-help.blade.php create mode 100644 tests/Unit/SourceControlProviders/GitlabTest.php diff --git a/app/Actions/SourceControl/ConnectSourceControl.php b/app/Actions/SourceControl/ConnectSourceControl.php index 3fe72d7a..4c2fae46 100644 --- a/app/Actions/SourceControl/ConnectSourceControl.php +++ b/app/Actions/SourceControl/ConnectSourceControl.php @@ -3,6 +3,7 @@ namespace App\Actions\SourceControl; use App\Models\SourceControl; +use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; use Illuminate\Validation\ValidationException; @@ -16,6 +17,7 @@ public function connect(array $input): void 'provider' => $input['provider'], 'profile' => $input['name'], 'access_token' => $input['token'], + 'url' => Arr::has($input, 'url') ? $input['url'] : null, ]); if (! $sourceControl->provider()->connect()) { @@ -44,6 +46,11 @@ private function validate(array $input): void 'token' => [ 'required', ], + 'url' => [ + 'nullable', + 'url:http,https', + 'ends_with:/', + ], ]; Validator::make($input, $rules)->validate(); } diff --git a/app/SourceControlProviders/Gitlab.php b/app/SourceControlProviders/Gitlab.php index 0933f452..6b546f16 100755 --- a/app/SourceControlProviders/Gitlab.php +++ b/app/SourceControlProviders/Gitlab.php @@ -10,12 +10,14 @@ class Gitlab extends AbstractSourceControlProvider { - protected string $apiUrl = 'https://gitlab.com/api/v4'; + protected string $defaultApiHost = 'https://gitlab.com/'; + + protected string $apiVersion = 'api/v4'; public function connect(): bool { $res = Http::withToken($this->sourceControl->access_token) - ->get($this->apiUrl.'/projects'); + ->get($this->getApiUrl().'/projects'); return $res->successful(); } @@ -27,7 +29,7 @@ public function getRepo(string $repo = null): mixed { $repository = $repo ? urlencode($repo) : null; $res = Http::withToken($this->sourceControl->access_token) - ->get($this->apiUrl.'/projects/'.$repository.'/repository/commits'); + ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits'); $this->handleResponseErrors($res, $repo); @@ -36,7 +38,9 @@ public function getRepo(string $repo = null): mixed public function fullRepoUrl(string $repo, string $key): string { - return sprintf('git@gitlab.com-%s:%s.git', $key, $repo); + $host = parse_url($this->getApiUrl())['host']; + + return sprintf('git@%s-%s:%s.git', $host, $key, $repo); } /** @@ -46,7 +50,7 @@ public function deployHook(string $repo, array $events, string $secret): array { $repository = urlencode($repo); $response = Http::withToken($this->sourceControl->access_token)->post( - $this->apiUrl.'/projects/'.$repository.'/hooks', + $this->getApiUrl().'/projects/'.$repository.'/hooks', [ 'description' => 'deploy', 'url' => url('/git-hooks?secret='.$secret), @@ -81,7 +85,7 @@ public function destroyHook(string $repo, string $hookId): void { $repository = urlencode($repo); $response = Http::withToken($this->sourceControl->access_token)->delete( - $this->apiUrl.'/projects/'.$repository.'/hooks/'.$hookId + $this->getApiUrl().'/projects/'.$repository.'/hooks/'.$hookId ); if ($response->status() != 204) { @@ -96,7 +100,7 @@ public function getLastCommit(string $repo, string $branch): ?array { $repository = urlencode($repo); $res = Http::withToken($this->sourceControl->access_token) - ->get($this->apiUrl.'/projects/'.$repository.'/repository/commits?ref_name='.$branch); + ->get($this->getApiUrl().'/projects/'.$repository.'/repository/commits?ref_name='.$branch); $this->handleResponseErrors($res, $repo); @@ -123,7 +127,7 @@ public function deployKey(string $title, string $repo, string $key): void { $repository = urlencode($repo); $response = Http::withToken($this->sourceControl->access_token)->post( - $this->apiUrl.'/projects/'.$repository.'/deploy_keys', + $this->getApiUrl().'/projects/'.$repository.'/deploy_keys', [ 'title' => $title, 'key' => $key, @@ -135,4 +139,13 @@ public function deployKey(string $title, string $repo, string $key): void throw new FailedToDeployGitKey(json_decode($response->body())->message); } } + + public function getApiUrl(): string + { + $host = $this->sourceControl->url === null + ? $this->defaultApiHost + : $this->sourceControl->url; + + return $host.$this->apiVersion; + } } diff --git a/database/factories/SourceControlFactory.php b/database/factories/SourceControlFactory.php index e0d3fb22..7de1a1bb 100644 --- a/database/factories/SourceControlFactory.php +++ b/database/factories/SourceControlFactory.php @@ -17,4 +17,40 @@ public function definition(): array 'access_token' => Str::random(10), ]; } + + public function gitlab(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'provider' => \App\Enums\SourceControl::GITLAB, + ]; + }); + } + + public function github(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'provider' => \App\Enums\SourceControl::GITHUB, + ]; + }); + } + + public function bitbucket(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'provider' => \App\Enums\SourceControl::BITBUCKET, + ]; + }); + } + + public function custom(): Factory + { + return $this->state(function (array $attributes) { + return [ + 'provider' => \App\Enums\SourceControl::CUSTOM, + ]; + }); + } } diff --git a/resources/views/components/input-help.blade.php b/resources/views/components/input-help.blade.php new file mode 100644 index 00000000..808655eb --- /dev/null +++ b/resources/views/components/input-help.blade.php @@ -0,0 +1 @@ +

merge(['class' => 'mt-2 text-sm text-gray-500 dark:text-gray-300']) }}>{{ $slot }}

diff --git a/resources/views/livewire/source-controls/connect.blade.php b/resources/views/livewire/source-controls/connect.blade.php index 6725d883..7d0c82ed 100644 --- a/resources/views/livewire/source-controls/connect.blade.php +++ b/resources/views/livewire/source-controls/connect.blade.php @@ -32,6 +32,17 @@ @enderror + @if($provider === App\Enums\SourceControl::GITLAB) +
+ + + If you run a self-managed gitlab enter the url here, leave empty to use gitlab.com + @error('url') + + @enderror +
+ @endif +
diff --git a/tests/Feature/Http/SourceControlsTest.php b/tests/Feature/Http/SourceControlsTest.php index ba34c121..820aa209 100644 --- a/tests/Feature/Http/SourceControlsTest.php +++ b/tests/Feature/Http/SourceControlsTest.php @@ -17,21 +17,28 @@ class SourceControlsTest extends TestCase /** * @dataProvider data */ - public function test_connect_provider(string $provider): void + public function test_connect_provider(string $provider, ?string $customUrl): void { $this->actingAs($this->user); Http::fake(); - Livewire::test(Connect::class) + $livewire = Livewire::test(Connect::class) ->set('token', 'token') ->set('name', 'profile') - ->set('provider', $provider) + ->set('provider', $provider); + + if ($customUrl !== null) { + $livewire->set('url', $customUrl); + } + + $livewire ->call('connect') ->assertSuccessful(); $this->assertDatabaseHas('source_controls', [ 'provider' => $provider, + 'url' => $customUrl, ]); } @@ -61,9 +68,10 @@ public function test_delete_provider(string $provider): void public static function data(): array { return [ - ['github'], - ['gitlab'], - ['bitbucket'], + ['github', null], + ['gitlab', null], + ['gitlab', 'https://git.example.com/'], + ['bitbucket', null], ]; } } diff --git a/tests/Unit/SourceControlProviders/GitlabTest.php b/tests/Unit/SourceControlProviders/GitlabTest.php new file mode 100644 index 00000000..62eab900 --- /dev/null +++ b/tests/Unit/SourceControlProviders/GitlabTest.php @@ -0,0 +1,86 @@ +gitlab() + ->create(); + + $gitlab = new Gitlab($sourceControlModel); + + $this->assertSame('https://gitlab.com/api/v4', $gitlab->getApiUrl()); + } + + public function test_default_gitlab_repo_url(): void + { + $repo = 'test/repo'; + $key = 'TEST_KEY'; + + $sourceControlModel = SourceControl::factory() + ->gitlab() + ->create(); + + $gitlab = new Gitlab($sourceControlModel); + + $this->assertSame('git@gitlab.com-TEST_KEY:test/repo.git', $gitlab->fullRepoUrl($repo, $key)); + } + + /** + * @dataProvider customUrlData + */ + public function test_custom_url(string $url, string $expected): void + { + $sourceControlModel = SourceControl::factory() + ->gitlab() + ->create(['url' => $url]); + + $gitlab = new Gitlab($sourceControlModel); + + $this->assertSame($expected, $gitlab->getApiUrl()); + } + + /** + * @dataProvider customRepoUrlData + */ + public function test_custom_full_repository_url(string $url, string $expected): void + { + $repo = 'test/repo'; + $key = 'TEST_KEY'; + + $sourceControlModel = SourceControl::factory() + ->gitlab() + ->create(['url' => $url]); + + $gitlab = new Gitlab($sourceControlModel); + + $this->assertSame($expected, $gitlab->fullRepoUrl($repo, $key)); + } + + public static function customRepoUrlData(): array + { + return [ + ['https://git.example.com/', 'git@git.example.com-TEST_KEY:test/repo.git'], + ['https://git.test.example.com/', 'git@git.test.example.com-TEST_KEY:test/repo.git'], + ['https://git.example.co.uk/', 'git@git.example.co.uk-TEST_KEY:test/repo.git'], + ]; + } + + public static function customUrlData(): array + { + return [ + ['https://git.example.com/', 'https://git.example.com/api/v4'], + ['https://git.test.example.com/', 'https://git.test.example.com/api/v4'], + ['https://git.example.co.uk/', 'https://git.example.co.uk/api/v4'], + ]; + } +}