0, 'is_api_enabled' => true]); $this->team = Team::factory()->create(); $this->admin = User::factory()->create(); $this->team->members()->attach($this->admin->id, ['role' => 'admin']); $this->member = User::factory()->create(); $this->team->members()->attach($this->member->id, ['role' => 'member']); $plainTextToken = Str::random(40); $token = $this->admin->tokens()->create([ 'name' => 'manual-webhook-secret-test-'.Str::random(6), 'token' => hash('sha256', $plainTextToken), 'abilities' => ['*'], 'team_id' => $this->team->id, ]); $this->bearerToken = $token->getKey().'|'.$plainTextToken; $this->server = Server::factory()->create(['team_id' => $this->team->id]); $this->destination = StandaloneDocker::query()->where('server_id', $this->server->id)->firstOrFail(); $this->project = Project::factory()->create(['team_id' => $this->team->id]); $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); $this->application = Application::factory()->create([ 'environment_id' => $this->environment->id, 'destination_id' => $this->destination->id, 'destination_type' => $this->destination->getMorphClass(), 'git_repository' => 'https://github.com/coollabsio/coolify-examples', 'git_branch' => 'main', 'build_pack' => 'nixpacks', 'manual_webhook_secret_github' => 'short-legacy', 'manual_webhook_secret_gitlab' => 'gitlab-legacy', 'manual_webhook_secret_bitbucket' => null, 'manual_webhook_secret_gitea' => Str::random(40), ]); }); afterEach(function () { Server::flushIdentityMap(); }); function manualWebhookSecretApiHeaders(string $bearerToken): array { return [ 'Authorization' => 'Bearer '.$bearerToken, 'Content-Type' => 'application/json', ]; } function manualWebhookSecretComponent(): Testable { test()->actingAs(test()->admin); session(['currentTeam' => test()->team]); return Livewire::test(Webhooks::class, ['resource' => test()->application->fresh()]); } describe('Webhooks component', function () { test('rejects a new secret shorter than 16 characters', function () { manualWebhookSecretComponent() ->set('githubManualWebhookSecret', 'too-short-12') ->call('submit') ->assertHasErrors(['githubManualWebhookSecret']) ->assertNotDispatched('success'); expect($this->application->fresh()->manual_webhook_secret_github)->toBe('short-legacy'); }); test('saves when existing short secrets are left unchanged', function () { $newSecret = Str::random(16); manualWebhookSecretComponent() ->set('bitbucketManualWebhookSecret', $newSecret) ->call('submit') ->assertHasNoErrors() ->assertDispatched('success'); $application = $this->application->fresh(); expect($application->manual_webhook_secret_github)->toBe('short-legacy') ->and($application->manual_webhook_secret_gitlab)->toBe('gitlab-legacy') ->and($application->manual_webhook_secret_bitbucket)->toBe($newSecret); }); test('allows clearing a secret', function () { manualWebhookSecretComponent() ->set('githubManualWebhookSecret', '') ->call('submit') ->assertHasNoErrors() ->assertDispatched('success'); expect($this->application->fresh()->manual_webhook_secret_github)->toBeEmpty(); }); test('generate saves a random secret of at least 32 characters for that provider only', function () { $component = manualWebhookSecretComponent() ->set('githubManualWebhookSecret', 'unsaved-github-edit') ->call('generateSecret', 'gitlab') ->assertHasNoErrors() ->assertDispatched('success'); $generated = $component->get('gitlabManualWebhookSecret'); $application = $this->application->fresh(); expect($generated)->toBeString()->not->toBe('gitlab-legacy') ->and(strlen($generated))->toBeGreaterThanOrEqual(32) ->and($application->manual_webhook_secret_gitlab)->toBe($generated) ->and($application->manual_webhook_secret_github)->toBe('short-legacy'); $component->assertSet('githubManualWebhookSecret', 'unsaved-github-edit') ->assertSet('savedSecrets.gitlabManualWebhookSecret', $generated) ->assertSet('savedSecrets.githubManualWebhookSecret', 'short-legacy'); }); test('generate rejects an unknown provider', function () { manualWebhookSecretComponent() ->call('generateSecret', 'deploywebhook') ->assertDispatched('error'); }); test('shows generate buttons only to users who can edit secrets', function () { manualWebhookSecretComponent() ->assertSeeHtml('aria-label="Generate GitHub webhook secret"') ->assertSeeHtml('aria-label="Generate Gitea webhook secret"'); $this->actingAs($this->member); Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()]) ->assertDontSeeHtml('aria-label="Generate GitHub webhook secret"'); }); test('members cannot generate a secret', function () { $this->actingAs($this->member); session(['currentTeam' => $this->team]); Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()]) ->assertSet('savedSecrets', []) ->call('generateSecret', 'github') ->assertDispatched('error'); expect($this->application->fresh()->manual_webhook_secret_github)->toBe('short-legacy'); }); }); describe('Applications API', function () { test('update rejects a new secret shorter than 16 characters', function () { $this->withHeaders(manualWebhookSecretApiHeaders($this->bearerToken)) ->patchJson("/api/v1/applications/{$this->application->uuid}", [ 'manual_webhook_secret_github' => 'too-short-12', ]) ->assertUnprocessable() ->assertInvalid(['manual_webhook_secret_github']); expect($this->application->fresh()->manual_webhook_secret_github)->toBe('short-legacy'); }); test('update accepts an unchanged short secret, a 16 character secret, and null', function () { $newSecret = Str::random(16); $this->withHeaders(manualWebhookSecretApiHeaders($this->bearerToken)) ->patchJson("/api/v1/applications/{$this->application->uuid}", [ 'manual_webhook_secret_github' => 'short-legacy', 'manual_webhook_secret_gitlab' => $newSecret, 'manual_webhook_secret_bitbucket' => null, ]) ->assertOk(); $application = $this->application->fresh(); expect($application->manual_webhook_secret_github)->toBe('short-legacy') ->and($application->manual_webhook_secret_gitlab)->toBe($newSecret) ->and($application->manual_webhook_secret_bitbucket)->toBeNull(); }); test('create rejects a secret shorter than 16 characters', function () { $this->withHeaders(manualWebhookSecretApiHeaders($this->bearerToken)) ->postJson('/api/v1/applications/dockerimage', [ 'project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid, 'server_uuid' => $this->server->uuid, 'docker_registry_image_name' => 'nginx', 'docker_registry_image_tag' => 'latest', 'ports_exposes' => '80', 'instant_deploy' => false, 'manual_webhook_secret_gitea' => 'too-short-12', ]) ->assertUnprocessable() ->assertInvalid(['manual_webhook_secret_gitea']); }); test('create accepts a secret of 16 characters', function () { $secret = Str::random(16); $response = $this->withHeaders(manualWebhookSecretApiHeaders($this->bearerToken)) ->postJson('/api/v1/applications/dockerimage', [ 'project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid, 'server_uuid' => $this->server->uuid, 'docker_registry_image_name' => 'nginx', 'docker_registry_image_tag' => 'latest', 'ports_exposes' => '80', 'instant_deploy' => false, 'manual_webhook_secret_gitea' => $secret, ]) ->assertCreated(); expect(Application::query()->where('uuid', $response->json('uuid'))->first()->manual_webhook_secret_gitea) ->toBe($secret); }); });