119 lines
5.3 KiB
PHP
119 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
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 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']);
|
|
}
|
|
|
|
/** A freshly-polled server (DB row updated by the poller) is sampled WITHOUT any cache — the
|
|
* history no longer depends on a cross-container cache, so it works whenever the live gauges do. */
|
|
public function test_sample_metrics_persists_a_row_from_the_polled_db_row_without_a_cache(): void
|
|
{
|
|
$s = $this->server();
|
|
$s->forceFill(['cpu' => 40, 'mem' => 55, 'disk' => 12, 'last_seen_at' => now()])->save();
|
|
// deliberately NO Cache::put — the DB row is the source of truth.
|
|
|
|
$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());
|
|
}
|
|
|
|
/** load is not on the server row; it is taken from the live cache when present (best-effort). */
|
|
public function test_sample_metrics_reads_load_from_the_cache_when_present(): void
|
|
{
|
|
$s = $this->server();
|
|
$s->forceFill(['cpu' => 10, 'mem' => 20, 'disk' => 30, 'last_seen_at' => now()])->save();
|
|
Cache::put("metrics:latest:{$s->id}", ['cpu' => 10, 'mem' => 20, 'disk' => 30, 'load' => 1.25], now()->addMinutes(5));
|
|
|
|
$this->artisan('clusev:sample-metrics')->assertSuccessful();
|
|
|
|
$this->assertSame(1.25, (float) MetricSample::first()->load);
|
|
}
|
|
|
|
public function test_sample_metrics_clamps_percentages_and_skips_stale_or_unpolled_servers(): void
|
|
{
|
|
$polled = $this->server();
|
|
$polled->forceFill(['cpu' => 250, 'mem' => 60, 'disk' => 10, 'last_seen_at' => now()])->save();
|
|
|
|
// never polled (last_seen_at null) → no sample
|
|
$this->server();
|
|
// polled long ago (stale) → no sample (an honest gap while offline)
|
|
$this->server()->forceFill(['cpu' => 5, 'mem' => 5, 'disk' => 5, 'last_seen_at' => now()->subMinutes(10)])->save();
|
|
|
|
$this->artisan('clusev:sample-metrics')->assertSuccessful();
|
|
|
|
$this->assertSame(1, MetricSample::count());
|
|
$this->assertSame(100, MetricSample::first()->cpu); // 250 clamped to 100
|
|
$this->assertSame($polled->id, MetricSample::first()->server_id);
|
|
}
|
|
|
|
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_averages_samples_into_non_empty_buckets(): 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);
|
|
|
|
$this->assertSame(300, $series['window']);
|
|
foreach (['from', 'now', 'bucket'] as $k) {
|
|
$this->assertArrayHasKey($k, $series);
|
|
}
|
|
$this->assertNotEmpty($series['points']);
|
|
// both samples land in the same bucket → averaged (20 + 40) / 2 = 30
|
|
$this->assertSame(30, $series['points'][0]['cpu']);
|
|
// only non-empty buckets are returned — no null gaps (the client connects them continuously)
|
|
foreach ($series['points'] as $p) {
|
|
$this->assertIsInt($p['cpu']);
|
|
}
|
|
}
|
|
|
|
public function test_history_route_returns_the_series_json(): void
|
|
{
|
|
$this->actingAs(User::factory()->create(['must_change_password' => false]));
|
|
$s = $this->server();
|
|
MetricSample::create(['server_id' => $s->id, 'cpu' => 33, 'mem' => 44, 'disk' => 11, 'load' => 0.4, 'sampled_at' => now()->subMinutes(5)]);
|
|
|
|
$this->getJson(route('servers.history', ['server' => $s, 'range' => '24h']))
|
|
->assertOk()
|
|
->assertJsonPath('window', 86400)
|
|
->assertJsonStructure(['window', 'from', 'now', 'bucket', 'points' => [['t', 'cpu', 'mem', 'disk', 'load']]]);
|
|
}
|
|
|
|
public function test_history_route_clamps_an_unknown_range(): void
|
|
{
|
|
$this->actingAs(User::factory()->create(['must_change_password' => false]));
|
|
|
|
$this->getJson(route('servers.history', ['server' => $this->server(), 'range' => 'evil']))
|
|
->assertOk()
|
|
->assertJsonPath('window', 86400); // unknown → default 24h
|
|
}
|
|
}
|