diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 69991f52e..acb93c93a 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -38,22 +38,23 @@ function githubUrlHost(?string $url): ?string /** * Build the scheme://host[:port] origin for a GitHub URL. * - * When the host cannot be parsed (e.g. a scheme-less or malformed URL such as - * "not-a-url"), the input is returned verbatim with any trailing slashes - * trimmed. Callers should pass already-validated URLs (see SafeExternalUrl), - * so this fallback only guards against unexpected input. + * This helper fails explicitly for blank, scheme-less, or malformed input when + * githubUrlHost() cannot parse a host, because returning the original input + * would not be a valid origin. Callers should pass already-validated URLs. * * @param string $url The URL to derive the origin from - * @return string The normalized origin, or the trimmed input when the host is unparseable + * @return string The normalized origin + * + * @throws InvalidArgumentException When the URL does not contain a parseable scheme and host */ function githubUrlOrigin(string $url): string { - $scheme = parse_url($url, PHP_URL_SCHEME) ?: 'https'; + $scheme = parse_url($url, PHP_URL_SCHEME); $host = githubUrlHost($url); $port = parse_url($url, PHP_URL_PORT); - if (! $host) { - return rtrim($url, '/'); + if (! is_string($scheme) || blank($scheme) || ! $host) { + throw new InvalidArgumentException('GitHub URL must include a valid scheme and host.'); } return $scheme.'://'.$host.($port ? ":{$port}" : ''); diff --git a/tests/Feature/Api/GithubAppsListApiTest.php b/tests/Feature/Api/GithubAppsListApiTest.php index 6357c25ba..8f3e6dac5 100644 --- a/tests/Feature/Api/GithubAppsListApiTest.php +++ b/tests/Feature/Api/GithubAppsListApiTest.php @@ -322,6 +322,32 @@ function validGithubAppsApiPrivateKey(): string ->assertJsonPath('data.api_url', 'https://github.com/api/v3'); }); + test('preserves provided api url when updating api url only', function () { + $githubApp = GithubApp::create([ + 'name' => 'GHE App', + 'api_url' => 'https://api.github.com', + '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_id' => $this->privateKey->id, + 'team_id' => $this->team->id, + 'is_system_wide' => false, + 'is_public' => false, + ]); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$this->bearerToken, + ])->patchJson("/api/v1/github-apps/{$githubApp->id}", [ + 'api_url' => 'https://github.com/api/v3', + ]); + + $response->assertSuccessful() + ->assertJsonPath('data.api_url', 'https://github.com/api/v3'); + }); + test('rejects invalid organization when creating github apps', function () { $response = $this->withHeaders([ 'Authorization' => 'Bearer '.$this->bearerToken, diff --git a/tests/Unit/GithubUrlHelpersTest.php b/tests/Unit/GithubUrlHelpersTest.php index 7c4ee6ff5..dcace7ebe 100644 --- a/tests/Unit/GithubUrlHelpersTest.php +++ b/tests/Unit/GithubUrlHelpersTest.php @@ -23,6 +23,14 @@ 'github enterprise server' => ['https://github.company.internal', 'https://github.company.internal/api/v3'], ]); +it('rejects malformed urls when deriving an origin', function (string $url) { + expect(fn () => githubUrlOrigin($url))->toThrow(InvalidArgumentException::class); +})->with([ + 'blank' => [''], + 'scheme-less' => ['github.company.internal'], + 'malformed' => ['not-a-url'], +]); + it('generates correct install paths for github cloud ghe cloud and ghes', function (array $attributes, string $expectedPrefix) { $githubApp = new GithubApp; $githubApp->forceFill(array_merge([