diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php index d71593048..16dfc8136 100644 --- a/app/Http/Controllers/Api/GithubController.php +++ b/app/Http/Controllers/Api/GithubController.php @@ -256,7 +256,9 @@ public function create_github_app(Request $request) 'uuid' => Str::uuid(), 'name' => $request->input('name'), 'organization' => $request->input('organization'), - 'api_url' => githubApiUrlFromHtmlUrl($request->input('html_url')), + 'api_url' => filled($request->input('api_url')) + ? $request->input('api_url') + : githubApiUrlFromHtmlUrl($request->input('html_url')), 'html_url' => $request->input('html_url'), 'custom_user' => $request->input('custom_user', 'git'), 'custom_port' => $request->input('custom_port', 22), @@ -650,7 +652,7 @@ public function update_github_app(Request $request, $github_app_id) if (array_key_exists('organization', $payload)) { $payload['organization'] = normalizeGithubOrganization($payload['organization']); } - if (isset($payload['html_url'])) { + if (isset($payload['html_url']) && ! filled($payload['api_url'] ?? null)) { $payload['api_url'] = githubApiUrlFromHtmlUrl($payload['html_url']); } diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 9c9f87469..a24ed9ce3 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -79,6 +79,8 @@ class Change extends Component public string $activeTab = 'general'; + private bool $shouldDeriveApiUrlAfterHtmlUrlUpdate = false; + protected function rules(): array { return [ @@ -104,9 +106,17 @@ protected function rules(): array ]; } + public function updatingHtmlUrl(): void + { + $this->shouldDeriveApiUrlAfterHtmlUrlUpdate = blank($this->apiUrl) + || $this->apiUrl === githubApiUrlFromHtmlUrl($this->htmlUrl); + } + public function updatedHtmlUrl(): void { - $this->apiUrl = githubApiUrlFromHtmlUrl($this->htmlUrl); + if ($this->shouldDeriveApiUrlAfterHtmlUrlUpdate) { + $this->apiUrl = githubApiUrlFromHtmlUrl($this->htmlUrl); + } } public function boot() @@ -126,7 +136,9 @@ private function syncData(bool $toModel = false): void if ($toModel) { // Sync TO model (before save) $this->organization = normalizeGithubOrganization($this->organization); - $this->apiUrl = githubApiUrlFromHtmlUrl($this->htmlUrl); + $this->apiUrl = filled($this->apiUrl) + ? $this->apiUrl + : githubApiUrlFromHtmlUrl($this->htmlUrl); $this->github_app->name = $this->name; $this->github_app->organization = $this->organization; @@ -354,7 +366,9 @@ public function submit() $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); $this->organization = normalizeGithubOrganization($this->organization); - $this->apiUrl = githubApiUrlFromHtmlUrl($this->htmlUrl); + $this->apiUrl = filled($this->apiUrl) + ? $this->apiUrl + : githubApiUrlFromHtmlUrl($this->htmlUrl); $this->validate(); $this->syncData(true); diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index 9965fe4b7..6a5bf6e60 100644 --- a/app/Livewire/Source/Github/Create.php +++ b/app/Livewire/Source/Github/Create.php @@ -5,6 +5,7 @@ use App\Models\GithubApp; use App\Rules\SafeExternalUrl; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Validation\ValidationException; use Livewire\Component; class Create extends Component @@ -25,14 +26,24 @@ class Create extends Component public bool $is_system_wide = false; + private bool $shouldDeriveApiUrlAfterHtmlUrlUpdate = false; + public function mount() { $this->name = substr(generate_random_name(), 0, 30); } + public function updatingHtmlUrl(): void + { + $this->shouldDeriveApiUrlAfterHtmlUrlUpdate = blank($this->api_url) + || $this->api_url === githubApiUrlFromHtmlUrl($this->html_url); + } + public function updatedHtmlUrl(): void { - $this->api_url = githubApiUrlFromHtmlUrl($this->html_url); + if ($this->shouldDeriveApiUrlAfterHtmlUrlUpdate) { + $this->api_url = githubApiUrlFromHtmlUrl($this->html_url); + } } public function createGitHubApp() @@ -41,7 +52,9 @@ public function createGitHubApp() $this->authorize('createAnyResource'); $this->organization = normalizeGithubOrganization($this->organization); - $this->api_url = githubApiUrlFromHtmlUrl($this->html_url); + $this->api_url = filled($this->api_url) + ? $this->api_url + : githubApiUrlFromHtmlUrl($this->html_url); $this->validate([ 'name' => 'required|string', @@ -68,6 +81,8 @@ public function createGitHubApp() } return redirectRoute($this, 'source.github.show', ['github_app_uuid' => $github_app->uuid]); + } catch (ValidationException $e) { + throw $e; } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/tests/Feature/Api/GithubAppsListApiTest.php b/tests/Feature/Api/GithubAppsListApiTest.php index 56deae733..6357c25ba 100644 --- a/tests/Feature/Api/GithubAppsListApiTest.php +++ b/tests/Feature/Api/GithubAppsListApiTest.php @@ -1,6 +1,7 @@ 0, 'is_api_enabled' => true]); + // Create a team with owner $this->team = Team::factory()->create(); $this->user = User::factory()->create(); $this->team->members()->attach($this->user->id, ['role' => 'owner']); + session(['currentTeam' => $this->team]); // Create an API token for the user - $this->token = $this->user->createToken('test-token', ['*'], $this->team->id); + $this->token = $this->user->createToken('test-token'); $this->bearerToken = $this->token->plainTextToken; // Create a private key for the team $this->privateKey = PrivateKey::create([ 'name' => 'Test Key', - 'private_key' => 'test-private-key-content', + 'private_key' => validGithubAppsApiPrivateKey(), 'team_id' => $this->team->id, ]); }); @@ -139,7 +143,8 @@ function validGithubAppsApiPrivateKey(): string $otherTeam = Team::factory()->create(); $otherUser = User::factory()->create(); $otherTeam->members()->attach($otherUser->id, ['role' => 'owner']); - $otherToken = $otherUser->createToken('other-token', ['*'], $otherTeam->id); + session(['currentTeam' => $otherTeam]); + $otherToken = $otherUser->createToken('other-token'); // System-wide apps should be visible to other teams $response = $this->withHeaders([ @@ -173,7 +178,7 @@ function validGithubAppsApiPrivateKey(): string $otherTeam = Team::factory()->create(); $otherPrivateKey = PrivateKey::create([ 'name' => 'Other Key', - 'private_key' => 'other-key', + 'private_key' => validGithubAppsApiPrivateKey(), 'team_id' => $otherTeam->id, ]); GithubApp::create([ @@ -249,7 +254,7 @@ function validGithubAppsApiPrivateKey(): string ])->postJson('/api/v1/github-apps', [ 'name' => 'GHE App', 'organization' => '/octocorp/', - 'html_url' => 'https://octocorp.ghe.com', + 'html_url' => 'https://github.ghe.com', 'app_id' => 12345, 'installation_id' => 67890, 'client_id' => 'test-client-id', @@ -261,12 +266,36 @@ function validGithubAppsApiPrivateKey(): string $response->assertCreated() ->assertJsonFragment([ 'organization' => 'octocorp', - 'api_url' => 'https://api.octocorp.ghe.com', - 'html_url' => 'https://octocorp.ghe.com', + 'api_url' => 'https://api.github.ghe.com', + 'html_url' => 'https://github.ghe.com', ]); }); - test('normalizes ghe dot com api url when updating github apps', function () { + test('preserves provided api url when creating github apps', function () { + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->postJson('/api/v1/github-apps', [ + 'name' => 'GHE App', + 'organization' => '/octocorp/', + 'api_url' => 'https://github.com/api/v3', + 'html_url' => 'https://github.com', + 'app_id' => 12345, + 'installation_id' => 67890, + 'client_id' => 'test-client-id', + 'client_secret' => 'test-client-secret', + 'webhook_secret' => 'test-webhook-secret', + 'private_key_uuid' => $this->privateKey->uuid, + ]); + + $response->assertCreated() + ->assertJsonFragment([ + 'organization' => 'octocorp', + 'api_url' => 'https://github.com/api/v3', + 'html_url' => 'https://github.com', + ]); + }); + + test('preserves provided api url when updating github apps', function () { $githubApp = GithubApp::create([ 'name' => 'GHE App', 'api_url' => 'https://github.company.internal/api/v3', @@ -285,12 +314,12 @@ function validGithubAppsApiPrivateKey(): string $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$this->bearerToken, ])->patchJson("/api/v1/github-apps/{$githubApp->id}", [ - 'html_url' => 'https://octocorp.ghe.com', - 'api_url' => 'https://octocorp.ghe.com/api/v3', + 'html_url' => 'https://github.com', + 'api_url' => 'https://github.com/api/v3', ]); $response->assertSuccessful() - ->assertJsonPath('data.api_url', 'https://api.octocorp.ghe.com'); + ->assertJsonPath('data.api_url', 'https://github.com/api/v3'); }); test('rejects invalid organization when creating github apps', function () { diff --git a/tests/Feature/Application/GithubSourceChangeTest.php b/tests/Feature/Application/GithubSourceChangeTest.php index f3ee7bedf..40a4e7314 100644 --- a/tests/Feature/Application/GithubSourceChangeTest.php +++ b/tests/Feature/Application/GithubSourceChangeTest.php @@ -419,7 +419,7 @@ function validPrivateKey(): string expect($githubApp->private_key_id)->toBe($privateKey->id); }); - test('normalizes resolvable ghe dot com api url when saving github app settings', function () { + 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', @@ -437,10 +437,10 @@ function validPrivateKey(): string ->set('apiUrl', 'https://github.ghe.com/api/v3') ->call('submit') ->assertDispatched('success') - ->assertSet('apiUrl', 'https://api.github.ghe.com'); + ->assertSet('apiUrl', 'https://github.ghe.com/api/v3'); $githubApp->refresh(); - expect($githubApp->api_url)->toBe('https://api.github.ghe.com'); + expect($githubApp->api_url)->toBe('https://github.ghe.com/api/v3'); }); test('rejects invalid github organization values', function () { diff --git a/tests/Feature/Application/GithubSourceCreateTest.php b/tests/Feature/Application/GithubSourceCreateTest.php index 82343092c..a3eb13d54 100644 --- a/tests/Feature/Application/GithubSourceCreateTest.php +++ b/tests/Feature/Application/GithubSourceCreateTest.php @@ -72,8 +72,8 @@ Livewire::test(Create::class) ->assertSuccessful() ->set('name', 'enterprise-app') - ->set('api_url', 'https://github.enterprise.com/api/v3') - ->set('html_url', 'https://github.enterprise.com') + ->set('api_url', 'https://github.ghe.com/api/v3') + ->set('html_url', 'https://github.ghe.com') ->set('custom_user', 'git-custom') ->set('custom_port', 2222) ->call('createGitHubApp') @@ -82,12 +82,28 @@ $githubApp = GithubApp::where('name', 'enterprise-app')->first(); expect($githubApp)->not->toBeNull(); - expect($githubApp->api_url)->toBe('https://github.enterprise.com/api/v3'); - expect($githubApp->html_url)->toBe('https://github.enterprise.com'); + expect($githubApp->api_url)->toBe('https://github.ghe.com/api/v3'); + expect($githubApp->html_url)->toBe('https://github.ghe.com'); expect($githubApp->custom_user)->toBe('git-custom'); expect($githubApp->custom_port)->toBe(2222); }); + test('preserves custom github enterprise api url when creating github app', function () { + Livewire::test(Create::class) + ->assertSuccessful() + ->set('name', 'ghe-custom-api-app') + ->set('api_url', 'https://github.ghe.com/api/v3') + ->set('html_url', 'https://github.ghe.com') + ->call('createGitHubApp') + ->assertRedirect(); + + $githubApp = GithubApp::where('name', 'ghe-custom-api-app')->first(); + + expect($githubApp)->not->toBeNull(); + expect($githubApp->api_url)->toBe('https://github.ghe.com/api/v3'); + expect($githubApp->html_url)->toBe('https://github.ghe.com'); + }); + test('validates required fields', function () { Livewire::test(Create::class) ->assertSuccessful()