clusev/tests/Feature/MetricHistoryTest.php

106 lines
4.1 KiB
PHP

<?php
namespace Tests\Feature;
use App\Livewire\Servers\Show;
use App\Models\MetricSample;
use App\Models\Server;
use App\Models\User;
use App\Services\MetricHistory;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Livewire\Livewire;
use Tests\TestCase;
class MetricHistoryTest extends TestCase
{
use RefreshDatabase;
private function server(): Server
{
return Server::create(['name' => 'box', 'ip' => '10.0.0.9', 'ssh_port' => 22, 'status' => 'online']);
}
public function test_sample_metrics_persists_a_row_from_the_cached_reading(): void
{
$s = $this->server();
Cache::put("metrics:latest:{$s->id}", ['cpu' => 40, 'mem' => 55, 'disk' => 12, 'load' => 0.7], now()->addMinutes(5));
$this->artisan('clusev:sample-metrics')->assertSuccessful();
$this->assertDatabaseHas('metric_samples', ['server_id' => $s->id, 'cpu' => 40, 'mem' => 55, 'disk' => 12]);
$this->assertSame(1, MetricSample::count());
}
public function test_sample_metrics_skips_a_server_with_no_cached_reading(): void
{
$this->server(); // no cached reading → no sample (honest gap)
$this->artisan('clusev:sample-metrics')->assertSuccessful();
$this->assertSame(0, MetricSample::count());
}
public function test_sample_metrics_prunes_beyond_the_retention_window(): void
{
$s = $this->server();
MetricSample::create(['server_id' => $s->id, 'cpu' => 1, 'mem' => 1, 'disk' => 1, 'load' => 0, 'sampled_at' => now()->subDays(40)]);
MetricSample::create(['server_id' => $s->id, 'cpu' => 2, 'mem' => 2, 'disk' => 2, 'load' => 0, 'sampled_at' => now()->subDays(5)]);
$this->artisan('clusev:sample-metrics', ['--retention' => 30])->assertSuccessful();
$this->assertDatabaseMissing('metric_samples', ['cpu' => 1]); // 40d old → pruned
$this->assertDatabaseHas('metric_samples', ['cpu' => 2]); // 5d old → kept
}
public function test_series_buckets_and_averages_samples_with_null_gaps(): void
{
$s = $this->server();
MetricSample::create(['server_id' => $s->id, 'cpu' => 20, 'mem' => 40, 'disk' => 10, 'load' => 0.5, 'sampled_at' => now()->subSeconds(90)]);
MetricSample::create(['server_id' => $s->id, 'cpu' => 40, 'mem' => 60, 'disk' => 10, 'load' => 1.5, 'sampled_at' => now()->subSeconds(80)]);
$series = app(MetricHistory::class)->series($s, 300, 10);
$this->assertSame(300, $series['window']);
$this->assertCount(10, $series['points']);
$cpu = array_column($series['points'], 'cpu');
// the bucket holding both samples averages them (20 + 40) / 2 = 30
$this->assertContains(30, $cpu);
// empty buckets stay null — gaps, not fabricated values
$this->assertContains(null, $cpu);
}
public function test_show_passes_a_history_series_to_the_view(): void
{
$this->actingAs(User::factory()->create());
$s = $this->server();
MetricSample::create(['server_id' => $s->id, 'cpu' => 30, 'mem' => 40, 'disk' => 10, 'load' => 0.3, 'sampled_at' => now()->subMinutes(5)]);
Livewire::test(Show::class, ['server' => $s])
->assertViewHas('history', fn ($h) => is_array($h) && isset($h['points']) && $h['window'] === 86400);
}
public function test_set_range_clamps_to_allowed_keys(): void
{
$this->actingAs(User::factory()->create());
$s = $this->server();
Livewire::test(Show::class, ['server' => $s])
->assertSet('historyRange', '24h')
->call('setRange', '7d')->assertSet('historyRange', '7d')
->call('setRange', 'bogus')->assertSet('historyRange', '7d'); // invalid → unchanged
}
public function test_a_direct_invalid_range_property_is_clamped(): void
{
$this->actingAs(User::factory()->create());
$s = $this->server();
// historyRange is a public prop — a client can set it directly, bypassing setRange().
Livewire::test(Show::class, ['server' => $s])
->set('historyRange', 'evil')
->assertSet('historyRange', '24h');
}
}