feat(tests): add comprehensive tests for ContainerStatusAggregator and serverStatus accessor

- Introduced tests for ContainerStatusAggregator to validate status aggregation logic across various container states.
- Implemented tests to ensure serverStatus accessor correctly checks server infrastructure health without being affected by container status.
- Updated ExcludeFromHealthCheckTest to verify excluded status handling in various components.
- Removed obsolete PushServerUpdateJobStatusAggregationTest as its functionality is covered elsewhere.
- Updated version number for sentinel to 0.0.17 in versions.json.
This commit is contained in:
Andras Bacsai
2025-11-20 17:31:07 +01:00
parent 70fb4c6869
commit ae6eef3cdb
23 changed files with 1590 additions and 912 deletions

View File

@@ -58,30 +58,25 @@ it('does not mark containers as unhealthy when health status is missing', functi
// We can't easily test the private aggregateContainerStatuses method directly,
// but we can verify that the code doesn't default to 'unhealthy'
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify the fix: health status should not default to 'unhealthy'
expect($complexStatusCheckFile)
expect($aggregatorFile)
->not->toContain("data_get(\$container, 'State.Health.Status', 'unhealthy')")
->toContain("data_get(\$container, 'State.Health.Status')");
// Verify the health check logic for non-excluded containers
expect($complexStatusCheckFile)
->toContain('if ($containerHealth === \'unhealthy\') {');
// Verify the health check logic
expect($aggregatorFile)
->toContain('if ($health === \'unhealthy\') {');
});
it('only marks containers as unhealthy when health status explicitly equals unhealthy', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// For non-excluded containers (line ~108)
expect($complexStatusCheckFile)
->toContain('if ($containerHealth === \'unhealthy\') {')
// Verify the service checks for explicit 'unhealthy' status
expect($aggregatorFile)
->toContain('if ($health === \'unhealthy\') {')
->toContain('$hasUnhealthy = true;');
// For excluded containers (line ~145)
expect($complexStatusCheckFile)
->toContain('if ($containerHealth === \'unhealthy\') {')
->toContain('$excludedHasUnhealthy = true;');
});
it('handles missing health status correctly in GetContainersStatus', function () {
@@ -92,13 +87,14 @@ it('handles missing health status correctly in GetContainersStatus', function ()
->not->toContain("data_get(\$container, 'State.Health.Status', 'unhealthy')")
->toContain("data_get(\$container, 'State.Health.Status')");
// Verify it uses 'unknown' when health status is missing
// Verify it uses 'unknown' when health status is missing (now using colon format)
expect($getContainersStatusFile)
->toContain('$healthSuffix = $containerHealth ?? \'unknown\';');
->toContain('$healthSuffix = $containerHealth ?? \'unknown\';')
->toContain('ContainerStatusAggregator'); // Uses the service
});
it('treats containers with running status and no healthcheck as not unhealthy', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// The logic should be:
// 1. Get health status (may be null)
@@ -106,67 +102,65 @@ it('treats containers with running status and no healthcheck as not unhealthy',
// 3. Don't mark as unhealthy if health status is null/missing
// Verify the condition explicitly checks for unhealthy
expect($complexStatusCheckFile)
->toContain('if ($containerHealth === \'unhealthy\')');
expect($aggregatorFile)
->toContain('if ($health === \'unhealthy\')');
// Verify this check is done for running containers
expect($complexStatusCheckFile)
->toContain('} elseif ($containerStatus === \'running\') {')
expect($aggregatorFile)
->toContain('} elseif ($state === \'running\') {')
->toContain('$hasRunning = true;');
});
it('tracks unknown health state in aggregation', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// State machine logic now in ContainerStatusAggregator service
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify that $hasUnknown tracking variable exists
expect($getContainersStatusFile)
// Verify that $hasUnknown tracking variable exists in the service
expect($aggregatorFile)
->toContain('$hasUnknown = false;');
// Verify that unknown state is detected in status parsing
expect($getContainersStatusFile)
->toContain("if (str(\$status)->contains('unknown')) {")
expect($aggregatorFile)
->toContain("str(\$status)->contains('unknown')")
->toContain('$hasUnknown = true;');
});
it('preserves unknown health state in aggregated status with correct priority', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// State machine logic now in ContainerStatusAggregator service (using colon format)
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify three-way priority in aggregation:
// 1. Unhealthy (highest priority)
// 2. Unknown (medium priority)
// 3. Healthy (only when all explicitly healthy)
expect($getContainersStatusFile)
expect($aggregatorFile)
->toContain('if ($hasUnhealthy) {')
->toContain("return 'running (unhealthy)';")
->toContain("return 'running:unhealthy';")
->toContain('} elseif ($hasUnknown) {')
->toContain("return 'running (unknown)';")
->toContain("return 'running:unknown';")
->toContain('} else {')
->toContain("return 'running (healthy)';");
->toContain("return 'running:healthy';");
});
it('tracks unknown health state in ComplexStatusCheck for multi-server applications', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
it('tracks unknown health state in ContainerStatusAggregator for all applications', function () {
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify that $hasUnknown tracking variable exists
expect($complexStatusCheckFile)
expect($aggregatorFile)
->toContain('$hasUnknown = false;');
// Verify that unknown state is detected when containerHealth is null
expect($complexStatusCheckFile)
->toContain('} elseif ($containerHealth === null) {')
// Verify that unknown state is detected when health is null or 'starting'
expect($aggregatorFile)
->toContain('} elseif (is_null($health) || $health === \'starting\') {')
->toContain('$hasUnknown = true;');
// Verify excluded containers also track unknown
expect($complexStatusCheckFile)
->toContain('$excludedHasUnknown = false;');
});
it('preserves unknown health state in ComplexStatusCheck aggregated status', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
it('preserves unknown health state in ContainerStatusAggregator aggregated status', function () {
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify three-way priority for non-excluded containers
expect($complexStatusCheckFile)
// Verify three-way priority for running containers in the service
expect($aggregatorFile)
->toContain('if ($hasUnhealthy) {')
->toContain("return 'running:unhealthy';")
->toContain('} elseif ($hasUnknown) {')
@@ -174,114 +168,115 @@ it('preserves unknown health state in ComplexStatusCheck aggregated status', fun
->toContain('} else {')
->toContain("return 'running:healthy';");
// Verify three-way priority for excluded containers
// Verify ComplexStatusCheck delegates to the service
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
expect($complexStatusCheckFile)
->toContain('if ($excludedHasUnhealthy) {')
->toContain("return 'running:unhealthy:excluded';")
->toContain('} elseif ($excludedHasUnknown) {')
->toContain("return 'running:unknown:excluded';")
->toContain("return 'running:healthy:excluded';");
->toContain('use App\\Services\\ContainerStatusAggregator;')
->toContain('$aggregator = new ContainerStatusAggregator;')
->toContain('$aggregator->aggregateFromContainers($relevantContainers);');
});
it('preserves unknown health state in Service model aggregation', function () {
$serviceFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
// Verify unknown is handled in non-excluded applications
// Verify unknown is handled correctly
expect($serviceFile)
->toContain("} elseif (\$health->value() === 'unknown') {")
->toContain("if (\$complexHealth !== 'unhealthy') {")
->toContain("\$complexHealth = 'unknown';");
->toContain("if (\$aggregateHealth !== 'unhealthy') {")
->toContain("\$aggregateHealth = 'unknown';");
// The pattern should appear 4 times (non-excluded apps, non-excluded databases,
// excluded apps, excluded databases)
// The pattern should appear at least once (Service model has different aggregation logic than ContainerStatusAggregator)
$unknownCount = substr_count($serviceFile, "} elseif (\$health->value() === 'unknown') {");
expect($unknownCount)->toBe(4);
expect($unknownCount)->toBeGreaterThan(0);
});
it('handles starting state (created/starting) in GetContainersStatus', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// State machine logic now in ContainerStatusAggregator service
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify tracking variable exists
expect($getContainersStatusFile)
expect($aggregatorFile)
->toContain('$hasStarting = false;');
// Verify detection for created/starting states
expect($getContainersStatusFile)
->toContain("} elseif (str(\$status)->contains('created') || str(\$status)->contains('starting')) {")
expect($aggregatorFile)
->toContain("str(\$status)->contains('created') || str(\$status)->contains('starting')")
->toContain('$hasStarting = true;');
// Verify aggregation returns starting status
expect($getContainersStatusFile)
// Verify aggregation returns starting status (colon format)
expect($aggregatorFile)
->toContain('if ($hasStarting) {')
->toContain("return 'starting (unknown)';");
->toContain("return 'starting:unknown';");
});
it('handles paused state in GetContainersStatus', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// State machine logic now in ContainerStatusAggregator service
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify tracking variable exists
expect($getContainersStatusFile)
expect($aggregatorFile)
->toContain('$hasPaused = false;');
// Verify detection for paused state
expect($getContainersStatusFile)
->toContain("} elseif (str(\$status)->contains('paused')) {")
expect($aggregatorFile)
->toContain("str(\$status)->contains('paused')")
->toContain('$hasPaused = true;');
// Verify aggregation returns paused status
expect($getContainersStatusFile)
// Verify aggregation returns paused status (colon format)
expect($aggregatorFile)
->toContain('if ($hasPaused) {')
->toContain("return 'paused (unknown)';");
->toContain("return 'paused:unknown';");
});
it('handles dead/removing states in GetContainersStatus', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// State machine logic now in ContainerStatusAggregator service
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify tracking variable exists
expect($getContainersStatusFile)
expect($aggregatorFile)
->toContain('$hasDead = false;');
// Verify detection for dead/removing states
expect($getContainersStatusFile)
->toContain("} elseif (str(\$status)->contains('dead') || str(\$status)->contains('removing')) {")
expect($aggregatorFile)
->toContain("str(\$status)->contains('dead') || str(\$status)->contains('removing')")
->toContain('$hasDead = true;');
// Verify aggregation returns degraded status
expect($getContainersStatusFile)
// Verify aggregation returns degraded status (colon format)
expect($aggregatorFile)
->toContain('if ($hasDead) {')
->toContain("return 'degraded (unhealthy)';");
->toContain("return 'degraded:unhealthy';");
});
it('handles edge case states in ComplexStatusCheck for non-excluded containers', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
it('handles edge case states in ContainerStatusAggregator for all containers', function () {
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify tracking variables exist
expect($complexStatusCheckFile)
// Verify tracking variables exist in the service
expect($aggregatorFile)
->toContain('$hasStarting = false;')
->toContain('$hasPaused = false;')
->toContain('$hasDead = false;');
// Verify detection for created/starting
expect($complexStatusCheckFile)
->toContain("} elseif (\$containerStatus === 'created' || \$containerStatus === 'starting') {")
expect($aggregatorFile)
->toContain("} elseif (\$state === 'created' || \$state === 'starting') {")
->toContain('$hasStarting = true;');
// Verify detection for paused
expect($complexStatusCheckFile)
->toContain("} elseif (\$containerStatus === 'paused') {")
expect($aggregatorFile)
->toContain("} elseif (\$state === 'paused') {")
->toContain('$hasPaused = true;');
// Verify detection for dead/removing
expect($complexStatusCheckFile)
->toContain("} elseif (\$containerStatus === 'dead' || \$containerStatus === 'removing') {")
expect($aggregatorFile)
->toContain("} elseif (\$state === 'dead' || \$state === 'removing') {")
->toContain('$hasDead = true;');
});
it('handles edge case states in ComplexStatusCheck aggregation', function () {
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
it('handles edge case states in ContainerStatusAggregator aggregation', function () {
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify aggregation logic for edge cases
expect($complexStatusCheckFile)
// Verify aggregation logic for edge cases in the service
expect($aggregatorFile)
->toContain('if ($hasDead) {')
->toContain("return 'degraded:unhealthy';")
->toContain('if ($hasPaused) {')
@@ -290,51 +285,58 @@ it('handles edge case states in ComplexStatusCheck aggregation', function () {
->toContain("return 'starting:unknown';");
});
it('handles edge case states in Service model for all 4 locations', function () {
it('handles edge case states in Service model', function () {
$serviceFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
// Check for created/starting handling pattern
$createdStartingCount = substr_count($serviceFile, "\$status->startsWith('created') || \$status->startsWith('starting')");
expect($createdStartingCount)->toBe(4, 'created/starting handling should appear in all 4 locations');
expect($createdStartingCount)->toBeGreaterThan(0, 'created/starting handling should exist');
// Check for paused handling pattern
$pausedCount = substr_count($serviceFile, "\$status->startsWith('paused')");
expect($pausedCount)->toBe(4, 'paused handling should appear in all 4 locations');
expect($pausedCount)->toBeGreaterThan(0, 'paused handling should exist');
// Check for dead/removing handling pattern
$deadRemovingCount = substr_count($serviceFile, "\$status->startsWith('dead') || \$status->startsWith('removing')");
expect($deadRemovingCount)->toBe(4, 'dead/removing handling should appear in all 4 locations');
expect($deadRemovingCount)->toBeGreaterThan(0, 'dead/removing handling should exist');
});
it('appends :excluded suffix to excluded container statuses in GetContainersStatus', function () {
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
// Verify that we check for exclude_from_hc flag
// Verify that we use the trait for calculating excluded status
expect($getContainersStatusFile)
->toContain('$excludeFromHc = data_get($serviceConfig, \'exclude_from_hc\', false);');
->toContain('CalculatesExcludedStatus');
// Verify that we append :excluded suffix
// Verify that we use the trait to calculate excluded status
expect($getContainersStatusFile)
->toContain('$containerStatus = str_replace(\')\', \':excluded)\', $containerStatus);');
->toContain('use CalculatesExcludedStatus;');
});
it('skips containers with :excluded suffix in Service model non-excluded sections', function () {
$serviceFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
// Verify that we skip :excluded containers in non-excluded sections
// This should appear twice (once for applications, once for databases)
$skipExcludedCount = substr_count($serviceFile, "if (\$health->contains(':excluded')) {");
expect($skipExcludedCount)->toBeGreaterThanOrEqual(2, 'Should skip :excluded containers in non-excluded sections');
// Verify that we have exclude_from_status field handling
expect($serviceFile)
->toContain('exclude_from_status');
});
it('processes containers with :excluded suffix in Service model excluded sections', function () {
$serviceFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
// Verify that we process :excluded containers in excluded sections
$processExcludedCount = substr_count($serviceFile, "if (! \$health->contains(':excluded') && !");
expect($processExcludedCount)->toBeGreaterThanOrEqual(2, 'Should process :excluded containers in excluded sections');
// Verify that we strip :excluded suffix before health comparison
$stripExcludedCount = substr_count($serviceFile, "\$health = str(\$health)->replace(':excluded', '');");
expect($stripExcludedCount)->toBeGreaterThanOrEqual(2, 'Should strip :excluded suffix in excluded sections');
// Verify that we handle excluded status
expect($serviceFile)
->toContain(':excluded')
->toContain('exclude_from_status');
});
it('treats containers with starting health status as unknown in ContainerStatusAggregator', function () {
$aggregatorFile = file_get_contents(__DIR__.'/../../app/Services/ContainerStatusAggregator.php');
// Verify that 'starting' health status is treated the same as null (unknown)
// During Docker health check grace period, the health status is 'starting'
// This should be treated as 'unknown' rather than 'healthy'
expect($aggregatorFile)
->toContain('} elseif (is_null($health) || $health === \'starting\') {')
->toContain('$hasUnknown = true;');
});