CluPilotCloud/tests/Feature/MonitoringStatusSyncTest.php

108 lines
4.0 KiB
PHP

<?php
use App\Models\Instance;
use App\Models\MonitoringTarget;
use App\Provisioning\Jobs\SyncMonitoringStatus;
use App\Services\Monitoring\MonitoringClient;
/**
* Monitoring verdicts have to be measured, not remembered.
*
* The status column was written once — 'up', at provisioning — and nothing ever
* updated it. The console's notices and the public status page both read it, so
* an outage would have been reported as healthy indefinitely, on a page whose
* whole purpose is to be believed.
*/
function target(string $externalId, array $attributes = []): MonitoringTarget
{
$instance = Instance::factory()->create(['status' => 'active']);
return MonitoringTarget::query()->create(array_merge([
'instance_id' => $instance->id,
'external_id' => $externalId,
'url' => 'https://example.test',
'status' => 'unknown',
], $attributes));
}
it('records what monitoring actually reports', function () {
$up = target('mon-up');
$down = target('mon-down');
$this->mock(MonitoringClient::class, function ($mock) {
$mock->shouldReceive('health')->with('mon-up')->andReturnTrue();
$mock->shouldReceive('health')->with('mon-down')->andReturnFalse();
});
(new SyncMonitoringStatus)->handle(app(MonitoringClient::class));
expect($up->refresh()->status)->toBe('up')
->and($up->checked_at)->not->toBeNull()
->and($down->refresh()->status)->toBe('down');
});
it('does not blame the instance when the monitoring service is the one that is down', function () {
// "The monitor did not answer" says nothing about the instance. Writing
// 'down' would raise an alarm about the wrong system; writing 'up' would
// hide a real one.
$target = target('mon-x', ['status' => 'up', 'checked_at' => now()->subMinutes(3)]);
$wasCheckedAt = $target->checked_at;
$this->mock(MonitoringClient::class, function ($mock) {
$mock->shouldReceive('health')->andThrow(new RuntimeException('connection refused'));
});
(new SyncMonitoringStatus)->handle(app(MonitoringClient::class));
expect($target->refresh()->status)->toBe('up')
// Not advanced — so the verdict is allowed to go stale and stop counting.
->and($target->checked_at->timestamp)->toBe($wasCheckedAt->timestamp);
});
it('does not treat a never-checked target as healthy on the public page', function () {
target('mon-fresh');
$this->get('/status')
->assertOk()
->assertSee(__('status.state.unknown'))
->assertDontSee(__('status.overall.operational'));
});
it('stops trusting a verdict nobody has refreshed', function () {
target('mon-old', ['status' => 'up', 'checked_at' => now()->subHours(2)]);
$this->get('/status')
->assertOk()
->assertSee(__('status.state.unknown'));
});
it('records nothing when the monitor cannot answer at all', function () {
// The boolean contract folded "no monitoring configured" into true and
// "monitor unreachable" into false. Recording either as a measurement
// publishes a fleet-wide all-clear nobody took, or a fleet-wide outage that
// is really one broken monitor.
$target = target('mon-quiet', ['status' => 'up', 'checked_at' => now()->subMinutes(4)]);
$wasCheckedAt = $target->checked_at;
$this->mock(MonitoringClient::class, function ($mock) {
$mock->shouldReceive('health')->andReturnNull();
});
(new SyncMonitoringStatus)->handle(app(MonitoringClient::class));
expect($target->refresh()->status)->toBe('up')
->and($target->checked_at->timestamp)->toBe($wasCheckedAt->timestamp);
});
it('keeps the acceptance check passing when monitoring is not set up', function () {
// isHealthy() still folds "cannot tell" into a pass — deliberately, and
// only there: a delivery must not fail because monitoring is unconfigured.
config()->set('services.monitoring.url', '');
$client = new App\Services\Monitoring\HttpMonitoringClient;
expect($client->health('anything'))->toBeNull()
->and($client->isHealthy('anything'))->toBeTrue();
});