refactor: standardize Service model status aggregation to use ContainerStatusAggregator
Fixes inconsistency where Service model used manual state machine logic while all other components (Application, ComplexStatusCheck, GetContainersStatus) use the centralized ContainerStatusAggregator service. Changes: - Refactored Service::aggregateResourceStatuses() to use ContainerStatusAggregator - Removed ~60 lines of duplicated state machine logic - Added comprehensive ServiceExcludedStatusTest with 24 test cases - Fixed bugs in old logic where paused/starting containers were incorrectly marked as unhealthy (should be unknown) Benefits: - Single source of truth for status aggregation across all models - Leverages 42 existing ContainerStatusAggregator tests - Consistent behavior between Service and Application/Database models - Easier maintenance (state machine changes only in one place) All tests pass (37 total): - ServiceExcludedStatusTest: 24/24 passed - AllExcludedContainersConsistencyTest: 13/13 passed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
35a8f54765
commit
70fb4c6869
2 changed files with 410 additions and 210 deletions
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Enums\ProcessStatus;
|
||||
use App\Services\ContainerStatusAggregator;
|
||||
use App\Traits\ClearsGlobalSearchCache;
|
||||
use App\Traits\HasSafeStringAttribute;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
|
|
@ -173,6 +174,21 @@ public function deleteConnectedNetworks()
|
|||
instant_remote_process(["docker network rm {$this->uuid}"], $server, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the service's aggregate status from its applications and databases.
|
||||
*
|
||||
* This method aggregates status from Eloquent model relationships (not Docker containers).
|
||||
* It differs from the CalculatesExcludedStatus trait which works with Docker container objects
|
||||
* during container inspection. This accessor runs on-demand for UI display and works with
|
||||
* already-stored status strings from the database.
|
||||
*
|
||||
* Status format: "{status}:{health}" or "{status}:{health}:excluded"
|
||||
* - Status values: running, exited, degraded, starting, paused, restarting
|
||||
* - Health values: healthy, unhealthy, unknown
|
||||
* - :excluded suffix: Indicates all containers are excluded from health monitoring
|
||||
*
|
||||
* @return string The aggregate status in format "status:health" or "status:health:excluded"
|
||||
*/
|
||||
public function getStatusAttribute()
|
||||
{
|
||||
if ($this->isStarting()) {
|
||||
|
|
@ -182,234 +198,97 @@ public function getStatusAttribute()
|
|||
$applications = $this->applications;
|
||||
$databases = $this->databases;
|
||||
|
||||
$complexStatus = null;
|
||||
$complexHealth = null;
|
||||
$hasNonExcluded = false;
|
||||
|
||||
foreach ($applications as $application) {
|
||||
if ($application->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
$status = str($application->status)->before('(')->trim();
|
||||
$health = str($application->status)->between('(', ')')->trim();
|
||||
|
||||
// Skip containers with :excluded suffix (they are excluded from health checks)
|
||||
if ($health->contains(':excluded')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasNonExcluded = true;
|
||||
if ($complexStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($complexStatus === 'exited') {
|
||||
$complexStatus = 'degraded';
|
||||
} else {
|
||||
$complexStatus = 'running';
|
||||
}
|
||||
} elseif ($status->startsWith('restarting')) {
|
||||
$complexStatus = 'degraded';
|
||||
} elseif ($status->startsWith('exited')) {
|
||||
$complexStatus = 'exited';
|
||||
} elseif ($status->startsWith('created') || $status->startsWith('starting')) {
|
||||
if ($complexStatus === null) {
|
||||
$complexStatus = 'starting';
|
||||
}
|
||||
} elseif ($status->startsWith('paused')) {
|
||||
if ($complexStatus === null) {
|
||||
$complexStatus = 'paused';
|
||||
}
|
||||
} elseif ($status->startsWith('dead') || $status->startsWith('removing')) {
|
||||
$complexStatus = 'degraded';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($complexHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$complexHealth = 'healthy';
|
||||
} elseif ($health->value() === 'unknown') {
|
||||
if ($complexHealth !== 'unhealthy') {
|
||||
$complexHealth = 'unknown';
|
||||
}
|
||||
} else {
|
||||
$complexHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
foreach ($databases as $database) {
|
||||
if ($database->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
$status = str($database->status)->before('(')->trim();
|
||||
$health = str($database->status)->between('(', ')')->trim();
|
||||
|
||||
// Skip containers with :excluded suffix (they are excluded from health checks)
|
||||
if ($health->contains(':excluded')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hasNonExcluded = true;
|
||||
if ($complexStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($complexStatus === 'exited') {
|
||||
$complexStatus = 'degraded';
|
||||
} else {
|
||||
$complexStatus = 'running';
|
||||
}
|
||||
} elseif ($status->startsWith('restarting')) {
|
||||
$complexStatus = 'degraded';
|
||||
} elseif ($status->startsWith('exited')) {
|
||||
$complexStatus = 'exited';
|
||||
} elseif ($status->startsWith('created') || $status->startsWith('starting')) {
|
||||
if ($complexStatus === null) {
|
||||
$complexStatus = 'starting';
|
||||
}
|
||||
} elseif ($status->startsWith('paused')) {
|
||||
if ($complexStatus === null) {
|
||||
$complexStatus = 'paused';
|
||||
}
|
||||
} elseif ($status->startsWith('dead') || $status->startsWith('removing')) {
|
||||
$complexStatus = 'degraded';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($complexHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$complexHealth = 'healthy';
|
||||
} elseif ($health->value() === 'unknown') {
|
||||
if ($complexHealth !== 'unhealthy') {
|
||||
$complexHealth = 'unknown';
|
||||
}
|
||||
} else {
|
||||
$complexHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
[$complexStatus, $complexHealth, $hasNonExcluded] = $this->aggregateResourceStatuses(
|
||||
$applications,
|
||||
$databases,
|
||||
excludedOnly: false
|
||||
);
|
||||
|
||||
// If all services are excluded from status checks, calculate status from excluded containers
|
||||
// but mark it with :excluded to indicate monitoring is disabled
|
||||
if (! $hasNonExcluded && ($complexStatus === null && $complexHealth === null)) {
|
||||
$excludedStatus = null;
|
||||
$excludedHealth = null;
|
||||
|
||||
// Calculate status from excluded containers
|
||||
foreach ($applications as $application) {
|
||||
$status = str($application->status)->before('(')->trim();
|
||||
$health = str($application->status)->between('(', ')')->trim();
|
||||
|
||||
// Only process containers with :excluded suffix (or truly excluded ones)
|
||||
if (! $health->contains(':excluded') && ! $application->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip :excluded suffix for health comparison
|
||||
$health = str($health)->replace(':excluded', '');
|
||||
|
||||
if ($excludedStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($excludedStatus === 'exited') {
|
||||
$excludedStatus = 'degraded';
|
||||
} else {
|
||||
$excludedStatus = 'running';
|
||||
}
|
||||
} elseif ($status->startsWith('restarting')) {
|
||||
$excludedStatus = 'degraded';
|
||||
} elseif ($status->startsWith('exited')) {
|
||||
$excludedStatus = 'exited';
|
||||
} elseif ($status->startsWith('created') || $status->startsWith('starting')) {
|
||||
if ($excludedStatus === null) {
|
||||
$excludedStatus = 'starting';
|
||||
}
|
||||
} elseif ($status->startsWith('paused')) {
|
||||
if ($excludedStatus === null) {
|
||||
$excludedStatus = 'paused';
|
||||
}
|
||||
} elseif ($status->startsWith('dead') || $status->startsWith('removing')) {
|
||||
$excludedStatus = 'degraded';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($excludedHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$excludedHealth = 'healthy';
|
||||
} elseif ($health->value() === 'unknown') {
|
||||
if ($excludedHealth !== 'unhealthy') {
|
||||
$excludedHealth = 'unknown';
|
||||
}
|
||||
} else {
|
||||
$excludedHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$status = str($database->status)->before('(')->trim();
|
||||
$health = str($database->status)->between('(', ')')->trim();
|
||||
|
||||
// Only process containers with :excluded suffix (or truly excluded ones)
|
||||
if (! $health->contains(':excluded') && ! $database->exclude_from_status) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Strip :excluded suffix for health comparison
|
||||
$health = str($health)->replace(':excluded', '');
|
||||
|
||||
if ($excludedStatus === 'degraded') {
|
||||
continue;
|
||||
}
|
||||
if ($status->startsWith('running')) {
|
||||
if ($excludedStatus === 'exited') {
|
||||
$excludedStatus = 'degraded';
|
||||
} else {
|
||||
$excludedStatus = 'running';
|
||||
}
|
||||
} elseif ($status->startsWith('restarting')) {
|
||||
$excludedStatus = 'degraded';
|
||||
} elseif ($status->startsWith('exited')) {
|
||||
$excludedStatus = 'exited';
|
||||
} elseif ($status->startsWith('created') || $status->startsWith('starting')) {
|
||||
if ($excludedStatus === null) {
|
||||
$excludedStatus = 'starting';
|
||||
}
|
||||
} elseif ($status->startsWith('paused')) {
|
||||
if ($excludedStatus === null) {
|
||||
$excludedStatus = 'paused';
|
||||
}
|
||||
} elseif ($status->startsWith('dead') || $status->startsWith('removing')) {
|
||||
$excludedStatus = 'degraded';
|
||||
}
|
||||
if ($health->value() === 'healthy') {
|
||||
if ($excludedHealth === 'unhealthy') {
|
||||
continue;
|
||||
}
|
||||
$excludedHealth = 'healthy';
|
||||
} elseif ($health->value() === 'unknown') {
|
||||
if ($excludedHealth !== 'unhealthy') {
|
||||
$excludedHealth = 'unknown';
|
||||
}
|
||||
} else {
|
||||
$excludedHealth = 'unhealthy';
|
||||
}
|
||||
}
|
||||
[$excludedStatus, $excludedHealth] = $this->aggregateResourceStatuses(
|
||||
$applications,
|
||||
$databases,
|
||||
excludedOnly: true
|
||||
);
|
||||
|
||||
// Return status with :excluded suffix to indicate monitoring is disabled
|
||||
if ($excludedStatus && $excludedHealth) {
|
||||
return "{$excludedStatus}:excluded";
|
||||
return "{$excludedStatus}:{$excludedHealth}:excluded";
|
||||
}
|
||||
|
||||
// If no status was calculated at all (no containers exist), return unknown
|
||||
if ($excludedStatus === null && $excludedHealth === null) {
|
||||
return 'unknown:excluded';
|
||||
return 'unknown:unknown:excluded';
|
||||
}
|
||||
|
||||
return 'exited:excluded';
|
||||
return 'exited:unhealthy:excluded';
|
||||
}
|
||||
|
||||
return "{$complexStatus}:{$complexHealth}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Aggregate status and health from collections of applications and databases.
|
||||
*
|
||||
* This helper method consolidates status aggregation logic using ContainerStatusAggregator.
|
||||
* It processes container status strings stored in the database (not live Docker data).
|
||||
*
|
||||
* @param \Illuminate\Database\Eloquent\Collection $applications Collection of Application models
|
||||
* @param \Illuminate\Database\Eloquent\Collection $databases Collection of Database models
|
||||
* @param bool $excludedOnly If true, only process excluded containers; if false, only process non-excluded
|
||||
* @return array{0: string|null, 1: string|null, 2?: bool} [status, health, hasNonExcluded (only when excludedOnly=false)]
|
||||
*/
|
||||
private function aggregateResourceStatuses($applications, $databases, bool $excludedOnly = false): array
|
||||
{
|
||||
$hasNonExcluded = false;
|
||||
$statusStrings = collect();
|
||||
|
||||
// Process both applications and databases using the same logic
|
||||
$resources = $applications->concat($databases);
|
||||
|
||||
foreach ($resources as $resource) {
|
||||
$isExcluded = $resource->exclude_from_status || str($resource->status)->contains(':excluded');
|
||||
|
||||
// Filter based on excludedOnly flag
|
||||
if ($excludedOnly && ! $isExcluded) {
|
||||
continue;
|
||||
}
|
||||
if (! $excludedOnly && $isExcluded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! $excludedOnly) {
|
||||
$hasNonExcluded = true;
|
||||
}
|
||||
|
||||
// Strip :excluded suffix before aggregation (it's in the 3rd part of "status:health:excluded")
|
||||
$status = str($resource->status)->before(':excluded')->toString();
|
||||
$statusStrings->push($status);
|
||||
}
|
||||
|
||||
// If no status strings collected, return nulls
|
||||
if ($statusStrings->isEmpty()) {
|
||||
return $excludedOnly ? [null, null] : [null, null, $hasNonExcluded];
|
||||
}
|
||||
|
||||
// Use ContainerStatusAggregator service for state machine logic
|
||||
$aggregator = new ContainerStatusAggregator;
|
||||
$aggregatedStatus = $aggregator->aggregateFromStrings($statusStrings);
|
||||
|
||||
// Parse the aggregated "status:health" string
|
||||
$parts = explode(':', $aggregatedStatus);
|
||||
$status = $parts[0] ?? null;
|
||||
$health = $parts[1] ?? null;
|
||||
|
||||
if ($excludedOnly) {
|
||||
return [$status, $health];
|
||||
}
|
||||
|
||||
return [$status, $health, $hasNonExcluded];
|
||||
}
|
||||
|
||||
public function extraFields()
|
||||
{
|
||||
$fields = collect([]);
|
||||
|
|
|
|||
321
tests/Unit/ServiceExcludedStatusTest.php
Normal file
321
tests/Unit/ServiceExcludedStatusTest.php
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Service;
|
||||
|
||||
/**
|
||||
* Test suite for Service model's excluded status calculation.
|
||||
*
|
||||
* These tests verify the Service model's aggregateResourceStatuses() method
|
||||
* and getStatusAttribute() accessor, which aggregate status from applications
|
||||
* and databases. This is separate from the CalculatesExcludedStatus trait
|
||||
* because Service works with Eloquent model relationships (database records)
|
||||
* rather than Docker container objects.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Helper to create a mock resource (application or database) with status.
|
||||
*/
|
||||
function makeResource(string $status, bool $excludeFromStatus = false): object
|
||||
{
|
||||
$resource = new stdClass;
|
||||
$resource->status = $status;
|
||||
$resource->exclude_from_status = $excludeFromStatus;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
describe('Service Excluded Status Calculation', function () {
|
||||
it('returns starting status when service is starting', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(true);
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect());
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('starting:unhealthy');
|
||||
});
|
||||
|
||||
it('aggregates status from non-excluded applications', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$app2 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
|
||||
it('returns excluded status when all containers are excluded', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:healthy:excluded');
|
||||
});
|
||||
|
||||
it('returns unknown status when no containers exist', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect());
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('unknown:unknown:excluded');
|
||||
});
|
||||
|
||||
it('handles mixed excluded and non-excluded containers', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$app2 = makeResource('exited:unhealthy', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
// Should only consider non-excluded containers
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
|
||||
it('detects degraded status with mixed running and exited containers', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$app2 = makeResource('exited:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
it('handles unknown health state', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:unknown', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:unknown');
|
||||
});
|
||||
|
||||
it('prioritizes unhealthy over unknown health', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:unknown', excludeFromStatus: false);
|
||||
$app2 = makeResource('running:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:unhealthy');
|
||||
});
|
||||
|
||||
it('prioritizes unknown over healthy health', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running (healthy)', excludeFromStatus: false);
|
||||
$app2 = makeResource('running (unknown)', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:unknown');
|
||||
});
|
||||
|
||||
it('handles restarting status as degraded', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('restarting:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
it('handles paused status', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('paused:unknown', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('paused:unknown');
|
||||
});
|
||||
|
||||
it('handles dead status as degraded', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('dead:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
it('handles removing status as degraded', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('removing:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('degraded:unhealthy');
|
||||
});
|
||||
|
||||
it('handles created status', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('created:unknown', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('starting:unknown');
|
||||
});
|
||||
|
||||
it('aggregates status from both applications and databases', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$db1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect([$db1]));
|
||||
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
|
||||
it('detects unhealthy when database is unhealthy', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$db1 = makeResource('running:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect([$db1]));
|
||||
|
||||
expect($service->status)->toBe('running:unhealthy');
|
||||
});
|
||||
|
||||
it('skips containers with :excluded suffix in non-excluded aggregation', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
$app2 = makeResource('exited:unhealthy:excluded', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
// Should skip app2 because it has :excluded suffix
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
|
||||
it('strips :excluded suffix when processing excluded containers', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy:excluded', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:healthy:excluded');
|
||||
});
|
||||
|
||||
it('returns exited:unhealthy:excluded when excluded containers have no valid status', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('exited:unhealthy:excluded');
|
||||
});
|
||||
|
||||
it('handles all excluded containers with degraded state', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:healthy', excludeFromStatus: true);
|
||||
$app2 = makeResource('exited:unhealthy', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('degraded:unhealthy:excluded');
|
||||
});
|
||||
|
||||
it('handles all excluded containers with unknown health', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:unknown', excludeFromStatus: true);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:unknown:excluded');
|
||||
});
|
||||
|
||||
it('handles exited containers correctly', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('exited:unhealthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('exited:unhealthy');
|
||||
});
|
||||
|
||||
it('prefers running over starting status', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('starting:unknown', excludeFromStatus: false);
|
||||
$app2 = makeResource('running:healthy', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1, $app2]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
|
||||
it('treats empty health as healthy', function () {
|
||||
$service = Mockery::mock(Service::class)->makePartial();
|
||||
$service->shouldReceive('isStarting')->andReturn(false);
|
||||
|
||||
$app1 = makeResource('running:', excludeFromStatus: false);
|
||||
|
||||
$service->shouldReceive('getAttribute')->with('applications')->andReturn(collect([$app1]));
|
||||
$service->shouldReceive('getAttribute')->with('databases')->andReturn(collect());
|
||||
|
||||
expect($service->status)->toBe('running:healthy');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue