mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 18:52:49 +00:00
Stop calling /zen before each installation token request. A short network problem to GitHub failed deployments with a cURL timeout on /zen. assertGithubClockInSync now takes the failed response and reads its Date header, so the "System time is out of sync" error still shows when GitHub rejects the JWT. Call it on failed JWT requests in the webhook installation check, the permission job, the source connection test, and the server transfer checklist. Remove the /zen fakes from the tests.
383 lines
16 KiB
PHP
383 lines
16 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Project\New\GithubPrivateRepository;
|
|
use App\Models\GithubApp;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
|
|
use Livewire\Livewire;
|
|
|
|
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->rsaKey = openssl_pkey_new([
|
|
'private_key_bits' => 2048,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
]);
|
|
openssl_pkey_export($this->rsaKey, $pemKey);
|
|
|
|
$this->privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => $pemKey,
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$this->githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'installation_id' => 67890,
|
|
'client_id' => 'test-client-id',
|
|
'client_secret' => 'test-client-secret',
|
|
'webhook_secret' => 'test-webhook-secret',
|
|
'private_key_id' => $this->privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
});
|
|
|
|
function fakeGithubHttp(array $repositories): void
|
|
{
|
|
Http::fake([
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
|
'token' => 'fake-installation-token',
|
|
], 201),
|
|
'https://api.github.com/installation/repositories*' => Http::response([
|
|
'total_count' => count($repositories),
|
|
'repositories' => $repositories,
|
|
], 200),
|
|
]);
|
|
}
|
|
|
|
function githubPrivateRepositoryTestPrivateKeyForTeam(Team $team): PrivateKey
|
|
{
|
|
$rsaKey = openssl_pkey_new([
|
|
'private_key_bits' => 2048,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
]);
|
|
openssl_pkey_export($rsaKey, $pemKey);
|
|
|
|
return PrivateKey::create([
|
|
'name' => 'Test Key '.$team->id,
|
|
'private_key' => $pemKey,
|
|
'team_id' => $team->id,
|
|
]);
|
|
}
|
|
|
|
describe('GitHub Private Repository Component', function () {
|
|
test('github app selection uses coolbox loading border', function () {
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->assertSee('wire:loading.class="coolbox-loading"', false)
|
|
->assertSee('wire:target="loadRepositories('.$this->githubApp->id.')"', false)
|
|
->assertDontSee('loading-spinner', false);
|
|
});
|
|
|
|
test('loadRepositories fetches and displays repositories', function () {
|
|
$repos = [
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
['id' => 2, 'name' => 'beta-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
fakeGithubHttp($repos);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->assertSet('current_step', 'github_apps')
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSet('current_step', 'repository')
|
|
->assertSet('total_repositories_count', 2)
|
|
->assertSet('selected_repository_id', 1);
|
|
});
|
|
|
|
test('repository selection uses the searchable listbox and disables loading action', function () {
|
|
fakeGithubHttp([
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSee('class="listbox-panel searchable-listbox-panel"', false)
|
|
->assertSee('placeholder="Search repositories…"', false)
|
|
->assertSee('wire:loading.attr="disabled"', false)
|
|
->assertSee('wire:target="loadBranches,selected_repository_id"', false)
|
|
->assertDontSee('<datalist', false);
|
|
});
|
|
|
|
test('loadBranches fetches available branches and selects the repository default branch', function () {
|
|
$repository = [
|
|
'id' => 1,
|
|
'name' => 'alpha-repo',
|
|
'default_branch' => 'production',
|
|
'owner' => ['login' => 'testuser'],
|
|
];
|
|
|
|
fakeGithubHttp([$repository]);
|
|
Http::fake([
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
|
'token' => 'fake-installation-token',
|
|
], 201),
|
|
'https://api.github.com/installation/repositories*' => Http::response([
|
|
'total_count' => 1,
|
|
'repositories' => [$repository],
|
|
], 200),
|
|
'https://api.github.com/repos/testuser/alpha-repo/branches*' => Http::response([
|
|
['name' => 'main'],
|
|
['name' => 'feature/login'],
|
|
['name' => 'production'],
|
|
], 200),
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->call('loadBranches')
|
|
->assertSet('selected_branch_name', 'production')
|
|
->assertSet('branches', collect([
|
|
['name' => 'main'],
|
|
['name' => 'feature/login'],
|
|
['name' => 'production'],
|
|
]))
|
|
->assertSee('placeholder="Search branches…"', false);
|
|
});
|
|
|
|
test('continue button uses the shared submit loading indicator', function () {
|
|
fakeGithubHttp([
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->set('branches', collect([['name' => 'main']]))
|
|
->assertSee('type="submit"', false)
|
|
->assertSee('wire:target="submit"', false)
|
|
->assertSee('wire:loading.class="is-loading"', false);
|
|
});
|
|
|
|
test('loadRepositories rejects a github app owned by another team', function () {
|
|
$victimTeam = Team::factory()->create();
|
|
$victimPrivateKey = githubPrivateRepositoryTestPrivateKeyForTeam($victimTeam);
|
|
$victimGithubApp = GithubApp::create([
|
|
'name' => 'Victim GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 54321,
|
|
'installation_id' => 98765,
|
|
'client_id' => 'victim-client-id',
|
|
'client_secret' => 'victim-client-secret',
|
|
'webhook_secret' => 'victim-webhook-secret',
|
|
'private_key_id' => $victimPrivateKey->id,
|
|
'team_id' => $victimTeam->id,
|
|
'is_public' => false,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Http::fake();
|
|
|
|
expect(fn () => Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $victimGithubApp->id)
|
|
)->toThrow(ModelNotFoundException::class);
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('mount lists another teams system wide github app', function () {
|
|
$victimTeam = Team::factory()->create();
|
|
$victimPrivateKey = githubPrivateRepositoryTestPrivateKeyForTeam($victimTeam);
|
|
$systemWideGithubApp = GithubApp::create([
|
|
'name' => 'System Wide GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 54321,
|
|
'installation_id' => 98765,
|
|
'client_id' => 'system-client-id',
|
|
'client_secret' => 'system-client-secret',
|
|
'webhook_secret' => 'system-webhook-secret',
|
|
'private_key_id' => $victimPrivateKey->id,
|
|
'team_id' => $victimTeam->id,
|
|
'is_public' => false,
|
|
'is_system_wide' => true,
|
|
]);
|
|
|
|
$component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app']);
|
|
|
|
expect($component->get('github_apps')->pluck('id')->all())
|
|
->toContain($this->githubApp->id)
|
|
->toContain($systemWideGithubApp->id);
|
|
});
|
|
|
|
test('loadRepositories can use another teams system wide github app', function () {
|
|
$victimTeam = Team::factory()->create();
|
|
$victimPrivateKey = githubPrivateRepositoryTestPrivateKeyForTeam($victimTeam);
|
|
$systemWideGithubApp = GithubApp::create([
|
|
'name' => 'System Wide GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 54321,
|
|
'installation_id' => 67890,
|
|
'client_id' => 'system-client-id',
|
|
'client_secret' => 'system-client-secret',
|
|
'webhook_secret' => 'system-webhook-secret',
|
|
'private_key_id' => $victimPrivateKey->id,
|
|
'team_id' => $victimTeam->id,
|
|
'is_public' => false,
|
|
'is_system_wide' => true,
|
|
]);
|
|
$repos = [
|
|
['id' => 1, 'name' => 'system-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
fakeGithubHttp($repos);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $systemWideGithubApp->id)
|
|
->assertSet('current_step', 'repository')
|
|
->assertSet('total_repositories_count', 1)
|
|
->assertSet('selected_repository_id', 1);
|
|
});
|
|
|
|
test('github installation token is not stored as public component state', function () {
|
|
expect((new ReflectionClass(GithubPrivateRepository::class))->hasProperty('token'))->toBeFalse();
|
|
});
|
|
|
|
test('selected github app id cannot be tampered with from the client', function () {
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->set('selected_github_app_id', $this->githubApp->id);
|
|
})->throws(CannotUpdateLockedPropertyException::class);
|
|
|
|
test('loadRepositories can be called again to refresh the repository list', function () {
|
|
$initialRepos = [
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
$updatedRepos = [
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
['id' => 2, 'name' => 'beta-repo', 'owner' => ['login' => 'testuser']],
|
|
['id' => 3, 'name' => 'gamma-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
$callCount = 0;
|
|
Http::fake([
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
|
'token' => 'fake-installation-token',
|
|
], 201),
|
|
'https://api.github.com/installation/repositories*' => function () use (&$callCount, $initialRepos, $updatedRepos) {
|
|
$callCount++;
|
|
$repos = $callCount === 1 ? $initialRepos : $updatedRepos;
|
|
|
|
return Http::response([
|
|
'total_count' => count($repos),
|
|
'repositories' => $repos,
|
|
], 200);
|
|
},
|
|
]);
|
|
|
|
$component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSet('total_repositories_count', 1);
|
|
|
|
// Simulate new repos becoming available after changing access on GitHub
|
|
$component
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSet('total_repositories_count', 3)
|
|
->assertSet('current_step', 'repository');
|
|
});
|
|
|
|
test('loadRepositories resets branches when refreshing', function () {
|
|
$repos = [
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
fakeGithubHttp($repos);
|
|
|
|
$component = Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id);
|
|
|
|
// Manually set branches to simulate a previous branch load
|
|
$component->set('branches', collect([['name' => 'main'], ['name' => 'develop']]));
|
|
$component->set('total_branches_count', 2);
|
|
|
|
// Refresh repositories should reset branches
|
|
fakeGithubHttp($repos);
|
|
|
|
$component
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSet('total_branches_count', 0)
|
|
->assertSet('branches', collect());
|
|
});
|
|
|
|
test('refresh button is visible when repositories are loaded', function () {
|
|
$repos = [
|
|
['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']],
|
|
];
|
|
|
|
fakeGithubHttp($repos);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertSee('Refresh');
|
|
});
|
|
|
|
test('loadRepositories shows an error when the installation is not found', function () {
|
|
Http::fake([
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response(['message' => 'Not Found'], 404),
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'installation 67890 was not found'))
|
|
->assertSet('current_step', 'github_apps');
|
|
});
|
|
|
|
test('loadRepositories shows the GitHub error when listing repositories fails', function () {
|
|
Http::fake([
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
|
'token' => 'fake-installation-token',
|
|
], 201),
|
|
'https://api.github.com/installation/repositories*' => Http::response(['message' => 'Resource not accessible by integration'], 403),
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'Resource not accessible by integration'))
|
|
->assertSet('current_step', 'github_apps');
|
|
});
|
|
|
|
test('loadBranches stops and shows the GitHub error when a later branch page fails', function () {
|
|
$repository = ['id' => 1, 'name' => 'alpha-repo', 'owner' => ['login' => 'testuser']];
|
|
fakeGithubHttp([$repository]);
|
|
Http::fake([
|
|
'https://api.github.com/repos/testuser/alpha-repo/branches*' => Http::sequence()
|
|
->push(collect(range(1, 100))->map(fn (int $i) => ['name' => "branch-{$i}"])->all(), 200)
|
|
->whenEmpty(Http::response(['message' => 'API rate limit exceeded'], 403)),
|
|
]);
|
|
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->call('loadRepositories', $this->githubApp->id)
|
|
->call('loadBranches')
|
|
->assertDispatched('error', fn (string $event, array $params) => str_contains($params[0], 'API rate limit exceeded'));
|
|
});
|
|
|
|
test('refresh button is not visible before repositories are loaded', function () {
|
|
Livewire::test(GithubPrivateRepository::class, ['type' => 'private-gh-app'])
|
|
->assertDontSee('Refresh');
|
|
});
|
|
});
|