mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 10:42:23 +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.
977 lines
36 KiB
PHP
977 lines
36 KiB
PHP
<?php
|
|
|
|
use App\Livewire\Source\Github\Change;
|
|
use App\Models\GithubApp;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\PrivateKey;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
// Create a team with owner
|
|
$this->team = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
|
|
|
// Set current team
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
InstanceSettings::forceCreate([
|
|
'id' => 0,
|
|
'fqdn' => null,
|
|
'public_ipv4' => null,
|
|
'public_ipv6' => null,
|
|
]);
|
|
});
|
|
|
|
function validPrivateKey(): string
|
|
{
|
|
$key = openssl_pkey_new([
|
|
'private_key_bits' => 2048,
|
|
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
|
]);
|
|
|
|
openssl_pkey_export($key, $privateKey);
|
|
|
|
return $privateKey;
|
|
}
|
|
|
|
describe('GitHub Source Change Component', function () {
|
|
test('all github app form controls declare explicit authorization', function () {
|
|
$view = file_get_contents(resource_path('views/livewire/source/github/change.blade.php'));
|
|
|
|
preg_match_all(
|
|
'/<x-forms\.(button|input|select|checkbox)\b(?![^>]*\bcanGate=)[^>]*>/s',
|
|
$view,
|
|
$matches,
|
|
PREG_OFFSET_CAPTURE
|
|
);
|
|
|
|
$missingAuthorization = collect($matches[0])
|
|
->map(fn (array $match): string => 'Line '.(substr_count(substr($view, 0, $match[1]), PHP_EOL) + 1).': '.trim(preg_replace('/\s+/', ' ', $match[0])))
|
|
->all();
|
|
|
|
expect($missingAuthorization)->toBeEmpty();
|
|
});
|
|
|
|
test('deletes an incomplete github app and redirects without a render error', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Incomplete GitHub App',
|
|
'organization' => 'coollabsio',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSee('Incomplete GitHub App')
|
|
->call('delete')
|
|
->assertHasNoErrors()
|
|
->assertRedirect(route('source.all'))
|
|
->assertSet('github_app', null)
|
|
->call('$refresh');
|
|
|
|
expect(GithubApp::find($githubApp->id))->toBeNull();
|
|
});
|
|
|
|
test('can mount with newly created github app with null app_id', function () {
|
|
// Create a GitHub app without app_id (simulating a newly created source)
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
// app_id is intentionally not set (null in database)
|
|
]);
|
|
|
|
// Test that the component can mount without errors
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('appId', null)
|
|
->assertSet('installationId', null)
|
|
->assertSet('clientId', null)
|
|
->assertSet('clientSecret', null)
|
|
->assertSet('webhookSecret', null)
|
|
->assertSet('privateKeyId', null);
|
|
});
|
|
|
|
test('creates one-time states for manifest conversion and installation callbacks', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
$component = Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful();
|
|
|
|
$manifestState = $component->get('manifestState');
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
parse_str(parse_url($installationUrl, PHP_URL_QUERY), $query);
|
|
$installState = $query['state'] ?? null;
|
|
|
|
expect($manifestState)->not->toBeEmpty()
|
|
->and($installState)->not->toBeEmpty()
|
|
->and($installState)->not->toBe($manifestState)
|
|
->and($installationUrl)->not->toContain($githubApp->uuid)
|
|
->and(Cache::get('github-app-setup-state:'.hash('sha256', $manifestState)))
|
|
->toMatchArray([
|
|
'action' => 'manifest',
|
|
'github_app_id' => $githubApp->id,
|
|
'team_id' => $githubApp->team_id,
|
|
])
|
|
->and(Cache::get('github-app-setup-state:'.hash('sha256', $installState)))
|
|
->toMatchArray([
|
|
'action' => 'install',
|
|
'github_app_id' => $githubApp->id,
|
|
'team_id' => $githubApp->team_id,
|
|
]);
|
|
});
|
|
|
|
test('installation path is generated from the provided github app instance', function () {
|
|
$githubApp = new GithubApp;
|
|
$githubApp->forceFill([
|
|
'id' => 123,
|
|
'name' => 'Provided GitHub App',
|
|
'html_url' => 'https://github.example.com',
|
|
'team_id' => 456,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
parse_str(parse_url($installationUrl, PHP_URL_QUERY), $query);
|
|
$installState = $query['state'] ?? null;
|
|
|
|
expect($installationUrl)->toStartWith('https://github.example.com/github-apps/provided-git-hub-app/installations/new?')
|
|
->and($installState)->not->toBeEmpty()
|
|
->and(Cache::get('github-app-setup-state:'.hash('sha256', $installState)))
|
|
->toMatchArray([
|
|
'action' => 'install',
|
|
'github_app_id' => 123,
|
|
'team_id' => 456,
|
|
]);
|
|
});
|
|
|
|
test('ghe.com installation path uses github cloud owner scoped route', function () {
|
|
$githubApp = new GithubApp;
|
|
$githubApp->forceFill([
|
|
'id' => 123,
|
|
'name' => 'provided-github-app',
|
|
'organization' => 'acme-enterprise',
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'team_id' => 456,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
parse_str(parse_url($installationUrl, PHP_URL_QUERY), $query);
|
|
$installState = $query['state'] ?? null;
|
|
|
|
expect($installationUrl)->toStartWith('https://octocorp.ghe.com/apps/acme-enterprise/provided-github-app/installations/new?')
|
|
->and($installState)->not->toBeEmpty()
|
|
->and(Cache::get('github-app-setup-state:'.hash('sha256', $installState)))
|
|
->toMatchArray([
|
|
'action' => 'install',
|
|
'github_app_id' => 123,
|
|
'team_id' => 456,
|
|
]);
|
|
});
|
|
|
|
test('installation path is pure and never calls github or mutates the app', function () {
|
|
Http::fake();
|
|
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'github-app-local-name',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
'is_git_related' => true,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Local Display Name',
|
|
'organization' => 'acme-enterprise',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
|
|
Http::assertNothingSent();
|
|
|
|
expect($installationUrl)->toStartWith('https://octocorp.ghe.com/apps/acme-enterprise/local-display-name/installations/new?')
|
|
->and($githubApp->refresh()->name)->toBe('Local Display Name')
|
|
->and($privateKey->refresh()->name)->toBe('github-app-local-name');
|
|
});
|
|
|
|
test('syncGithubAppName persists the github slug and renames the private key', function () {
|
|
Http::fake([
|
|
'*/app' => Http::response(['slug' => 'actual-github-slug']),
|
|
]);
|
|
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'github-app-local-name',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
'is_git_related' => true,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Local Display Name',
|
|
'organization' => 'acme-enterprise',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
$appSlug = syncGithubAppName($githubApp, true);
|
|
|
|
expect($appSlug)->toBe('actual-github-slug')
|
|
->and($githubApp->refresh()->name)->toBe('actual-github-slug')
|
|
->and($privateKey->refresh()->name)->toBe('github-app-actual-github-slug');
|
|
});
|
|
|
|
test('saving settings keeps key selection within the source team', function (string $action) {
|
|
$otherTeam = Team::factory()->create();
|
|
$foreignKey = PrivateKey::create([
|
|
'name' => 'other-team-key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $otherTeam->id,
|
|
]);
|
|
$ownKey = PrivateKey::create([
|
|
'name' => 'own-team-key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$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,
|
|
'private_key_id' => $ownKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->set('privateKeyId', $foreignKey->id)
|
|
->call($action);
|
|
|
|
expect($githubApp->refresh()->private_key_id)->toBe($ownKey->id);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->set('privateKeyId', $ownKey->id)
|
|
->call($action)
|
|
->assertHasNoErrors();
|
|
|
|
expect($githubApp->refresh()->private_key_id)->toBe($ownKey->id);
|
|
})->with(['submit', 'instantSave']);
|
|
|
|
test('github app tokens only use a key from the source team', function () {
|
|
Http::fake();
|
|
|
|
$otherTeam = Team::factory()->create();
|
|
$foreignKey = PrivateKey::create([
|
|
'name' => 'other-team-key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $otherTeam->id,
|
|
]);
|
|
|
|
$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,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
GithubApp::query()->whereKey($githubApp->id)->update(['private_key_id' => $foreignKey->id]);
|
|
$githubApp->refresh();
|
|
|
|
expect(fn () => generateGithubJwt($githubApp))->toThrow(RuntimeException::class)
|
|
->and(syncGithubAppName($githubApp))->toBeNull()
|
|
->and($foreignKey->refresh()->name)->toBe('other-team-key');
|
|
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('ghe.com installation path encodes the organization segment', function () {
|
|
$githubApp = new GithubApp;
|
|
$githubApp->forceFill([
|
|
'id' => 123,
|
|
'name' => 'provided-github-app',
|
|
'organization' => '/acme enterprise/',
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'team_id' => 456,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
|
|
expect($installationUrl)->toStartWith('https://octocorp.ghe.com/apps/acme%20enterprise/provided-github-app/installations/new?');
|
|
});
|
|
|
|
test('ghe.com installation path encodes plus signs in the organization segment', function () {
|
|
$githubApp = new GithubApp;
|
|
$githubApp->forceFill([
|
|
'id' => 123,
|
|
'name' => 'Provided GitHub App',
|
|
'organization' => 'octo+corp',
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'team_id' => 456,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
|
|
expect($installationUrl)->toStartWith('https://octocorp.ghe.com/apps/octo%2Bcorp/provided-git-hub-app/installations/new?');
|
|
});
|
|
|
|
test('ghe.com installation path keeps app scoped fallback when organization is blank', function () {
|
|
$githubApp = new GithubApp;
|
|
$githubApp->forceFill([
|
|
'id' => 123,
|
|
'name' => 'provided-github-app',
|
|
'organization' => null,
|
|
'html_url' => 'https://octocorp.ghe.com',
|
|
'team_id' => 456,
|
|
]);
|
|
|
|
$installationUrl = getInstallationPath($githubApp);
|
|
|
|
expect($installationUrl)->toStartWith('https://octocorp.ghe.com/apps/provided-github-app/installations/new?');
|
|
});
|
|
|
|
test('defaults webhook endpoint to app url when it is the first available endpoint', function () {
|
|
config(['app.url' => 'http://localhost:8000']);
|
|
|
|
InstanceSettings::findOrFail(0)->update([
|
|
'fqdn' => null,
|
|
'public_ipv4' => null,
|
|
'public_ipv6' => null,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('webhook_endpoint', 'http://localhost:8000');
|
|
});
|
|
|
|
test('custom webhook endpoint is selected explicitly with a checkbox', function () {
|
|
config(['app.url' => 'http://localhost:8000']);
|
|
|
|
InstanceSettings::findOrFail(0)->update([
|
|
'fqdn' => 'http://staging.example.com',
|
|
'public_ipv4' => '84.1.202.183',
|
|
'public_ipv6' => null,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('use_custom_webhook_endpoint', false)
|
|
->set('custom_webhook_endpoint', 'https://staging.example.com')
|
|
->set('use_custom_webhook_endpoint', true)
|
|
->assertSet('webhook_endpoint', 'http://staging.example.com')
|
|
->assertSet('custom_webhook_endpoint', 'https://staging.example.com')
|
|
->assertSet('use_custom_webhook_endpoint', true)
|
|
->assertSee('Webhook endpoint')
|
|
->assertSee('Use a custom endpoint')
|
|
->assertSee('Custom endpoint')
|
|
->assertSee('createGithubApp(webhookEndpoint, useCustomWebhookEndpoint, customWebhookEndpoint');
|
|
});
|
|
|
|
test('can mount with fully configured github app', function () {
|
|
Http::fake([
|
|
'https://api.github.com/app' => Http::response(['slug' => 'test-github-app']),
|
|
]);
|
|
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$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' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('appId', 12345)
|
|
->assertSet('installationId', 67890)
|
|
->assertSet('clientId', 'test-client-id')
|
|
->assertSet('clientSecret', 'test-client-secret')
|
|
->assertSet('webhookSecret', 'test-webhook-secret')
|
|
->assertSet('privateKeyId', $privateKey->id);
|
|
});
|
|
|
|
test('can update github app from null to valid values', function () {
|
|
Http::fake([
|
|
'https://api.github.com/app' => Http::response(['slug' => 'test-github-app']),
|
|
]);
|
|
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->set('appId', 12345)
|
|
->set('installationId', 67890)
|
|
->set('clientId', 'new-client-id')
|
|
->set('clientSecret', 'new-client-secret')
|
|
->set('webhookSecret', 'new-webhook-secret')
|
|
->set('privateKeyId', $privateKey->id)
|
|
->call('submit')
|
|
->assertDispatched('success');
|
|
|
|
// Verify the database was updated
|
|
$githubApp->refresh();
|
|
expect($githubApp->app_id)->toBe(12345);
|
|
expect($githubApp->installation_id)->toBe(67890);
|
|
expect($githubApp->client_id)->toBe('new-client-id');
|
|
expect($githubApp->private_key_id)->toBe($privateKey->id);
|
|
});
|
|
|
|
test('preserves custom ghe api url when saving github app settings', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://github.ghe.com/api/v3',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->set('htmlUrl', 'https://github.ghe.com')
|
|
->set('apiUrl', 'https://github.ghe.com/api/v3')
|
|
->call('submit')
|
|
->assertDispatched('success')
|
|
->assertSet('apiUrl', 'https://github.ghe.com/api/v3');
|
|
|
|
$githubApp->refresh();
|
|
expect($githubApp->api_url)->toBe('https://github.ghe.com/api/v3');
|
|
});
|
|
|
|
test('rejects invalid github organization values', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->set('organization', 'octo/corp')
|
|
->call('submit')
|
|
->assertHasErrors(['organization']);
|
|
});
|
|
|
|
test('validation allows nullable values for app configuration', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
// Test that validation passes with null values
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('submit')
|
|
->assertHasNoErrors();
|
|
});
|
|
|
|
test('createGithubAppManually redirects to avoid morphing issues', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
// Test that createGithubAppManually redirects instead of updating in place
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('createGithubAppManually')
|
|
->assertRedirect(route('source.github.show', ['github_app_uuid' => $githubApp->uuid]));
|
|
|
|
// Verify the database was updated
|
|
$githubApp->refresh();
|
|
expect($githubApp->app_id)->toBe(1234567890);
|
|
expect($githubApp->installation_id)->toBe(1234567890);
|
|
});
|
|
|
|
test('checkPermissions validates required fields', function () {
|
|
// Create a GitHub app without app_id and private_key_id
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
// Test that checkPermissions fails with appropriate error
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('checkPermissions')
|
|
->assertDispatched('error', function ($event, $message) {
|
|
$message = is_array($message) ? implode(' ', $message) : $message;
|
|
|
|
return str_contains($message, 'App ID') && str_contains($message, 'Private Key');
|
|
});
|
|
});
|
|
|
|
test('checkPermissions validates private key exists', function () {
|
|
$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,
|
|
'private_key_id' => 99999, // Non-existent private key ID
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
// Test that checkPermissions fails when private key doesn't exist
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('checkPermissions')
|
|
->assertDispatched('error', function ($event, $message) {
|
|
$message = is_array($message) ? implode(' ', $message) : $message;
|
|
|
|
return str_contains($message, 'Private Key not found');
|
|
});
|
|
});
|
|
|
|
test('checkPermissions syncs refetched permissions into input fields', function () {
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$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' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'contents' => null,
|
|
'metadata' => null,
|
|
'pull_requests' => null,
|
|
]);
|
|
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'https://api.github.com/app' => Http::response([
|
|
'permissions' => [
|
|
'contents' => 'read',
|
|
'metadata' => 'read',
|
|
'pull_requests' => 'write',
|
|
],
|
|
]),
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('name', 'test-git-hub-app')
|
|
->assertSet('contents', null)
|
|
->assertSet('metadata', null)
|
|
->assertSet('pullRequests', null)
|
|
->call('checkPermissions')
|
|
->assertDispatched('success')
|
|
->assertSet('name', 'test-git-hub-app')
|
|
->assertSet('contents', 'read')
|
|
->assertSet('metadata', 'read')
|
|
->assertSet('pullRequests', 'write');
|
|
|
|
$githubApp->refresh();
|
|
|
|
expect($githubApp->contents)->toBe('read')
|
|
->and($githubApp->metadata)->toBe('read')
|
|
->and($githubApp->pull_requests)->toBe('write');
|
|
});
|
|
|
|
test('sync name uses normalized resolvable ghe dot com api url', function () {
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.ghe.com',
|
|
'html_url' => 'https://github.ghe.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'installation_id' => 67890,
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'https://api.github.ghe.com/app' => Http::response([
|
|
'slug' => 'octocorp-app',
|
|
]),
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('updateGithubAppName')
|
|
->assertDispatched('success');
|
|
|
|
Http::assertSent(fn ($request) => $request->url() === 'https://api.github.ghe.com/app');
|
|
});
|
|
|
|
test('isConnected is true only when app and installation are present', function () {
|
|
$incomplete = GithubApp::create([
|
|
'name' => 'Incomplete App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'is_public' => false,
|
|
]);
|
|
|
|
$connected = GithubApp::create([
|
|
'name' => 'Connected App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'installation_id' => 67890,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'is_public' => false,
|
|
]);
|
|
|
|
$public = new GithubApp([
|
|
'is_public' => true,
|
|
]);
|
|
|
|
expect($incomplete->isConnected())->toBeFalse()
|
|
->and($connected->isConnected())->toBeTrue()
|
|
->and($public->isConnected())->toBeTrue();
|
|
});
|
|
|
|
test('shows test connection for installed github apps', function () {
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Connected 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,
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('isConnected', true)
|
|
->assertSee('Test connection')
|
|
->assertSeeHtml('wire:click.prevent="testConnection"');
|
|
});
|
|
|
|
test('testConnection succeeds when github app credentials are valid', function () {
|
|
$privateKey = PrivateKey::create([
|
|
'name' => 'Test Key',
|
|
'private_key' => validPrivateKey(),
|
|
'team_id' => $this->team->id,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Connected 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,
|
|
'private_key_id' => $privateKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'https://api.github.com/app' => Http::response([
|
|
'name' => 'Coolify GitHub App',
|
|
'slug' => 'coolify-github-app',
|
|
]),
|
|
'https://api.github.com/app/installations/67890/access_tokens' => Http::response([
|
|
'token' => 'ghs_test_installation_token',
|
|
]),
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->call('testConnection')
|
|
->assertDispatched('success', 'Connection successful! Authenticated as GitHub App: Coolify GitHub App');
|
|
});
|
|
|
|
test('testConnection fails when github app is not fully installed', function () {
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Incomplete GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'app_id' => 12345,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSuccessful()
|
|
->assertSet('isConnected', false)
|
|
->call('testConnection')
|
|
->assertDispatched('error', 'GitHub App is not fully set up. Please complete installation first.');
|
|
});
|
|
|
|
test('sources list shows Connected for finished github apps', function () {
|
|
GithubApp::create([
|
|
'name' => 'Finished 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,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'is_public' => false,
|
|
]);
|
|
|
|
GithubApp::create([
|
|
'name' => 'Incomplete GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
'is_public' => false,
|
|
]);
|
|
|
|
$this->get(route('source.all'))
|
|
->assertSuccessful()
|
|
->assertSee('Finished GitHub App')
|
|
->assertSee('Connected')
|
|
->assertSee('Incomplete GitHub App')
|
|
->assertSee('Setup incomplete');
|
|
});
|
|
});
|
|
|
|
describe('GitHub App name sync after the user switches the session team', function () {
|
|
beforeEach(function () {
|
|
Storage::fake('ssh-keys');
|
|
$this->otherTeam = Team::factory()->create();
|
|
$this->otherTeam->members()->attach($this->user->id, ['role' => 'owner']);
|
|
$this->appKey = PrivateKey::create(['name' => 'App Key', 'private_key' => validPrivateKey(), 'team_id' => $this->team->id]);
|
|
$this->otherTeamKey = PrivateKey::create(['name' => 'Other Team Key', 'private_key' => validPrivateKey(), 'team_id' => $this->otherTeam->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,
|
|
'private_key_id' => $this->appKey->id,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Http::preventStrayRequests();
|
|
Http::fake([
|
|
'https://api.github.com/app' => Http::response(['slug' => 'synced-app']),
|
|
]);
|
|
|
|
$this->component = Livewire::withQueryParams(['github_app_uuid' => $this->githubApp->uuid])->test(Change::class);
|
|
session(['currentTeam' => $this->otherTeam]);
|
|
});
|
|
|
|
test('rejects a private key of the session team', function () {
|
|
$this->component->set('privateKeyId', $this->otherTeamKey->id)
|
|
->call('updateGithubAppName')
|
|
->assertDispatched('error', 'No private key found for this GitHub App.');
|
|
|
|
expect($this->githubApp->fresh()->private_key_id)->toBe($this->appKey->id);
|
|
Http::assertNothingSent();
|
|
});
|
|
|
|
test('syncs the name with a private key of the GitHub App team', function () {
|
|
$this->component->call('updateGithubAppName')
|
|
->assertDispatched('success');
|
|
|
|
expect($this->githubApp->fresh()->name)->toBe('synced-app');
|
|
});
|
|
});
|
|
|
|
describe('GitHub webhook endpoint options for IPv6 instance addresses', function () {
|
|
test('brackets the public IPv6 address and keeps IPv4 unchanged', function (string $ipv6) {
|
|
config(['app.port' => 8000]);
|
|
|
|
InstanceSettings::findOrFail(0)->update([
|
|
'public_ipv4' => '203.0.113.10',
|
|
'public_ipv6' => $ipv6,
|
|
]);
|
|
|
|
$githubApp = GithubApp::create([
|
|
'name' => 'Test GitHub App',
|
|
'api_url' => 'https://api.github.com',
|
|
'html_url' => 'https://github.com',
|
|
'custom_user' => 'git',
|
|
'custom_port' => 22,
|
|
'team_id' => $this->team->id,
|
|
'is_system_wide' => false,
|
|
]);
|
|
|
|
Livewire::withQueryParams(['github_app_uuid' => $githubApp->uuid])
|
|
->test(Change::class)
|
|
->assertSet('ipv4', 'http://203.0.113.10:8000')
|
|
->assertSet('ipv6', 'http://[2a01:4f8::1]:8000');
|
|
})->with([
|
|
'raw' => '2a01:4f8::1',
|
|
'bracketed' => '[2a01:4f8::1]',
|
|
]);
|
|
});
|