diff --git a/.ai/lessons.md b/.ai/lessons.md index d9365ef354..1ea84dab0f 100644 --- a/.ai/lessons.md +++ b/.ai/lessons.md @@ -26,6 +26,7 @@ - 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. - Do not call `sleep()` in a browser test. The app server runs in the test process, so `sleep()` stops it; use `$page->wait()`. +- `Queue::fake()` also captures `dispatchSync()` of a `ShouldQueue` job, so the job never runs. Fake only the jobs to block (`Queue::fake([SomeJob::class])`). - In tests, turning on `is_metrics_enabled` (and other Sentinel settings) with `save()`/`update()` runs the settings hook, which starts Sentinel over a real SSH connection on the sync queue. Use `saveQuietly()` or `Queue::fake()`. - 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`. - SQLite returns `0`/`1` for boolean columns without a `boolean` cast, while Postgres returns real booleans. A strict `=== false` check on such a column never matches in tests, so tests can pass on behavior that Postgres blocks. Add the cast or compare loosely. diff --git a/app/Jobs/CheckForUpdatesJob.php b/app/Jobs/CheckForUpdatesJob.php index 2c16b4475e..2414238b86 100644 --- a/app/Jobs/CheckForUpdatesJob.php +++ b/app/Jobs/CheckForUpdatesJob.php @@ -8,6 +8,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue { @@ -21,15 +22,18 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue $this->cdnTries = $cdnTries; } - public function handle(): void + /** + * @return bool Whether the check finished and new_version_available is up to date + */ + public function handle(): bool { try { if (isDev() || isCloud()) { - return; + return false; } $versions = refresh_versions_file($this->cdnTries); if ($versions === null) { - return; + return false; } CheckTraefikVersionJob::dispatch(); @@ -37,8 +41,12 @@ class CheckForUpdatesJob implements ShouldBeEncrypted, ShouldQueue $latestVersion = data_get($versions, 'coolify.v4.version'); $newVersionAvailable = version_compare($latestVersion, config('constants.coolify.version'), '>'); instanceSettings()->update(['new_version_available' => $newVersionAvailable]); + + return true; } catch (\Throwable $e) { - // Consider implementing a notification to administrators + Log::warning('Could not check for Coolify updates', ['error' => $e->getMessage()]); + + return false; } } } diff --git a/app/Livewire/Settings/Updates.php b/app/Livewire/Settings/Updates.php index 27fe705423..8fd02e046e 100644 --- a/app/Livewire/Settings/Updates.php +++ b/app/Livewire/Settings/Updates.php @@ -159,10 +159,14 @@ class Updates extends Component public function checkManually() { $this->authorize('update', $this->settings); - CheckForUpdatesJob::dispatchSync(cdnTries: 1); + $checked = (new CheckForUpdatesJob(cdnTries: 1))->handle(); $this->dispatch('updateAvailable'); - $settings = instanceSettings(); - if ($settings->new_version_available) { + if (! $checked) { + $this->dispatch('error', 'Could not check for updates. Try again later.'); + + return; + } + if (instanceSettings()->new_version_available) { $this->dispatch('success', 'New version available!'); } else { $this->dispatch('success', 'No new version available.'); diff --git a/tests/Feature/SettingsUpdatesCheckNowTest.php b/tests/Feature/SettingsUpdatesCheckNowTest.php new file mode 100644 index 0000000000..dbdd049fd9 --- /dev/null +++ b/tests/Feature/SettingsUpdatesCheckNowTest.php @@ -0,0 +1,52 @@ +set('constants.coolify.version', '4.4.6'); + + $rootTeam = Team::find(0) ?? Team::factory()->create(['id' => 0]); + Server::factory()->create(['id' => 0, 'team_id' => $rootTeam->id]); + InstanceSettings::forceCreate(['id' => 0]); + Once::flush(); + + $user = User::factory()->create(); + $rootTeam->members()->attach($user->id, ['role' => 'admin']); + $this->actingAs($user); + session(['currentTeam' => ['id' => $rootTeam->id]]); +}); + +it('reports a failed CDN request instead of "No new version available"', function () { + Http::fake(['*' => Http::response('error code: 1200', 503)]); + File::partialMock()->shouldReceive('put')->with(base_path('versions.json'), Mockery::any())->never(); + + Livewire::test(Updates::class) + ->call('checkManually') + ->assertDispatched('error', 'Could not check for updates. Try again later.') + ->assertNotDispatched('success'); +}); + +it('reports a new version when the CDN has a newer version', function () { + Http::fake(['*' => Http::response(['coolify' => ['v4' => ['version' => '4.4.7']]])]); + File::partialMock()->shouldReceive('put')->with(base_path('versions.json'), Mockery::any())->once(); + + Livewire::test(Updates::class) + ->call('checkManually') + ->assertDispatched('success', 'New version available!'); + + expect(InstanceSettings::get()->new_version_available)->toBeTruthy(); +});