mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-13 02:14:36 +00:00
Merge remote-tracking branch 'origin/next' into env-var-descriptions
This commit is contained in:
commit
9b7e2e15b0
18 files changed
+1008
-171
No files matched your search
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('cleans up servers with unreachable_count >= 3 after 7 days', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'unreachable_count' => 50,
|
||||
'unreachable_notification_sent' => true,
|
||||
'updated_at' => now()->subDays(8),
|
||||
]);
|
||||
|
||||
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
|
||||
|
||||
$server->refresh();
|
||||
expect($server->ip)->toBe('1.2.3.4');
|
||||
});
|
||||
|
||||
it('does not clean up servers with unreachable_count less than 3', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'unreachable_count' => 2,
|
||||
'unreachable_notification_sent' => true,
|
||||
'updated_at' => now()->subDays(8),
|
||||
]);
|
||||
|
||||
$originalIp = $server->ip;
|
||||
|
||||
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
|
||||
|
||||
$server->refresh();
|
||||
expect($server->ip)->toBe($originalIp);
|
||||
});
|
||||
|
||||
it('does not clean up servers updated within 7 days', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'unreachable_count' => 10,
|
||||
'unreachable_notification_sent' => true,
|
||||
'updated_at' => now()->subDays(3),
|
||||
]);
|
||||
|
||||
$originalIp = $server->ip;
|
||||
|
||||
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
|
||||
|
||||
$server->refresh();
|
||||
expect($server->ip)->toBe($originalIp);
|
||||
});
|
||||
|
||||
it('does not clean up servers without notification sent', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'unreachable_count' => 10,
|
||||
'unreachable_notification_sent' => false,
|
||||
'updated_at' => now()->subDays(8),
|
||||
]);
|
||||
|
||||
$originalIp = $server->ip;
|
||||
|
||||
$this->artisan('cleanup:unreachable-servers')->assertSuccessful();
|
||||
|
||||
$server->refresh();
|
||||
expect($server->ip)->toBe($originalIp);
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ConnectProxyToNetworksJob;
|
||||
use App\Jobs\PushServerUpdateJob;
|
||||
use App\Jobs\ServerStorageCheckJob;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
Cache::flush();
|
||||
});
|
||||
|
||||
it('dispatches storage check when disk percentage changes', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$data = [
|
||||
'containers' => [],
|
||||
'filesystem_usage_root' => ['used_percentage' => 45],
|
||||
];
|
||||
|
||||
$job = new PushServerUpdateJob($server, $data);
|
||||
$job->handle();
|
||||
|
||||
Queue::assertPushed(ServerStorageCheckJob::class, function ($job) use ($server) {
|
||||
return $job->server->id === $server->id && $job->percentage === 45;
|
||||
});
|
||||
});
|
||||
|
||||
it('does not dispatch storage check when disk percentage is unchanged', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
// Simulate a previous push that cached the percentage
|
||||
Cache::put('storage-check:'.$server->id, 45, 600);
|
||||
|
||||
$data = [
|
||||
'containers' => [],
|
||||
'filesystem_usage_root' => ['used_percentage' => 45],
|
||||
];
|
||||
|
||||
$job = new PushServerUpdateJob($server, $data);
|
||||
$job->handle();
|
||||
|
||||
Queue::assertNotPushed(ServerStorageCheckJob::class);
|
||||
});
|
||||
|
||||
it('dispatches storage check when disk percentage changes from cached value', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
// Simulate a previous push that cached 45%
|
||||
Cache::put('storage-check:'.$server->id, 45, 600);
|
||||
|
||||
$data = [
|
||||
'containers' => [],
|
||||
'filesystem_usage_root' => ['used_percentage' => 50],
|
||||
];
|
||||
|
||||
$job = new PushServerUpdateJob($server, $data);
|
||||
$job->handle();
|
||||
|
||||
Queue::assertPushed(ServerStorageCheckJob::class, function ($job) use ($server) {
|
||||
return $job->server->id === $server->id && $job->percentage === 50;
|
||||
});
|
||||
});
|
||||
|
||||
it('rate-limits ConnectProxyToNetworksJob dispatch to every 10 minutes', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$server->settings->update(['is_reachable' => true, 'is_usable' => true]);
|
||||
|
||||
// First push: should dispatch ConnectProxyToNetworksJob
|
||||
$containersWithProxy = [
|
||||
[
|
||||
'name' => 'coolify-proxy',
|
||||
'state' => 'running',
|
||||
'health_status' => 'healthy',
|
||||
'labels' => ['coolify.managed' => true],
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
'containers' => $containersWithProxy,
|
||||
'filesystem_usage_root' => ['used_percentage' => 10],
|
||||
];
|
||||
|
||||
$job = new PushServerUpdateJob($server, $data);
|
||||
$job->handle();
|
||||
|
||||
Queue::assertPushed(ConnectProxyToNetworksJob::class, 1);
|
||||
|
||||
// Second push: should NOT dispatch ConnectProxyToNetworksJob (rate-limited)
|
||||
Queue::fake();
|
||||
$job2 = new PushServerUpdateJob($server, $data);
|
||||
$job2->handle();
|
||||
|
||||
Queue::assertNotPushed(ConnectProxyToNetworksJob::class);
|
||||
});
|
||||
|
||||
it('dispatches ConnectProxyToNetworksJob again after cache expires', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
$server->settings->update(['is_reachable' => true, 'is_usable' => true]);
|
||||
|
||||
$containersWithProxy = [
|
||||
[
|
||||
'name' => 'coolify-proxy',
|
||||
'state' => 'running',
|
||||
'health_status' => 'healthy',
|
||||
'labels' => ['coolify.managed' => true],
|
||||
],
|
||||
];
|
||||
|
||||
$data = [
|
||||
'containers' => $containersWithProxy,
|
||||
'filesystem_usage_root' => ['used_percentage' => 10],
|
||||
];
|
||||
|
||||
// First push
|
||||
$job = new PushServerUpdateJob($server, $data);
|
||||
$job->handle();
|
||||
|
||||
Queue::assertPushed(ConnectProxyToNetworksJob::class, 1);
|
||||
|
||||
// Clear cache to simulate expiration
|
||||
Cache::forget('connect-proxy:'.$server->id);
|
||||
|
||||
// Next push: should dispatch again
|
||||
Queue::fake();
|
||||
$job2 = new PushServerUpdateJob($server, $data);
|
||||
$job2->handle();
|
||||
|
||||
Queue::assertPushed(ConnectProxyToNetworksJob::class, 1);
|
||||
});
|
||||
|
||||
it('uses default queue for PushServerUpdateJob', function () {
|
||||
$team = Team::factory()->create();
|
||||
$server = Server::factory()->create(['team_id' => $team->id]);
|
||||
|
||||
$job = new PushServerUpdateJob($server, ['containers' => []]);
|
||||
|
||||
expect($job->queue)->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\ScheduledJobManager;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
beforeEach(function () {
|
||||
// Clear any dedup keys
|
||||
Cache::flush();
|
||||
});
|
||||
|
||||
it('dispatches backup when job runs on time at the cron minute', function () {
|
||||
// Freeze time at exactly 02:00 — daily cron "0 2 * * *" is due
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
|
||||
// Use reflection to test shouldRunNow
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('catches delayed job when cache has a baseline from previous run', function () {
|
||||
// Simulate a previous dispatch yesterday at 02:00
|
||||
Cache::put('test-backup:1', Carbon::create(2026, 2, 27, 2, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Freeze time at 02:07 — job was delayed 7 minutes past today's 02:00 cron
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 7, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// isDue() would return false at 02:07, but getPreviousRunDate() = 02:00 today
|
||||
// lastDispatched = 02:00 yesterday → 02:00 today > yesterday → fires
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:1');
|
||||
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not double-dispatch on subsequent runs within same cron window', function () {
|
||||
// First run at 02:00 — dispatches and sets cache
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$first = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:2');
|
||||
expect($first)->toBeTrue();
|
||||
|
||||
// Second run at 02:01 — should NOT dispatch (previousDue=02:00, lastDispatched=02:00)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$second = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:2');
|
||||
expect($second)->toBeFalse();
|
||||
});
|
||||
|
||||
it('fires every_minute cron correctly on consecutive minutes', function () {
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Minute 1
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result1 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result1)->toBeTrue();
|
||||
|
||||
// Minute 2
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result2 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result2)->toBeTrue();
|
||||
|
||||
// Minute 3
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 2, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
$result3 = $method->invoke($job, '* * * * *', 'UTC', 'test-backup:3');
|
||||
expect($result3)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not fire non-due jobs on restart when cache is empty', function () {
|
||||
// Time is 10:00, cron is daily at 02:00 — NOT due right now
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Cache is empty (fresh restart) — should NOT fire daily backup at 10:00
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:4');
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('fires due jobs on restart when cache is empty', function () {
|
||||
// Time is exactly 02:00, cron is daily at 02:00 — IS due right now
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Cache is empty (fresh restart) — but cron IS due → should fire
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:4b');
|
||||
expect($result)->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not dispatch when cron is not due and was not recently due', function () {
|
||||
// Time is 10:00, cron is daily at 02:00 — last due was 8 hours ago
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 10, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// previousDue = 02:00, but lastDispatched was set at 02:00 (simulate)
|
||||
Cache::put('test-backup:5', Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC', 'test-backup:5');
|
||||
expect($result)->toBeFalse();
|
||||
});
|
||||
|
||||
it('falls back to isDue when no dedup key is provided', function () {
|
||||
// Time is exactly 02:00, cron is "0 2 * * *" — should be due
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// No dedup key → simple isDue check
|
||||
$result = $method->invoke($job, '0 2 * * *', 'UTC');
|
||||
expect($result)->toBeTrue();
|
||||
|
||||
// At 02:01 without dedup key → isDue returns false
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 2, 1, 0, 'UTC'));
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$result2 = $method->invoke($job, '0 2 * * *', 'UTC');
|
||||
expect($result2)->toBeFalse();
|
||||
});
|
||||
|
||||
it('respects server timezone for cron evaluation', function () {
|
||||
// UTC time is 22:00 Feb 28, which is 06:00 Mar 1 in Asia/Singapore (+8)
|
||||
Carbon::setTestNow(Carbon::create(2026, 2, 28, 22, 0, 0, 'UTC'));
|
||||
|
||||
$job = new ScheduledJobManager;
|
||||
$reflection = new ReflectionClass($job);
|
||||
|
||||
$executionTimeProp = $reflection->getProperty('executionTime');
|
||||
$executionTimeProp->setAccessible(true);
|
||||
$executionTimeProp->setValue($job, Carbon::now());
|
||||
|
||||
$method = $reflection->getMethod('shouldRunNow');
|
||||
$method->setAccessible(true);
|
||||
|
||||
// Simulate that today's 06:00 UTC run was already dispatched (at 06:00 UTC)
|
||||
Cache::put('test-backup:7', Carbon::create(2026, 2, 28, 6, 0, 0, 'UTC')->toIso8601String(), 86400);
|
||||
|
||||
// Cron "0 6 * * *" in Asia/Singapore: local time is 06:00 Mar 1 → previousDue = 06:00 Mar 1 SGT
|
||||
// That's a NEW cron window (Mar 1) that hasn't been dispatched → should fire
|
||||
$resultSingapore = $method->invoke($job, '0 6 * * *', 'Asia/Singapore', 'test-backup:6');
|
||||
expect($resultSingapore)->toBeTrue();
|
||||
|
||||
// Cron "0 6 * * *" in UTC: previousDue = 06:00 Feb 28 UTC, already dispatched at 06:00 → should NOT fire
|
||||
$resultUtc = $method->invoke($job, '0 6 * * *', 'UTC', 'test-backup:7');
|
||||
expect($resultUtc)->toBeFalse();
|
||||
});
|
||||
@@ -173,6 +173,42 @@ test('scheduler log parser parses skip entries correctly', function () {
|
||||
@unlink($logPath);
|
||||
});
|
||||
|
||||
test('scheduler log parser excludes started events from runs', function () {
|
||||
$logPath = storage_path('logs/scheduled-test-started-filter.log');
|
||||
$logDir = dirname($logPath);
|
||||
if (! is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
// Temporarily rename existing logs so they don't interfere
|
||||
$existingLogs = glob(storage_path('logs/scheduled-*.log'));
|
||||
$renamed = [];
|
||||
foreach ($existingLogs as $log) {
|
||||
$tmp = $log.'.bak';
|
||||
rename($log, $tmp);
|
||||
$renamed[$tmp] = $log;
|
||||
}
|
||||
|
||||
$logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
|
||||
$lines = [
|
||||
'['.now()->format('Y-m-d H:i:s').'] production.INFO: ScheduledJobManager started {}',
|
||||
'['.now()->format('Y-m-d H:i:s').'] production.INFO: ScheduledJobManager completed {"duration_ms":74,"dispatched":1,"skipped":13}',
|
||||
];
|
||||
file_put_contents($logPath, implode("\n", $lines)."\n");
|
||||
|
||||
$parser = new SchedulerLogParser;
|
||||
$runs = $parser->getRecentRuns();
|
||||
|
||||
expect($runs)->toHaveCount(1);
|
||||
expect($runs->first()['message'])->toContain('completed');
|
||||
|
||||
// Cleanup
|
||||
@unlink($logPath);
|
||||
foreach ($renamed as $tmp => $original) {
|
||||
rename($tmp, $original);
|
||||
}
|
||||
});
|
||||
|
||||
test('scheduler log parser filters by team id', function () {
|
||||
$logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
|
||||
$logDir = dirname($logPath);
|
||||
@@ -198,3 +234,39 @@ test('scheduler log parser filters by team id', function () {
|
||||
// Cleanup
|
||||
@unlink($logPath);
|
||||
});
|
||||
|
||||
test('skipped jobs show fallback when resource is deleted', function () {
|
||||
$this->actingAs($this->rootUser);
|
||||
session(['currentTeam' => $this->rootTeam]);
|
||||
|
||||
$logPath = storage_path('logs/scheduled-'.now()->format('Y-m-d').'.log');
|
||||
$logDir = dirname($logPath);
|
||||
if (! is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
// Temporarily rename existing logs so they don't interfere
|
||||
$existingLogs = glob(storage_path('logs/scheduled-*.log'));
|
||||
$renamed = [];
|
||||
foreach ($existingLogs as $log) {
|
||||
$tmp = $log.'.bak';
|
||||
rename($log, $tmp);
|
||||
$renamed[$tmp] = $log;
|
||||
}
|
||||
|
||||
$lines = [
|
||||
'['.now()->format('Y-m-d H:i:s').'] production.INFO: Task skipped {"type":"task","skip_reason":"application_not_running","task_id":99999,"task_name":"my-cron-job","team_id":0}',
|
||||
];
|
||||
file_put_contents($logPath, implode("\n", $lines)."\n");
|
||||
|
||||
Livewire::test(ScheduledJobs::class)
|
||||
->assertStatus(200)
|
||||
->assertSee('my-cron-job')
|
||||
->assertSee('Application not running');
|
||||
|
||||
// Cleanup
|
||||
@unlink($logPath);
|
||||
foreach ($renamed as $tmp => $original) {
|
||||
rename($tmp, $original);
|
||||
}
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
use App\Jobs\CheckAndStartSentinelJob;
|
||||
use App\Jobs\ServerConnectionCheckJob;
|
||||
use App\Jobs\ServerManagerJob;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
@@ -10,23 +11,22 @@ use Mockery;
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
Carbon::setTestNow('2025-01-15 12:00:00'); // Set to top of the hour for cron matching
|
||||
Carbon::setTestNow('2025-01-15 12:00:00');
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
Mockery::close();
|
||||
Carbon::setTestNow(); // Reset frozen time
|
||||
Carbon::setTestNow();
|
||||
});
|
||||
|
||||
it('dispatches CheckAndStartSentinelJob hourly for sentinel-enabled servers', function () {
|
||||
// Mock InstanceSettings
|
||||
it('does not dispatch CheckAndStartSentinelJob hourly anymore', function () {
|
||||
$settings = Mockery::mock(InstanceSettings::class);
|
||||
$settings->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->shouldReceive('isSentinelLive')->andReturn(true);
|
||||
$server->id = 1;
|
||||
$server->name = 'test-server';
|
||||
$server->ip = '192.168.1.100';
|
||||
@@ -34,29 +34,76 @@ it('dispatches CheckAndStartSentinelJob hourly for sentinel-enabled servers', fu
|
||||
$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;
|
||||
});
|
||||
// Hourly CheckAndStartSentinelJob dispatch was removed — ServerCheckJob handles it when Sentinel is out of sync
|
||||
Queue::assertNotPushed(CheckAndStartSentinelJob::class);
|
||||
});
|
||||
|
||||
it('does not dispatch CheckAndStartSentinelJob for servers without sentinel enabled', function () {
|
||||
// Mock InstanceSettings
|
||||
it('skips ServerConnectionCheckJob when sentinel is live', function () {
|
||||
$settings = Mockery::mock(InstanceSettings::class);
|
||||
$settings->instance_timezone = 'UTC';
|
||||
$this->app->instance(InstanceSettings::class, $settings);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->shouldReceive('isSentinelEnabled')->andReturn(true);
|
||||
$server->shouldReceive('isSentinelLive')->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);
|
||||
|
||||
Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf();
|
||||
Server::shouldReceive('get')->andReturn(collect([$server]));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$job->handle();
|
||||
|
||||
// Sentinel is healthy so SSH connection check is skipped
|
||||
Queue::assertNotPushed(ServerConnectionCheckJob::class);
|
||||
});
|
||||
|
||||
it('dispatches ServerConnectionCheckJob when sentinel is not live', function () {
|
||||
$settings = Mockery::mock(InstanceSettings::class);
|
||||
$settings->instance_timezone = 'UTC';
|
||||
$this->app->instance(InstanceSettings::class, $settings);
|
||||
|
||||
$server = Mockery::mock(Server::class)->makePartial();
|
||||
$server->shouldReceive('isSentinelEnabled')->andReturn(true);
|
||||
$server->shouldReceive('isSentinelLive')->andReturn(false);
|
||||
$server->id = 1;
|
||||
$server->name = 'test-server';
|
||||
$server->ip = '192.168.1.100';
|
||||
$server->sentinel_updated_at = Carbon::now()->subMinutes(10);
|
||||
$server->shouldReceive('getAttribute')->with('settings')->andReturn((object) ['server_timezone' => 'UTC']);
|
||||
$server->shouldReceive('waitBeforeDoingSshCheck')->andReturn(120);
|
||||
|
||||
Server::shouldReceive('where')->with('ip', '!=', '1.2.3.4')->andReturnSelf();
|
||||
Server::shouldReceive('get')->andReturn(collect([$server]));
|
||||
|
||||
$job = new ServerManagerJob;
|
||||
$job->handle();
|
||||
|
||||
// Sentinel is out of sync so SSH connection check is needed
|
||||
Queue::assertPushed(ServerConnectionCheckJob::class, function ($job) use ($server) {
|
||||
return $job->server->id === $server->id;
|
||||
});
|
||||
});
|
||||
|
||||
it('dispatches ServerConnectionCheckJob when sentinel is not enabled', function () {
|
||||
$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->shouldReceive('isSentinelLive')->never();
|
||||
$server->id = 2;
|
||||
$server->name = 'test-server-no-sentinel';
|
||||
$server->ip = '192.168.1.101';
|
||||
@@ -64,78 +111,14 @@ it('does not dispatch CheckAndStartSentinelJob for servers without sentinel enab
|
||||
$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) {
|
||||
// Sentinel is not enabled so SSH connection check must run
|
||||
Queue::assertPushed(ServerConnectionCheckJob::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);
|
||||
});
|
||||
Reference in new issue
Block a user