0]); $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]); $privateKey = PrivateKey::create([ 'name' => 'test-key', 'private_key' => $validKey, 'team_id' => $this->team->id, ]); $this->githubApp = GithubApp::create([ 'name' => 'Test App', 'app_id' => 111, 'installation_id' => 222, 'client_id' => 'Iv1.abc', 'client_secret' => 'secret', 'webhook_secret' => 'hook-secret', 'private_key_id' => $privateKey->id, 'team_id' => $this->team->id, 'api_url' => 'https://api.github.com', 'html_url' => 'https://github.com', 'organization' => 'test-org', 'organization_self_hosted_runners' => 'write', ]); $this->server = Server::factory()->create([ 'team_id' => $this->team->id, 'private_key_id' => $privateKey->id, ]); }); describe('GithubRunners accessible repositories', function () { test('mount does not load repositories before frontend initialization', function () { GithubRunnerConfig::create([ 'server_id' => $this->server->id, 'github_app_id' => $this->githubApp->id, 'labels' => ['self-hosted', 'coolify'], 'max_runners' => 4, 'capacity_wait_timeout' => 60, 'runner_user' => 'runner', 'runner_base_dir' => '/opt/github-runners', 'is_enabled' => true, ]); Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 1, 'repositories' => [ ['full_name' => 'test-org/deferred-repo', 'name' => 'deferred-repo'], ], ], 200), ]); $component = Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->assertSet('selectedGithubAppId', $this->githubApp->id) ->assertSet('repositoriesLoaded', false) ->assertSet('accessibleRepositories', []); Http::assertNothingSent(); $component->call('initializeRepositories') ->assertSet('repositoriesLoaded', true) ->assertSet('accessibleRepositories', ['test-org/deferred-repo']); }); test('initializeRepositories loads repositories only once for preselected app', function () { GithubRunnerConfig::create([ 'server_id' => $this->server->id, 'github_app_id' => $this->githubApp->id, 'labels' => ['self-hosted', 'coolify'], 'max_runners' => 4, 'capacity_wait_timeout' => 60, 'runner_user' => 'runner', 'runner_base_dir' => '/opt/github-runners', 'is_enabled' => true, ]); Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 1, 'repositories' => [ ['full_name' => 'test-org/only-once', 'name' => 'only-once'], ], ], 200), ]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->assertSet('selectedGithubAppId', $this->githubApp->id) ->call('initializeRepositories') ->assertSet('accessibleRepositories', ['test-org/only-once']); Http::assertSentCount(3); }); test('loadAccessibleRepositories populates accessibleRepositories from GitHub API', function () { Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 2, 'repositories' => [ ['full_name' => 'test-org/repo-b', 'name' => 'repo-b'], ['full_name' => 'test-org/repo-a', 'name' => 'repo-a'], ], ], 200), ]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->set('selectedGithubAppId', $this->githubApp->id) ->call('loadAccessibleRepositories') ->assertSet('repositoryError', null) ->assertSet('accessibleRepositories', ['test-org/repo-a', 'test-org/repo-b']); }); test('loadAccessibleRepositories sorts repositories alphabetically', function () { Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 3, 'repositories' => [ ['full_name' => 'test-org/zebra', 'name' => 'zebra'], ['full_name' => 'test-org/alpha', 'name' => 'alpha'], ['full_name' => 'test-org/middle', 'name' => 'middle'], ], ], 200), ]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->set('selectedGithubAppId', $this->githubApp->id) ->call('loadAccessibleRepositories') ->assertSet('accessibleRepositories', ['test-org/alpha', 'test-org/middle', 'test-org/zebra']); }); test('loadAccessibleRepositories returns empty list when no repos accessible', function () { Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 0, 'repositories' => [], ], 200), ]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->set('selectedGithubAppId', $this->githubApp->id) ->call('loadAccessibleRepositories') ->assertSet('repositoryError', null) ->assertSet('accessibleRepositories', []); }); test('loadAccessibleRepositories does nothing when no app is selected', function () { Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->call('loadAccessibleRepositories') ->assertSet('accessibleRepositories', []) ->assertSet('repositoryError', null); }); test('loadAccessibleRepositories does nothing when app has no installation_id', function () { $this->githubApp->update(['installation_id' => null]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->set('selectedGithubAppId', $this->githubApp->id) ->call('loadAccessibleRepositories') ->assertSet('accessibleRepositories', []) ->assertSet('repositoryError', null); }); test('changing selectedGithubAppId triggers repository reload', function () { Http::fake([ 'https://api.github.com/zen' => Http::response('Keep it logically awesome.', 200, ['Date' => now()->toRfc7231String()]), 'https://api.github.com/app/installations/222/access_tokens' => Http::response([ 'token' => 'ghs_test_token', 'expires_at' => now()->addHour()->toIso8601String(), ], 200), 'https://api.github.com/installation/repositories*' => Http::response([ 'total_count' => 1, 'repositories' => [ ['full_name' => 'test-org/my-repo', 'name' => 'my-repo'], ], ], 200), ]); Livewire::test(GithubRunners::class, ['server_uuid' => $this->server->uuid]) ->set('selectedGithubAppId', $this->githubApp->id) ->assertSet('accessibleRepositories', ['test-org/my-repo']); }); });