diff --git a/.ai/lessons.md b/.ai/lessons.md index 79eb319551..3ce9a99b08 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -17,6 +17,7 @@ - Never run `php artisan test` inside a dev container (`./scripts/dev exec` or `docker exec coolify-dev-*`). The container sets `DB_CONNECTION=pgsql` as a real environment variable, which wins over `phpunit.xml`, so `RefreshDatabase` runs `migrate:fresh` on the dev database and wipes it. Run tests on the host only. - The helper image runs `docker compose` for deployments and for the Coolify upgrade itself. Before you bump its Docker CLI or Compose, test them against the oldest supported Docker Engine (for example a `docker:-dind` container); newer clients can reject data from older daemons. - Do not symlink `vendor` into another worktree. Composer resolves the base path from the real `vendor` path, so the tests load the app classes of the other checkout. Run `composer install --ignore-platform-req=ext-zip` in the worktree. +- To assert that the unsaved-changes bar shows, check `el.classList.contains('is-dirty')`. Playwright treats the hidden bar (`opacity-0`) as visible, and its class attribute always contains the text `is-dirty` from Tailwind variants. - Call `visit()` directly in each `tests/v4/Browser` test body; Pest does not mark a test that only uses helper-wrapped `visit()` as a browser test, so it fails with `sendText() on null`. ## Verify the complete user flow diff --git a/tests/Feature/Webhook/WebhookHmacTest.php b/tests/Feature/Webhook/WebhookHmacTest.php index 1ff2e2c742..2a24b177cc 100644 --- a/tests/Feature/Webhook/WebhookHmacTest.php +++ b/tests/Feature/Webhook/WebhookHmacTest.php @@ -917,139 +917,33 @@ describe('Manual Webhook Repository Matching', function () { ], ]); - test('github matches ssh git repository URL exactly', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'git@github.com:test-org/test-repo.git', - ]); - $secret = $app->manual_webhook_secret_github; + test('signed pushes deploy applications whose repository URL names the pushed repository', function (string $provider, string $gitRepository) { + Queue::fake(); + $application = makeWebhookApplicationServerFunctional(createApplicationWithWebhook(overrides: [ + 'git_repository' => $gitRepository, + ])); - $payload = json_encode([ - 'ref' => 'refs/heads/main', - 'repository' => ['full_name' => 'test-org/test-repo'], - 'after' => 'abc1234', - 'commits' => [], - ]); + sendManualWebhookPush($this, $provider, $application)->assertOk(); - $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ - 'HTTP_X-GitHub-Event' => 'push', - 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), - 'CONTENT_TYPE' => 'application/json', - ], $payload); + expect(ApplicationDeploymentQueue::query()->where('application_id', $application->id)->exists())->toBeTrue(); + })->with(['github', 'gitlab', 'bitbucket', 'gitea'])->with([ + 'scp-style' => 'git@git.example.com:test-org/test-repo.git', + 'scp-style with custom port' => 'git@10.0.0.80:2222/test-org/test-repo.git', + 'scp-style with custom user and port' => 'custom-user@git.example.com:2222/test-org/test-repo.git', + 'ssh scheme with custom port' => 'ssh://git@10.0.0.80:2222/test-org/test-repo.git', + 'https with different case' => 'https://github.com/Test-Org/Test-Repo.git', + ]); - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); + test('signed pushes do not deploy applications for another repository', function (string $provider) { + Queue::fake(); + $application = makeWebhookApplicationServerFunctional(createApplicationWithWebhook(overrides: [ + 'git_repository' => 'git@10.0.0.80:2222/other-org/test-repo.git', + ])); - test('github matches an ssh repository URL with a non-git username', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'custom-user@git.example.com:test-org/test-repo.git', - ]); - $secret = $app->manual_webhook_secret_github; + sendManualWebhookPush($this, $provider, $application)->assertOk(); - $payload = json_encode([ - 'ref' => 'refs/heads/main', - 'repository' => ['full_name' => 'test-org/test-repo'], - 'after' => 'abc1234', - 'commits' => [], - ]); - - $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ - 'HTTP_X-GitHub-Event' => 'push', - 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), - 'CONTENT_TYPE' => 'application/json', - ], $payload); - - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); - - test('github matches an ssh repository URL with a non-git username and custom port', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'custom-user@git.example.com:2222/test-org/test-repo.git', - ]); - $secret = $app->manual_webhook_secret_github; - - $payload = json_encode([ - 'ref' => 'refs/heads/main', - 'repository' => ['full_name' => 'test-org/test-repo'], - 'after' => 'abc1234', - 'commits' => [], - ]); - - $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ - 'HTTP_X-GitHub-Event' => 'push', - 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), - 'CONTENT_TYPE' => 'application/json', - ], $payload); - - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); - - test('gitlab matches scp-style ssh repository URL with custom port', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'git@gitlab.example.com:2222/services/xyz.git', - 'git_branch' => 'master', - ]); - $secret = $app->manual_webhook_secret_gitlab; - - $response = $this->postJson('/webhooks/source/gitlab/events/manual', [ - 'object_kind' => 'push', - 'ref' => 'refs/heads/master', - 'project' => ['path_with_namespace' => 'services/xyz'], - 'after' => 'abc1234', - 'commits' => [], - ], [ - 'X-Gitlab-Token' => $secret, - ]); - - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); - - test('gitlab matches scp-style ssh repository URL without port', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'git@gitlab.example.com:services/xyz.git', - 'git_branch' => 'master', - ]); - $secret = $app->manual_webhook_secret_gitlab; - - $response = $this->postJson('/webhooks/source/gitlab/events/manual', [ - 'object_kind' => 'push', - 'ref' => 'refs/heads/master', - 'project' => ['path_with_namespace' => 'services/xyz'], - 'after' => 'abc1234', - 'commits' => [], - ], [ - 'X-Gitlab-Token' => $secret, - ]); - - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); - - test('github matches repository case-insensitively', function () { - $app = createApplicationWithWebhook(overrides: [ - 'git_repository' => 'https://github.com/Test-Org/Test-Repo.git', - ]); - $secret = $app->manual_webhook_secret_github; - - $payload = json_encode([ - 'ref' => 'refs/heads/main', - 'repository' => ['full_name' => 'test-org/test-repo'], - 'after' => 'abc1234', - 'commits' => [], - ]); - - $response = $this->call('POST', '/webhooks/source/github/events/manual', [], [], [], [ - 'HTTP_X-GitHub-Event' => 'push', - 'HTTP_X-Hub-Signature-256' => 'sha256='.hash_hmac('sha256', $payload, $secret), - 'CONTENT_TYPE' => 'application/json', - ], $payload); - - $response->assertOk(); - expect($response->getContent())->not->toContain('No applications found'); - }); + expect(ApplicationDeploymentQueue::query()->where('application_id', $application->id)->exists())->toBeFalse(); + })->with(['github', 'gitlab', 'bitbucket', 'gitea']); }); describe('Webhook Secret Auto-Generation', function () { diff --git a/tests/v4/Browser/ApplicationConfigurationTest.php b/tests/v4/Browser/ApplicationConfigurationTest.php index cb60e12949..39b0b37a39 100644 --- a/tests/v4/Browser/ApplicationConfigurationTest.php +++ b/tests/v4/Browser/ApplicationConfigurationTest.php @@ -5,6 +5,7 @@ use App\Models\InstanceSettings; use App\Models\LocalPersistentVolume; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Process; use Visus\Cuid2\Cuid2; uses(RefreshDatabase::class); @@ -135,6 +136,42 @@ it('saves application name description and ports from the general form', functio ->screenshot(filename: 'application-general-reloaded'); }); +it('shows the unsaved bar for a Compose location edit and saves it with the name', function () { + Process::fake(fn ($process) => match (true) { + str_contains($process->command, 'git --version') => Process::result('git version 2.43.0'), + str_contains($process->command, 'head -c') => Process::result("services:\n web:\n image: nginx\n"), + default => Process::result(''), + }); + $this->application->update([ + 'build_pack' => 'dockercompose', + 'docker_compose_location' => '/docker-compose.yaml', + 'docker_compose_raw' => "services:\n web:\n image: nginx\n", + ]); + loginAndSkipBoarding(); + + $page = visit(applicationConfigurationUrl( + $this->stack['project'], + $this->stack['environment'], + $this->application + )); + + // The Compose location input is bound through @entangle, not wire:model. + $page->assertSee('Docker compose location') + ->type('input[placeholder="/docker-compose.yaml"]', '/compose.prod.yaml') + ->assertScript(<<<'JS' + () => [...document.querySelectorAll('.is-dirty')].some((bar) => bar.textContent.includes('Save changes')) + JS) + ->type('name', 'Renamed Compose App') + ->click('button:visible:has-text("Save changes")') + ->wait(2) + ->screenshot(filename: 'application-compose-location-saved'); + + expect($this->application->refresh()->only(['name', 'docker_compose_location']))->toBe([ + 'name' => 'Renamed Compose App', + 'docker_compose_location' => '/compose.prod.yaml', + ]); +}); + it('saves custom docker run options from the UI', function () { loginAndSkipBoarding();