mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-15 16:14:35 +00:00
feat: self-hosted GitLab Apps OAuth integration
Adds self-hosted GitLab OAuth sources so Coolify can connect to a self-managed GitLab instance, list private repositories, clone over an OAuth token, and deploy (the GitLab counterpart to GitHub Apps). Hardening: authenticated, one-time team-bound OAuth callback state; token redaction in deploy logs; custom host port/path kept in clone and ls-remote URLs; submodule OAuth auth; system-wide source selection. Covered by unit and feature tests. cosigned by OpenAI Codex at M1 Max
This commit is contained in:
1 parent
9d341d0bb9
commit
a26091de0a
32 files changed
+2216
-26
No files matched your search
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Source;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
if (! InstanceSettings::find(0)) {
|
||||
$settings = new InstanceSettings;
|
||||
$settings->id = 0;
|
||||
$settings->save();
|
||||
}
|
||||
|
||||
$this->user = User::factory()->create();
|
||||
$this->team = Team::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
$this->project = Project::factory()->create(['team_id' => $this->team->id]);
|
||||
$this->environment = Environment::factory()->create(['project_id' => $this->project->id]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
test('a GitLab source is not hidden by a GitHub source sharing the same numeric id', function () {
|
||||
$githubApp = GithubApp::create([
|
||||
'name' => 'gh',
|
||||
'team_id' => $this->team->id,
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => false,
|
||||
'app_id' => 123,
|
||||
]);
|
||||
|
||||
$gitlabApp = GitlabApp::create([
|
||||
'name' => 'gl',
|
||||
'team_id' => $this->team->id,
|
||||
'api_url' => 'https://gitlab.example.test/api/v4',
|
||||
'html_url' => 'https://gitlab.example.test',
|
||||
'is_public' => false,
|
||||
'access_token' => 'token',
|
||||
'refresh_token' => 'refresh',
|
||||
'expires_at' => time() + 3600,
|
||||
]);
|
||||
|
||||
// The two source tables auto-increment independently, so the first row in each shares id 1.
|
||||
expect($gitlabApp->id)->toBe($githubApp->id);
|
||||
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'private_key_id' => null,
|
||||
'source_id' => $githubApp->id,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
|
||||
$component = Livewire::test(Source::class, ['application' => $application]);
|
||||
$sources = $component->get('sources');
|
||||
|
||||
expect($sources->contains(fn ($s) => $s instanceof GitlabApp && $s->id === $gitlabApp->id))->toBeTrue();
|
||||
expect($sources->contains(fn ($s) => $s instanceof GithubApp && $s->id === $githubApp->id))->toBeFalse();
|
||||
|
||||
// The GitLab source renders as selectable, not flagged as the current GitHub source despite the shared id.
|
||||
$component->assertSee('gl')->assertDontSee('(current)');
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Source\Gitlab\Change as GitlabSource;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->gitlabApp = GitlabApp::create([
|
||||
'name' => 'Self-hosted GitLab',
|
||||
'api_url' => 'https://gitlab.example.com/api/v4',
|
||||
'html_url' => 'https://gitlab.example.com',
|
||||
'client_id' => 'client-id',
|
||||
'client_secret' => 'client-secret',
|
||||
'redirect_uri' => 'https://coolify.example.com/webhooks/source/gitlab/redirect',
|
||||
'team_id' => $this->team->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
describe('GitLab OAuth callback state validation', function () {
|
||||
test('rejects a callback whose state is the source UUID (the old attack vector)', function () {
|
||||
Http::fake();
|
||||
|
||||
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state='.$this->gitlabApp->uuid);
|
||||
|
||||
$response->assertRedirect(route('source.all'));
|
||||
Http::assertNothingSent();
|
||||
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('rejects a callback with an unknown / expired state and never exchanges the code', function () {
|
||||
Http::fake();
|
||||
|
||||
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state=not-a-real-state');
|
||||
|
||||
$response->assertRedirect(route('source.all'));
|
||||
Http::assertNothingSent();
|
||||
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('rejects a state issued for a different team', function () {
|
||||
Http::fake();
|
||||
$otherTeam = Team::factory()->create();
|
||||
$state = 'state-for-other-team';
|
||||
Cache::put(GitlabSource::oauthStateCacheKey($state), [
|
||||
'gitlab_app_id' => $this->gitlabApp->id,
|
||||
'team_id' => $otherTeam->id,
|
||||
], now()->addMinutes(60));
|
||||
|
||||
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state='.$state);
|
||||
|
||||
$response->assertRedirect(route('source.all'));
|
||||
Http::assertNothingSent();
|
||||
expect($this->gitlabApp->refresh()->access_token)->toBeNull();
|
||||
});
|
||||
|
||||
test('accepts a valid one-time state, exchanges the code, and consumes the state', function () {
|
||||
Http::fake([
|
||||
'*/oauth/token' => Http::response([
|
||||
'access_token' => 'new-access-token',
|
||||
'refresh_token' => 'new-refresh-token',
|
||||
'expires_in' => 7200,
|
||||
]),
|
||||
]);
|
||||
|
||||
$state = 'a-valid-server-issued-state';
|
||||
$key = GitlabSource::oauthStateCacheKey($state);
|
||||
Cache::put($key, [
|
||||
'gitlab_app_id' => $this->gitlabApp->id,
|
||||
'team_id' => $this->team->id,
|
||||
], now()->addMinutes(60));
|
||||
|
||||
$response = $this->get('/webhooks/source/gitlab/redirect?code=valid-code&state='.$state);
|
||||
|
||||
$response->assertRedirect(route('source.gitlab.show', ['gitlab_app_uuid' => $this->gitlabApp->uuid]));
|
||||
|
||||
$fresh = $this->gitlabApp->refresh();
|
||||
$fresh->makeVisible(['access_token', 'refresh_token']);
|
||||
expect($fresh->access_token)->toBe('new-access-token');
|
||||
expect($fresh->refresh_token)->toBe('new-refresh-token');
|
||||
|
||||
// State must be single-use.
|
||||
expect(Cache::get($key))->toBeNull();
|
||||
});
|
||||
|
||||
test('requires authentication', function () {
|
||||
auth()->logout();
|
||||
session()->forget('currentTeam');
|
||||
|
||||
$response = $this->get('/webhooks/source/gitlab/redirect?code=any&state=any');
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
beforeEach(function () {
|
||||
Model::encryptUsing(new Encrypter(str_repeat('a', 32), 'AES-256-CBC'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Model::encryptUsing(null);
|
||||
});
|
||||
|
||||
it('returns a stable shape with has_more false when GitLab repo listing fails', function () {
|
||||
Http::fake(['*' => Http::response(['message' => '401 Unauthorized'], 401)]);
|
||||
|
||||
$source = new GitlabApp([
|
||||
'api_url' => 'https://gitlab.example.test/api/v4',
|
||||
'access_token' => str_repeat('t', 20), // ggignore
|
||||
'refresh_token' => str_repeat('r', 20), // ggignore
|
||||
'expires_at' => time() + 3600,
|
||||
]);
|
||||
|
||||
$result = loadGitlabRepositories($source);
|
||||
|
||||
expect($result)->toHaveKeys(['total_count', 'has_more', 'repositories']);
|
||||
expect($result['has_more'])->toBeFalse();
|
||||
expect($result['repositories'])->toBe([]);
|
||||
});
|
||||
|
||||
it('limits GitLab repositories to the exact group and its descendants', function () {
|
||||
Http::fake(['*' => Http::response([
|
||||
['id' => 1, 'name' => 'a', 'path_with_namespace' => 'team/a', 'namespace' => ['full_path' => 'team', 'kind' => 'group']],
|
||||
['id' => 2, 'name' => 'b', 'path_with_namespace' => 'team/sub/b', 'namespace' => ['full_path' => 'team/sub', 'kind' => 'group']],
|
||||
['id' => 3, 'name' => 'c', 'path_with_namespace' => 'team-secret/c', 'namespace' => ['full_path' => 'team-secret', 'kind' => 'group']],
|
||||
], 200)]);
|
||||
|
||||
$source = new GitlabApp([
|
||||
'api_url' => 'https://gitlab.example.test/api/v4',
|
||||
'access_token' => str_repeat('t', 20), // ggignore
|
||||
'refresh_token' => str_repeat('r', 20), // ggignore
|
||||
'expires_at' => time() + 3600,
|
||||
'group_name' => 'team',
|
||||
]);
|
||||
|
||||
$paths = collect(loadGitlabRepositories($source)['repositories'])->pluck('path_with_namespace')->all();
|
||||
|
||||
expect($paths)->toContain('team/a', 'team/sub/b');
|
||||
expect($paths)->not->toContain('team-secret/c');
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
test('ownedByCurrentTeam resolves a system-wide GitLab source owned by another team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
|
||||
$systemWide = GitlabApp::create([
|
||||
'name' => 'Shared GitLab',
|
||||
'api_url' => 'https://gitlab.example.com/api/v4',
|
||||
'html_url' => 'https://gitlab.example.com',
|
||||
'team_id' => $otherTeam->id,
|
||||
'is_system_wide' => true,
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
// Mirrors Source::changeSource() resolution; before the fix this returned null for system-wide sources (404).
|
||||
$resolved = GitlabApp::ownedByCurrentTeam()->find($systemWide->id);
|
||||
|
||||
expect($resolved)->not->toBeNull();
|
||||
expect($resolved->id)->toBe($systemWide->id);
|
||||
});
|
||||
|
||||
test('ownedByCurrentTeam still excludes other teams private (non system-wide) sources', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
|
||||
$foreign = GitlabApp::create([
|
||||
'name' => 'Foreign GitLab',
|
||||
'api_url' => 'https://gitlab.example.com/api/v4',
|
||||
'html_url' => 'https://gitlab.example.com',
|
||||
'team_id' => $otherTeam->id,
|
||||
'is_system_wide' => false,
|
||||
'is_public' => false,
|
||||
]);
|
||||
|
||||
expect(GitlabApp::ownedByCurrentTeam()->find($foreign->id))->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationSetting;
|
||||
use App\Models\GitlabApp;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
|
||||
beforeEach(function () {
|
||||
Model::encryptUsing(new Encrypter(str_repeat('a', 32), 'AES-256-CBC'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Model::encryptUsing(null);
|
||||
});
|
||||
|
||||
test('connected gitlab oauth submodule credentials use per command git config', function () {
|
||||
$application = new Application;
|
||||
$application->forceFill([
|
||||
'uuid' => 'test-app-uuid',
|
||||
'git_repository' => 'group/private-app',
|
||||
'git_branch' => 'main',
|
||||
'git_commit_sha' => 'HEAD',
|
||||
]);
|
||||
|
||||
$settings = new ApplicationSetting;
|
||||
$settings->is_git_shallow_clone_enabled = false;
|
||||
$settings->is_git_submodules_enabled = true;
|
||||
$settings->is_git_lfs_enabled = false;
|
||||
$application->setRelation('settings', $settings);
|
||||
|
||||
$source = new GitlabApp;
|
||||
$source->forceFill([
|
||||
'html_url' => 'https://gitlab.example.test',
|
||||
'api_url' => 'https://gitlab.example.test/api/v4',
|
||||
'is_public' => false,
|
||||
]);
|
||||
// A non-expired token short-circuits refreshGitlabToken(), so no HTTP call is made.
|
||||
$source->access_token = 'gl-token/with+sym';
|
||||
$source->refresh_token = 'gl-refresh-token';
|
||||
$source->expires_at = time() + 3600;
|
||||
$application->setRelation('source', $source);
|
||||
|
||||
$result = $application->generateGitImportCommands(
|
||||
deployment_uuid: 'test-deployment',
|
||||
exec_in_docker: false,
|
||||
);
|
||||
|
||||
$expectedConfig = "git -c 'url.https://oauth2:gl-token%2Fwith%2Bsym@gitlab.example.test/.insteadOf=https://gitlab.example.test/' -c http.version=HTTP/1.1";
|
||||
|
||||
expect($result['commands'])
|
||||
->not->toContain('git config --global')
|
||||
->toContain("{$expectedConfig} clone --recurse-submodules -b 'main'")
|
||||
->toContain("{$expectedConfig} submodule sync")
|
||||
->toContain("{$expectedConfig} submodule update --init --recursive");
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use App\Models\GitlabApp;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
|
||||
beforeEach(function () {
|
||||
Model::encryptUsing(new Encrypter(str_repeat('a', 32), 'AES-256-CBC'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Model::encryptUsing(null);
|
||||
});
|
||||
|
||||
it('returns api base url with /api/v4 appended when missing', function () {
|
||||
$app = new GitlabApp(['api_url' => 'https://gitlab.example.com']);
|
||||
expect($app->apiUrlBase())->toBe('https://gitlab.example.com/api/v4');
|
||||
});
|
||||
|
||||
it('returns api base url unchanged when /api/v4 already present', function () {
|
||||
$app = new GitlabApp(['api_url' => 'https://gitlab.example.com/api/v4']);
|
||||
expect($app->apiUrlBase())->toBe('https://gitlab.example.com/api/v4');
|
||||
});
|
||||
|
||||
it('strips trailing slash from api url base', function () {
|
||||
$app = new GitlabApp(['api_url' => 'https://gitlab.example.com/api/v4/']);
|
||||
expect($app->apiUrlBase())->toBe('https://gitlab.example.com/api/v4');
|
||||
});
|
||||
|
||||
it('reports connected when tokens are present', function () {
|
||||
$app = new GitlabApp([
|
||||
'access_token' => str_repeat('t', 20), // ggignore
|
||||
'refresh_token' => str_repeat('r', 20), // ggignore
|
||||
]);
|
||||
expect($app->isConnected())->toBeTrue();
|
||||
});
|
||||
|
||||
it('reports not connected when tokens are missing', function () {
|
||||
$app = new GitlabApp([
|
||||
'access_token' => null,
|
||||
'refresh_token' => null,
|
||||
]);
|
||||
expect($app->isConnected())->toBeFalse();
|
||||
});
|
||||
|
||||
it('reports not connected when only access token is present', function () {
|
||||
$app = new GitlabApp([
|
||||
'access_token' => str_repeat('t', 20), // ggignore
|
||||
'refresh_token' => null,
|
||||
]);
|
||||
expect($app->isConnected())->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
it('redacts oauth2 tokens from deployment logs', function () {
|
||||
$fakeOAuthToken = str_repeat('a', 30); // ggignore
|
||||
$text = "git clone https://oauth2:{$fakeOAuthToken}@gitlab.example.com/group/repo.git /app";
|
||||
$result = remove_iip($text);
|
||||
|
||||
expect($result)->not->toContain($fakeOAuthToken);
|
||||
expect($result)->toContain('oauth2:');
|
||||
});
|
||||
|
||||
it('redacts x-access-token from logs', function () {
|
||||
$fakeToken = str_repeat('x', 40); // ggignore
|
||||
$text = "git clone https://x-access-token:{$fakeToken}@github.com/org/repo.git /app";
|
||||
$result = remove_iip($text);
|
||||
|
||||
expect($result)->not->toContain($fakeToken);
|
||||
expect($result)->toContain('x-access-token:');
|
||||
});
|
||||
|
||||
it('redacts gitlab personal access tokens', function () {
|
||||
$fakeToken = 'glpat-'.str_repeat('y', 20); // ggignore
|
||||
$text = "Authorization: Bearer {$fakeToken}";
|
||||
$result = remove_iip($text);
|
||||
|
||||
expect($result)->not->toContain($fakeToken);
|
||||
});
|
||||
@@ -3,9 +3,16 @@
|
||||
use App\Models\Application;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
beforeEach(function () {
|
||||
Model::encryptUsing(new Encrypter(str_repeat('a', 32), 'AES-256-CBC'));
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Model::encryptUsing(null);
|
||||
Mockery::close();
|
||||
});
|
||||
|
||||
@@ -54,6 +61,9 @@ it('generates ls-remote commands for GitLab source with private key', function (
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn($privateKey);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(1);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('custom_port')->andReturn(22);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('access_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('refresh_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('isConnected')->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->git_branch = 'main';
|
||||
@@ -83,6 +93,9 @@ it('generates ls-remote commands for GitLab source without private key', functio
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('access_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('refresh_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('isConnected')->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->git_branch = 'main';
|
||||
@@ -112,6 +125,9 @@ it('does not return null for GitLab source type', function () {
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('html_url')->andReturn('https://gitlab.com');
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('privateKey')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('private_key_id')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('access_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('getAttribute')->with('refresh_token')->andReturn(null);
|
||||
$gitlabSource->shouldReceive('isConnected')->andReturn(false);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->git_branch = 'main';
|
||||
@@ -127,3 +143,73 @@ it('does not return null for GitLab source type', function () {
|
||||
expect($lsRemoteResult)->not->toBeNull();
|
||||
expect($lsRemoteResult)->toHaveKeys(['commands', 'branch', 'fullRepoUrl']);
|
||||
});
|
||||
|
||||
it('preserves custom GitLab http port for connected OAuth sources', function () {
|
||||
$deploymentUuid = 'test-deployment-uuid';
|
||||
|
||||
$gitlabSource = new GitlabApp([
|
||||
'html_url' => 'http://gitlab.example.test:8081',
|
||||
'access_token' => 'gitlab-access-token',
|
||||
'refresh_token' => 'gitlab-refresh-token',
|
||||
'expires_at' => time() + 3600,
|
||||
]);
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->git_branch = 'main';
|
||||
$application->shouldReceive('deploymentType')->andReturn('source');
|
||||
$application->shouldReceive('customRepository')->andReturn([
|
||||
'repository' => 'root/qa-private-app',
|
||||
'port' => 22,
|
||||
]);
|
||||
$application->shouldReceive('getAttribute')->with('source')->andReturn($gitlabSource);
|
||||
$application->source = $gitlabSource;
|
||||
|
||||
$result = $application->generateGitLsRemoteCommands($deploymentUuid, false);
|
||||
|
||||
expect($result['fullRepoUrl'])
|
||||
->toContain('gitlab.example.test:8081')
|
||||
->toBe('http://oauth2:gitlab-access-token@gitlab.example.test:8081/root/qa-private-app.git');
|
||||
expect($result['commands'])->toContain('gitlab.example.test:8081/root/qa-private-app.git');
|
||||
});
|
||||
|
||||
it('applies OAuth git config to GitLab merge-request fetch and submodule checkout', function () {
|
||||
$deploymentUuid = 'test-deployment-uuid';
|
||||
|
||||
$gitlabSource = new GitlabApp([
|
||||
'html_url' => 'https://gitlab.example.test',
|
||||
'access_token' => 'gitlab-access-token',
|
||||
'refresh_token' => 'gitlab-refresh-token',
|
||||
'expires_at' => time() + 3600,
|
||||
]);
|
||||
|
||||
$settings = (object) [
|
||||
'is_git_shallow_clone_enabled' => false,
|
||||
'is_git_submodules_enabled' => true,
|
||||
];
|
||||
|
||||
$application = Mockery::mock(Application::class)->makePartial();
|
||||
$application->git_branch = 'main';
|
||||
$application->shouldReceive('deploymentType')->andReturn('source');
|
||||
$application->shouldReceive('customRepository')->andReturn([
|
||||
'repository' => 'root/qa-private-app',
|
||||
'port' => 22,
|
||||
]);
|
||||
$application->shouldReceive('getAttribute')->with('source')->andReturn($gitlabSource);
|
||||
$application->shouldReceive('getAttribute')->with('settings')->andReturn($settings);
|
||||
$application->source = $gitlabSource;
|
||||
|
||||
$result = $application->generateGitImportCommands(
|
||||
deployment_uuid: $deploymentUuid,
|
||||
pull_request_id: 2,
|
||||
exec_in_docker: false,
|
||||
only_checkout: true,
|
||||
custom_base_dir: '/artifacts/test',
|
||||
);
|
||||
|
||||
$commands = $result['commands'];
|
||||
// The MR-ref fetch and submodule update must run through the OAuth-rewritten git, or private same-host submodules fail.
|
||||
expect($commands)
|
||||
->toContain('oauth2:gitlab-access-token@gitlab.example.test')
|
||||
->toContain("http.version=HTTP/1.1 fetch origin 'merge-requests/2/head:pr-2-coolify'")
|
||||
->toContain('http.version=HTTP/1.1 submodule update --init --recursive');
|
||||
});
|
||||
Reference in new issue
Block a user