From 858b1906ec34e76950262e18135c0ecc5d22eb15 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 3 Jun 2026 09:33:46 +0200
Subject: [PATCH 1/2] Improve GitHub App setup flow
---
app/Http/Controllers/Webhook/Github.php | 55 ++++++--
app/Livewire/Source/Github/Change.php | 2 +
bootstrap/helpers/github.php | 22 ++--
.../livewire/source/github/change.blade.php | 7 +-
tests/Feature/GithubSourceChangeTest.php | 99 +++++++++++++++
.../Security/GithubAppSetupCallbackTest.php | 119 +++++++++++++++++-
6 files changed, 276 insertions(+), 28 deletions(-)
diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php
index 8e3ffcd7c..40c5cbdf0 100644
--- a/app/Http/Controllers/Webhook/Github.php
+++ b/app/Http/Controllers/Webhook/Github.php
@@ -11,6 +11,8 @@
use App\Models\GithubApp;
use App\Models\PrivateKey;
use Exception;
+use Illuminate\Http\Exceptions\HttpResponseException;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
@@ -539,19 +541,22 @@ public function redirect(Request $request)
public function install(Request $request)
{
- $source = (string) $request->query('source', '');
- abort_if(blank($source), 404);
-
- $github_app = GithubApp::ownedByCurrentTeam()->where('uuid', $source)->firstOrFail();
-
$setup_action = (string) $request->query('setup_action', '');
- if ($setup_action !== 'install') {
- return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
- }
+ abort_unless(in_array($setup_action, ['install', 'update'], true), 422, 'Invalid GitHub App setup action.');
$installation_id = (string) $request->query('installation_id', '');
abort_unless(ctype_digit($installation_id), 422, 'Missing GitHub App installation id.');
+ if ($setup_action === 'update') {
+ return $this->redirectAfterGithubAppInstallationUpdate($installation_id);
+ }
+
+ $github_app = $this->consumeGithubAppSetupState(
+ request: $request,
+ state: (string) $request->query('state', ''),
+ action: 'install',
+ );
+
abort_unless(
$this->githubInstallationBelongsToApp($github_app, $installation_id),
403,
@@ -564,6 +569,19 @@ public function install(Request $request)
return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
}
+ private function redirectAfterGithubAppInstallationUpdate(string $installation_id): RedirectResponse
+ {
+ $github_app = GithubApp::ownedByCurrentTeam()
+ ->where('installation_id', $installation_id)
+ ->first();
+
+ if ($github_app) {
+ return redirect()->route('source.github.show', ['github_app_uuid' => $github_app->uuid]);
+ }
+
+ return redirect()->route('source.all');
+ }
+
/**
* Verify that the given installation id actually belongs to this GitHub App.
*
@@ -596,11 +614,14 @@ private function githubInstallationBelongsToApp(GithubApp $github_app, string $i
private function consumeGithubAppSetupState(Request $request, string $state, string $action): GithubApp
{
- abort_if(blank($state), 404);
+ if (blank($state)) {
+ $this->rejectInvalidGithubAppSetupState($request);
+ }
$payload = Cache::pull($this->githubAppSetupStateCacheKey($state));
- abort_unless(is_array($payload), 404);
- abort_unless(data_get($payload, 'action') === $action, 404);
+ if (! is_array($payload) || data_get($payload, 'action') !== $action) {
+ $this->rejectInvalidGithubAppSetupState($request);
+ }
$team_id = $request->user()?->currentTeam()?->id;
abort_unless(! is_null($team_id) && (int) data_get($payload, 'team_id') === $team_id, 403);
@@ -610,6 +631,18 @@ private function consumeGithubAppSetupState(Request $request, string $state, str
->firstOrFail();
}
+ private function rejectInvalidGithubAppSetupState(Request $request): never
+ {
+ if ($request->expectsJson()) {
+ abort(404);
+ }
+
+ throw new HttpResponseException(
+ redirect()
+ ->route('source.all')
+ );
+ }
+
private function githubAppSetupStateCacheKey(string $state): string
{
return 'github-app-setup-state:'.hash('sha256', $state);
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 1470b95db..74ed812af 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -210,6 +210,8 @@ public function checkPermissions()
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
+ $this->syncData(false);
+
$this->dispatch('success', 'Github App permissions updated.');
} catch (\Throwable $e) {
// Provide better error message for unsupported key formats
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index 4a61960fb..bb819e4aa 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -4,6 +4,7 @@
use App\Models\GitlabApp;
use Carbon\Carbon;
use Carbon\CarbonImmutable;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Lcobucci\JWT\Encoding\ChainedFormatter;
@@ -20,7 +21,7 @@ function generateGithubToken(GithubApp $source, string $type)
$timeDiff = abs($serverTime->diffInSeconds($githubTime));
if ($timeDiff > 50) {
- throw new \Exception(
+ throw new Exception(
'System time is out of sync with GitHub API time:
'.
'- System time: '.$serverTime->format('Y-m-d H:i:s').' UTC
'.
'- GitHub time: '.$githubTime->format('Y-m-d H:i:s').' UTC
'.
@@ -60,7 +61,7 @@ function generateGithubToken(GithubApp $source, string $type)
return $response->json()['token'];
})(),
- default => throw new \InvalidArgumentException("Unsupported token type: {$type}")
+ default => throw new InvalidArgumentException("Unsupported token type: {$type}")
};
}
@@ -77,11 +78,11 @@ function generateGithubJwt(GithubApp $source)
function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true)
{
if (is_null($source)) {
- throw new \Exception('Source is required for API calls');
+ throw new Exception('Source is required for API calls');
}
if ($source->getMorphClass() !== GithubApp::class) {
- throw new \InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
+ throw new InvalidArgumentException("Unsupported source type: {$source->getMorphClass()}");
}
if ($source->is_public) {
@@ -100,7 +101,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
$errorMessage = data_get($response->json(), 'message', 'no error message found');
$remainingCalls = $response->header('X-RateLimit-Remaining', '0');
- throw new \Exception(
+ throw new Exception(
'GitHub API call failed:
'.
"Error: {$errorMessage}
".
'Rate Limit Status:
'.
@@ -116,13 +117,20 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
];
}
-function getInstallationPath(GithubApp $source)
+function getInstallationPath(GithubApp $source): string
{
$github = GithubApp::where('uuid', $source->uuid)->first();
$name = str(Str::kebab($github->name));
$installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps';
+ $state = Str::random(64);
- return "$github->html_url/$installation_path/$name/installations/new";
+ Cache::put('github-app-setup-state:'.hash('sha256', $state), [
+ 'action' => 'install',
+ 'github_app_id' => $github->id,
+ 'team_id' => $github->team_id,
+ ], now()->addMinutes(60));
+
+ return "$github->html_url/$installation_path/$name/installations/new?".http_build_query(['state' => $state]);
}
function getPermissionsPath(GithubApp $source)
diff --git a/resources/views/livewire/source/github/change.blade.php b/resources/views/livewire/source/github/change.blade.php
index d52e35646..dc9560a1f 100644
--- a/resources/views/livewire/source/github/change.blade.php
+++ b/resources/views/livewire/source/github/change.blade.php
@@ -351,9 +351,8 @@ class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-neutral-100 dark:b
function createGithubApp(webhook_endpoint, use_custom_webhook_endpoint, custom_webhook_endpoint, preview_deployment_permissions, administration) {
const {
organization,
- html_url,
- uuid
- } = @js($github_app->only(['organization', 'html_url', 'uuid']));
+ html_url
+ } = @js($github_app->only(['organization', 'html_url']));
const selectedEndpoint = webhook_endpoint ? webhook_endpoint.trim() : '';
const customEndpoint = custom_webhook_endpoint ? custom_webhook_endpoint.trim() : '';
if (use_custom_webhook_endpoint && !customEndpoint) {
@@ -401,7 +400,7 @@ function createGithubApp(webhook_endpoint, use_custom_webhook_endpoint, custom_w
callback_urls: [`${baseUrl}/login/github/app`],
public: false,
request_oauth_on_install: false,
- setup_url: `${webhookBaseUrl}/source/github/install?source=${uuid}`,
+ setup_url: `${webhookBaseUrl}/source/github/install`,
setup_on_update: true,
default_permissions,
default_events
diff --git a/tests/Feature/GithubSourceChangeTest.php b/tests/Feature/GithubSourceChangeTest.php
index 91296dd0f..e6f758682 100644
--- a/tests/Feature/GithubSourceChangeTest.php
+++ b/tests/Feature/GithubSourceChangeTest.php
@@ -7,6 +7,8 @@
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
+use Illuminate\Support\Facades\Cache;
+use Illuminate\Support\Facades\Http;
use Livewire\Livewire;
uses(RefreshDatabase::class);
@@ -84,6 +86,44 @@ function validPrivateKey(): string
->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('defaults webhook endpoint to app url when it is the first available endpoint', function () {
config(['app.url' => 'http://localhost:8000']);
@@ -305,4 +345,63 @@ function validPrivateKey(): string
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/zen' => Http::response('Keep it logically awesome.', 200, [
+ 'date' => now()->toRfc7231String(),
+ ]),
+ '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('contents', null)
+ ->assertSet('metadata', null)
+ ->assertSet('pullRequests', null)
+ ->call('checkPermissions')
+ ->assertDispatched('success')
+ ->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');
+ });
});
diff --git a/tests/Feature/Security/GithubAppSetupCallbackTest.php b/tests/Feature/Security/GithubAppSetupCallbackTest.php
index 9c6893fd1..f56a77d5f 100644
--- a/tests/Feature/Security/GithubAppSetupCallbackTest.php
+++ b/tests/Feature/Security/GithubAppSetupCallbackTest.php
@@ -199,8 +199,9 @@ function fakeGithubInstallationVerificationFailure(): void
it('requires authentication before processing github app install callbacks', function () {
Http::preventStrayRequests();
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
- $this->get('/webhooks/source/github/install?source='.$this->githubApp->uuid.'&setup_action=install&installation_id=123456')
+ $this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456')
->assertRedirect();
Http::assertNothingSent();
@@ -209,22 +210,110 @@ function fakeGithubInstallationVerificationFailure(): void
expect($this->githubApp->installation_id)->toBeNull();
});
-it('rejects github app install callbacks for an unknown github app', function () {
+it('rejects github app install callbacks with an app uuid as state', function () {
authenticateGithubSetupCallbackTest($this);
Http::preventStrayRequests();
- $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?source=does-not-exist&setup_action=install&installation_id=123456')
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?state='.$this->githubApp->uuid.'&setup_action=install&installation_id=123456')
->assertNotFound();
Http::assertNothingSent();
});
+it('redirects browser github app install callbacks with missing or expired state to sources', function () {
+ authenticateGithubSetupCallbackTest($this);
+ Http::preventStrayRequests();
+
+ $this->get('/webhooks/source/github/install?setup_action=install&installation_id=123456')
+ ->assertRedirect(route('source.all'));
+
+ $this->get('/webhooks/source/github/install?state=expired-state&setup_action=install&installation_id=123456')
+ ->assertRedirect(route('source.all'));
+
+ Http::assertNothingSent();
+});
+
+it('rejects github app setup states for the wrong callback action', function () {
+ authenticateGithubSetupCallbackTest($this);
+ Http::preventStrayRequests();
+ cacheGithubAppSetupState('manifest-state', 'manifest', $this->githubApp);
+ cacheGithubAppSetupState('install-state', 'install', $this->githubApp);
+
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?state=manifest-state&setup_action=install&installation_id=123456')
+ ->assertNotFound();
+
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/redirect?state=install-state&code=real-code')
+ ->assertNotFound();
+
+ Http::assertNothingSent();
+});
+
+it('allows github app install callbacks for repository update setup actions', function () {
+ authenticateGithubSetupCallbackTest($this);
+ configureGithubAppCredentials($this->githubApp);
+ $this->githubApp->forceFill(['installation_id' => 111111])->save();
+ Http::preventStrayRequests();
+
+ $this->get('/webhooks/source/github/install?setup_action=update&installation_id=111111')
+ ->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid]));
+
+ Http::assertNothingSent();
+
+ $this->githubApp->refresh();
+ expect($this->githubApp->installation_id)->toBe(111111);
+});
+
+it('redirects github app repository update callbacks without a matching source to the sources page', function () {
+ authenticateGithubSetupCallbackTest($this);
+ Http::preventStrayRequests();
+
+ $this->get('/webhooks/source/github/install?setup_action=update&installation_id=123456')
+ ->assertRedirect(route('source.all'));
+
+ Http::assertNothingSent();
+});
+
+it('rejects github app install callbacks for unknown setup actions', function () {
+ authenticateGithubSetupCallbackTest($this);
+ Http::preventStrayRequests();
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
+
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?state=valid-install-state&setup_action=remove&installation_id=123456')
+ ->assertUnprocessable();
+
+ Http::assertNothingSent();
+});
+
+it('rejects github app setup states from another team', function () {
+ authenticateGithubSetupCallbackTest($this);
+ Http::preventStrayRequests();
+
+ $otherTeam = Team::factory()->create();
+ $otherGithubApp = GithubApp::create([
+ 'name' => 'Other GitHub App',
+ 'api_url' => 'https://api.github.com',
+ 'html_url' => 'https://github.com',
+ 'custom_user' => 'git',
+ 'custom_port' => 22,
+ 'team_id' => $otherTeam->id,
+ 'is_system_wide' => false,
+ ]);
+
+ cacheGithubAppSetupState('other-team-state', 'manifest', $otherGithubApp);
+
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/redirect?state=other-team-state&code=real-code')
+ ->assertForbidden();
+
+ Http::assertNothingSent();
+});
+
it('rejects an installation id that github does not confirm belongs to the app', function () {
authenticateGithubSetupCallbackTest($this);
configureGithubAppCredentials($this->githubApp);
fakeGithubInstallationVerificationFailure();
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
- $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?source='.$this->githubApp->uuid.'&setup_action=install&installation_id=999999')
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=999999')
->assertForbidden();
$this->githubApp->refresh();
@@ -235,21 +324,39 @@ function fakeGithubInstallationVerificationFailure(): void
authenticateGithubSetupCallbackTest($this);
configureGithubAppCredentials($this->githubApp);
fakeGithubInstallationVerification($this->githubApp->app_id);
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
- $this->get('/webhooks/source/github/install?source='.$this->githubApp->uuid.'&setup_action=install&installation_id=123456')
+ $this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456')
->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid]));
$this->githubApp->refresh();
expect($this->githubApp->installation_id)->toBe(123456);
});
+it('rejects replayed github app install states', function () {
+ authenticateGithubSetupCallbackTest($this);
+ configureGithubAppCredentials($this->githubApp);
+ fakeGithubInstallationVerification($this->githubApp->app_id);
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
+
+ $this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456')
+ ->assertRedirect();
+
+ $this->withHeader('Accept', 'application/json')->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=123456')
+ ->assertNotFound();
+
+ $this->githubApp->refresh();
+ expect($this->githubApp->installation_id)->toBe(123456);
+});
+
it('allows reinstalling an already configured github app installation id', function () {
authenticateGithubSetupCallbackTest($this);
configureGithubAppCredentials($this->githubApp);
$this->githubApp->forceFill(['installation_id' => 111111])->save();
fakeGithubInstallationVerification($this->githubApp->app_id);
+ cacheGithubAppSetupState('valid-install-state', 'install', $this->githubApp);
- $this->get('/webhooks/source/github/install?source='.$this->githubApp->uuid.'&setup_action=install&installation_id=222222')
+ $this->get('/webhooks/source/github/install?state=valid-install-state&setup_action=install&installation_id=222222')
->assertRedirect(route('source.github.show', ['github_app_uuid' => $this->githubApp->uuid]));
$this->githubApp->refresh();
From a047971bc1568ff833047d3ca5b6fc4e53209aa7 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 3 Jun 2026 10:07:57 +0200
Subject: [PATCH 2/2] fix(github): use provided app for installation URLs
Generate GitHub App installation links and setup cache state from the
current app instance, and keep the Livewire app name in sync after
permission checks.
---
app/Livewire/Source/Github/Change.php | 1 +
bootstrap/helpers/github.php | 11 +++++------
tests/Feature/GithubSourceChangeTest.php | 25 ++++++++++++++++++++++++
3 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 74ed812af..648bfe6ee 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -211,6 +211,7 @@ public function checkPermissions()
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->syncData(false);
+ $this->name = str($this->github_app->name)->kebab();
$this->dispatch('success', 'Github App permissions updated.');
} catch (\Throwable $e) {
diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php
index bb819e4aa..0ec76f6fa 100644
--- a/bootstrap/helpers/github.php
+++ b/bootstrap/helpers/github.php
@@ -119,18 +119,17 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m
function getInstallationPath(GithubApp $source): string
{
- $github = GithubApp::where('uuid', $source->uuid)->first();
- $name = str(Str::kebab($github->name));
- $installation_path = $github->html_url === 'https://github.com' ? 'apps' : 'github-apps';
+ $name = str(Str::kebab($source->name));
+ $installation_path = $source->html_url === 'https://github.com' ? 'apps' : 'github-apps';
$state = Str::random(64);
Cache::put('github-app-setup-state:'.hash('sha256', $state), [
'action' => 'install',
- 'github_app_id' => $github->id,
- 'team_id' => $github->team_id,
+ 'github_app_id' => $source->id,
+ 'team_id' => $source->team_id,
], now()->addMinutes(60));
- return "$github->html_url/$installation_path/$name/installations/new?".http_build_query(['state' => $state]);
+ return "$source->html_url/$installation_path/$name/installations/new?".http_build_query(['state' => $state]);
}
function getPermissionsPath(GithubApp $source)
diff --git a/tests/Feature/GithubSourceChangeTest.php b/tests/Feature/GithubSourceChangeTest.php
index e6f758682..07bc2a2c3 100644
--- a/tests/Feature/GithubSourceChangeTest.php
+++ b/tests/Feature/GithubSourceChangeTest.php
@@ -124,6 +124,29 @@ function validPrivateKey(): string
]);
});
+ 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('defaults webhook endpoint to app url when it is the first available endpoint', function () {
config(['app.url' => 'http://localhost:8000']);
@@ -389,11 +412,13 @@ function validPrivateKey(): string
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');