From 40020448773a5200de8f0c8dd840c6cb21a2cf77 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 4 Dec 2025 14:58:18 +0100 Subject: [PATCH] Refactor: Move sentinel update checks to ServerManagerJob and add tests for hourly dispatch --- app/Console/Kernel.php | 13 +- app/Jobs/ServerManagerJob.php | 9 + .../SentinelUpdateCheckIndependenceTest.php | 187 ++++++++++++++++++ .../ServerManagerJobSentinelCheckTest.php | 141 +++++++++++++ 4 files changed, 338 insertions(+), 12 deletions(-) create mode 100644 tests/Feature/SentinelUpdateCheckIndependenceTest.php create mode 100644 tests/Unit/ServerManagerJobSentinelCheckTest.php diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 832bed5ae..9fb5e8a19 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,7 +2,6 @@ namespace App\Console; -use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; use App\Jobs\CheckHelperImageJob; use App\Jobs\CheckTraefikVersionJob; @@ -100,17 +99,7 @@ private function pullImages(): void } else { $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); } - foreach ($servers as $server) { - try { - if ($server->isSentinelEnabled()) { - $this->scheduleInstance->job(function () use ($server) { - CheckAndStartSentinelJob::dispatch($server); - })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); - } - } catch (\Exception $e) { - Log::error('Error pulling images: '.$e->getMessage()); - } - } + // Sentinel update checks are now handled by ServerManagerJob $this->scheduleInstance->job(new CheckHelperImageJob) ->cron($this->updateCheckFrequency) ->timezone($this->instanceTimezone) diff --git a/app/Jobs/ServerManagerJob.php b/app/Jobs/ServerManagerJob.php index 45ab1dde8..e2929362f 100644 --- a/app/Jobs/ServerManagerJob.php +++ b/app/Jobs/ServerManagerJob.php @@ -146,6 +146,15 @@ private function processServerTasks(Server $server): void ServerPatchCheckJob::dispatch($server); } + // Check for sentinel updates hourly (independent of user-configurable update_check_frequency) + if ($server->isSentinelEnabled()) { + $shouldCheckSentinel = $this->shouldRunNow('0 * * * *', $serverTimezone); + + if ($shouldCheckSentinel) { + CheckAndStartSentinelJob::dispatch($server); + } + } + // Dispatch Sentinel restart if due (daily for Sentinel-enabled servers) $isSentinelEnabled = $server->isSentinelEnabled(); $shouldRestartSentinel = $isSentinelEnabled && $this->shouldRunNow('0 0 * * *', $serverTimezone); diff --git a/tests/Feature/SentinelUpdateCheckIndependenceTest.php b/tests/Feature/SentinelUpdateCheckIndependenceTest.php new file mode 100644 index 000000000..080a3ee7c --- /dev/null +++ b/tests/Feature/SentinelUpdateCheckIndependenceTest.php @@ -0,0 +1,187 @@ +create(); + $this->team = $user->teams()->first(); + + // Create server with sentinel enabled + $this->server = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); + + // Enable sentinel on the server + $this->server->settings->update([ + 'is_sentinel_enabled' => true, + 'server_timezone' => 'UTC', + ]); + + $this->server->refresh(); +}); + +afterEach(function () { + Carbon::setTestNow(); // Reset frozen time +}); + +it('dispatches sentinel check hourly regardless of instance update_check_frequency setting', function () { + // Set instance update_check_frequency to yearly (most infrequent option) + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'update_check_frequency' => '0 0 1 1 *', // Yearly - January 1st at midnight + 'instance_timezone' => 'UTC', + ]); + + // Set time to top of any hour (sentinel should check every hour) + Carbon::setTestNow('2025-06-15 14:00:00'); // Random hour, not January 1st + + // Run ServerManagerJob + $job = new ServerManagerJob; + $job->handle(); + + // Assert that CheckAndStartSentinelJob was dispatched despite yearly update check frequency + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) { + return $job->server->id === $this->server->id; + }); +}); + +it('does not dispatch sentinel check when not at top of hour', function () { + // Set instance update_check_frequency to hourly (most frequent) + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'update_check_frequency' => '0 * * * *', // Hourly + 'instance_timezone' => 'UTC', + ]); + + // Set time to middle of the hour (sentinel check cron won't match) + Carbon::setTestNow('2025-06-15 14:30:00'); // 30 minutes past the hour + + // Run ServerManagerJob + $job = new ServerManagerJob; + $job->handle(); + + // Assert that CheckAndStartSentinelJob was NOT dispatched (not top of hour) + Queue::assertNotPushed(CheckAndStartSentinelJob::class); +}); + +it('dispatches sentinel check at every hour mark throughout the day', function () { + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'update_check_frequency' => '0 0 1 1 *', // Yearly + 'instance_timezone' => 'UTC', + ]); + + // Test multiple hours throughout a day + $hoursToTest = [0, 6, 12, 18, 23]; // Various hours of the day + + foreach ($hoursToTest as $hour) { + Queue::fake(); // Reset queue for each test + + Carbon::setTestNow("2025-06-15 {$hour}:00:00"); + + $job = new ServerManagerJob; + $job->handle(); + + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) { + return $job->server->id === $this->server->id; + }, "Failed to dispatch sentinel check at hour {$hour}"); + } +}); + +it('respects server timezone when checking sentinel updates', function () { + // Update server timezone to America/New_York + $this->server->settings->update([ + 'server_timezone' => 'America/New_York', + ]); + + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'instance_timezone' => 'UTC', + ]); + + // Set time to 17:00 UTC which is 12:00 PM EST (top of hour in server's timezone) + Carbon::setTestNow('2025-01-15 17:00:00'); + + $job = new ServerManagerJob; + $job->handle(); + + // Should dispatch because it's top of hour in server's timezone (America/New_York) + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) { + return $job->server->id === $this->server->id; + }); +}); + +it('does not dispatch sentinel check for servers without sentinel enabled', function () { + // Disable sentinel + $this->server->settings->update([ + 'is_sentinel_enabled' => false, + ]); + + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'update_check_frequency' => '0 * * * *', + 'instance_timezone' => 'UTC', + ]); + + Carbon::setTestNow('2025-06-15 14:00:00'); + + $job = new ServerManagerJob; + $job->handle(); + + // Should NOT dispatch because sentinel is disabled + Queue::assertNotPushed(CheckAndStartSentinelJob::class); +}); + +it('handles multiple servers with different sentinel configurations', function () { + // Create a second server with sentinel disabled + $server2 = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); + $server2->settings->update([ + 'is_sentinel_enabled' => false, + 'server_timezone' => 'UTC', + ]); + + // Create a third server with sentinel enabled + $server3 = Server::factory()->create([ + 'team_id' => $this->team->id, + ]); + $server3->settings->update([ + 'is_sentinel_enabled' => true, + 'server_timezone' => 'UTC', + ]); + + $instanceSettings = InstanceSettings::first(); + $instanceSettings->update([ + 'instance_timezone' => 'UTC', + ]); + + Carbon::setTestNow('2025-06-15 14:00:00'); + + $job = new ServerManagerJob; + $job->handle(); + + // Should dispatch for server1 (sentinel enabled) and server3 (sentinel enabled) + Queue::assertPushed(CheckAndStartSentinelJob::class, 2); + + // Verify it was dispatched for the correct servers + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) { + return $job->server->id === $this->server->id; + }); + + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) use ($server3) { + return $job->server->id === $server3->id; + }); +}); diff --git a/tests/Unit/ServerManagerJobSentinelCheckTest.php b/tests/Unit/ServerManagerJobSentinelCheckTest.php new file mode 100644 index 000000000..0f2613f11 --- /dev/null +++ b/tests/Unit/ServerManagerJobSentinelCheckTest.php @@ -0,0 +1,141 @@ +instance_timezone = 'UTC'; + $this->app->instance(InstanceSettings::class, $settings); + + // Create a mock server with sentinel enabled + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('isSentinelEnabled')->andReturn(true); + $server->id = 1; + $server->name = 'test-server'; + $server->ip = '192.168.1.100'; + $server->sentinel_updated_at = Carbon::now(); + $server->shouldReceive('getAttribute')->with('settings')->andReturn((object) ['server_timezone' => 'UTC']); + $server->shouldReceive('waitBeforeDoingSshCheck')->andReturn(120); + + // Mock the Server query + Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf(); + Server::shouldReceive('get')->andReturn(collect([$server])); + + // Execute the job + $job = new ServerManagerJob; + $job->handle(); + + // Assert CheckAndStartSentinelJob was dispatched for the sentinel-enabled server + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) use ($server) { + return $job->server->id === $server->id; + }); +}); + +it('does not dispatch CheckAndStartSentinelJob for servers without sentinel enabled', function () { + // Mock InstanceSettings + $settings = Mockery::mock(InstanceSettings::class); + $settings->instance_timezone = 'UTC'; + $this->app->instance(InstanceSettings::class, $settings); + + // Create a mock server with sentinel disabled + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('isSentinelEnabled')->andReturn(false); + $server->id = 2; + $server->name = 'test-server-no-sentinel'; + $server->ip = '192.168.1.101'; + $server->sentinel_updated_at = Carbon::now(); + $server->shouldReceive('getAttribute')->with('settings')->andReturn((object) ['server_timezone' => 'UTC']); + $server->shouldReceive('waitBeforeDoingSshCheck')->andReturn(120); + + // Mock the Server query + Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf(); + Server::shouldReceive('get')->andReturn(collect([$server])); + + // Execute the job + $job = new ServerManagerJob; + $job->handle(); + + // Assert CheckAndStartSentinelJob was NOT dispatched + Queue::assertNotPushed(CheckAndStartSentinelJob::class); +}); + +it('respects server timezone when scheduling sentinel checks', function () { + // Mock InstanceSettings + $settings = Mockery::mock(InstanceSettings::class); + $settings->instance_timezone = 'UTC'; + $this->app->instance(InstanceSettings::class, $settings); + + // Set test time to top of hour in America/New_York (which is 17:00 UTC) + Carbon::setTestNow('2025-01-15 17:00:00'); // 12:00 PM EST (top of hour in EST) + + // Create a mock server with sentinel enabled and America/New_York timezone + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('isSentinelEnabled')->andReturn(true); + $server->id = 3; + $server->name = 'test-server-est'; + $server->ip = '192.168.1.102'; + $server->sentinel_updated_at = Carbon::now(); + $server->shouldReceive('getAttribute')->with('settings')->andReturn((object) ['server_timezone' => 'America/New_York']); + $server->shouldReceive('waitBeforeDoingSshCheck')->andReturn(120); + + // Mock the Server query + Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf(); + Server::shouldReceive('get')->andReturn(collect([$server])); + + // Execute the job + $job = new ServerManagerJob; + $job->handle(); + + // Assert CheckAndStartSentinelJob was dispatched (should run at top of hour in server's timezone) + Queue::assertPushed(CheckAndStartSentinelJob::class, function ($job) use ($server) { + return $job->server->id === $server->id; + }); +}); + +it('does not dispatch sentinel check when not at top of hour', function () { + // Mock InstanceSettings + $settings = Mockery::mock(InstanceSettings::class); + $settings->instance_timezone = 'UTC'; + $this->app->instance(InstanceSettings::class, $settings); + + // Set test time to middle of the hour (not top of hour) + Carbon::setTestNow('2025-01-15 12:30:00'); + + // Create a mock server with sentinel enabled + $server = Mockery::mock(Server::class)->makePartial(); + $server->shouldReceive('isSentinelEnabled')->andReturn(true); + $server->id = 4; + $server->name = 'test-server-mid-hour'; + $server->ip = '192.168.1.103'; + $server->sentinel_updated_at = Carbon::now(); + $server->shouldReceive('getAttribute')->with('settings')->andReturn((object) ['server_timezone' => 'UTC']); + $server->shouldReceive('waitBeforeDoingSshCheck')->andReturn(120); + + // Mock the Server query + Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf(); + Server::shouldReceive('get')->andReturn(collect([$server])); + + // Execute the job + $job = new ServerManagerJob; + $job->handle(); + + // Assert CheckAndStartSentinelJob was NOT dispatched (not top of hour) + Queue::assertNotPushed(CheckAndStartSentinelJob::class); +});