Files
coolify/tests/Feature/Webhook/ManualWebhookSecretStrengthTest.php
e3d4411ff2 chore: merge main into release/v4.5 (#12192)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Furox <177975472+Furox-Art@users.noreply.github.com>
Co-authored-by: johandenhollander <johan.den.hollander@finalist.nl>
Co-authored-by: João Cruz <161638790+SrJooJ@users.noreply.github.com>
Co-authored-by: Saim Hashmi <hashmisaim037@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-10-18 23:43:20 +02:00

230 lines
9.1 KiB
PHP

<?php
use App\Livewire\Project\Shared\Webhooks;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Str;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Server::flushIdentityMap();
InstanceSettings::forceCreate(['id' => 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);
});
});