2025-11-18 22:24:11 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unit tests to verify that applications and services with all containers
|
|
|
|
|
* excluded from health checks (exclude_from_hc: true) show correct status.
|
|
|
|
|
*
|
|
|
|
|
* These tests verify the fix for the issue where services with all containers
|
2025-11-19 09:54:51 +00:00
|
|
|
* excluded would show incorrect status, causing broken UI state.
|
|
|
|
|
*
|
|
|
|
|
* The fix now returns status with :excluded suffix to show real container state
|
|
|
|
|
* while indicating monitoring is disabled (e.g., "running:excluded").
|
2025-11-18 22:24:11 +00:00
|
|
|
*/
|
2025-11-19 09:54:51 +00:00
|
|
|
it('ensures ComplexStatusCheck returns excluded status when all containers excluded', function () {
|
2025-11-18 22:24:11 +00:00
|
|
|
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
|
|
|
|
|
|
2025-11-20 16:31:07 +00:00
|
|
|
// Check that when all containers are excluded, ComplexStatusCheck uses the trait
|
2025-11-18 22:24:11 +00:00
|
|
|
expect($complexStatusCheckFile)
|
2025-11-19 09:54:51 +00:00
|
|
|
->toContain('// If all containers are excluded, calculate status from excluded containers')
|
|
|
|
|
->toContain('// but mark it with :excluded to indicate monitoring is disabled')
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('if ($relevantContainers->isEmpty()) {')
|
|
|
|
|
->toContain('return $this->calculateExcludedStatus($containers, $excludedContainers);');
|
|
|
|
|
|
|
|
|
|
// Check that the trait uses ContainerStatusAggregator and appends :excluded suffix
|
|
|
|
|
$traitFile = file_get_contents(__DIR__.'/../../app/Traits/CalculatesExcludedStatus.php');
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('ContainerStatusAggregator')
|
|
|
|
|
->toContain('appendExcludedSuffix')
|
|
|
|
|
->toContain('$aggregator->aggregateFromContainers($excludedOnly)')
|
2025-11-19 09:54:51 +00:00
|
|
|
->toContain("return 'degraded:excluded';")
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain("return 'paused:excluded';")
|
2025-11-24 08:09:08 +00:00
|
|
|
->toContain("return 'exited';")
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('return "$status:excluded";'); // For running:healthy:excluded
|
2025-11-18 22:24:11 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-19 09:54:51 +00:00
|
|
|
it('ensures Service model returns excluded status when all services excluded', function () {
|
2025-11-18 22:24:11 +00:00
|
|
|
$serviceModelFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
|
|
|
|
|
|
|
|
|
|
// Check that when all services are excluded from status checks,
|
2025-11-19 09:54:51 +00:00
|
|
|
// the Service model calculates real status and returns it with :excluded suffix
|
|
|
|
|
expect($serviceModelFile)
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('exclude_from_status')
|
|
|
|
|
->toContain(':excluded')
|
|
|
|
|
->toContain('CalculatesExcludedStatus');
|
2025-11-19 09:54:51 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-20 16:31:07 +00:00
|
|
|
it('ensures Service model returns unknown:unknown:excluded when no containers exist', function () {
|
2025-11-19 09:54:51 +00:00
|
|
|
$serviceModelFile = file_get_contents(__DIR__.'/../../app/Models/Service.php');
|
|
|
|
|
|
|
|
|
|
// Check that when a service has no applications or databases at all,
|
2025-11-24 08:09:08 +00:00
|
|
|
// the Service model returns 'unknown:unknown:excluded' instead of 'exited'
|
2025-11-19 09:54:51 +00:00
|
|
|
// This prevents misleading status display when containers don't exist
|
2025-11-18 22:24:11 +00:00
|
|
|
expect($serviceModelFile)
|
2025-11-19 09:54:51 +00:00
|
|
|
->toContain('// If no status was calculated at all (no containers exist), return unknown')
|
|
|
|
|
->toContain('if ($excludedStatus === null && $excludedHealth === null) {')
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain("return 'unknown:unknown:excluded';");
|
2025-11-18 22:24:11 +00:00
|
|
|
});
|
|
|
|
|
|
2025-11-20 16:31:07 +00:00
|
|
|
it('ensures GetContainersStatus calculates excluded status when all containers excluded', function () {
|
2025-11-18 22:24:11 +00:00
|
|
|
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
|
|
|
|
|
|
|
|
|
|
// Check that when all containers are excluded, the aggregateApplicationStatus
|
2025-11-20 16:31:07 +00:00
|
|
|
// method calculates and returns status with :excluded suffix
|
2025-11-18 22:24:11 +00:00
|
|
|
expect($getContainersStatusFile)
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('// If all containers are excluded, calculate status from excluded containers')
|
|
|
|
|
->toContain('if ($relevantStatuses->isEmpty()) {')
|
|
|
|
|
->toContain('return $this->calculateExcludedStatusFromStrings($containerStatuses);');
|
2025-11-18 22:24:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures exclude_from_hc flag is properly checked in ComplexStatusCheck', function () {
|
|
|
|
|
$complexStatusCheckFile = file_get_contents(__DIR__.'/../../app/Actions/Shared/ComplexStatusCheck.php');
|
|
|
|
|
|
2025-11-20 16:31:07 +00:00
|
|
|
// Verify that exclude_from_hc is parsed using trait helper
|
2025-11-18 22:24:11 +00:00
|
|
|
expect($complexStatusCheckFile)
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);');
|
2025-11-18 22:24:11 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures exclude_from_hc flag is properly checked in GetContainersStatus', function () {
|
|
|
|
|
$getContainersStatusFile = file_get_contents(__DIR__.'/../../app/Actions/Docker/GetContainersStatus.php');
|
|
|
|
|
|
2025-11-20 16:31:07 +00:00
|
|
|
// Verify that exclude_from_hc is parsed using trait helper
|
2025-11-18 22:24:11 +00:00
|
|
|
expect($getContainersStatusFile)
|
2025-11-20 16:31:07 +00:00
|
|
|
->toContain('$excludedContainers = $this->getExcludedContainersFromDockerCompose($dockerComposeRaw);');
|
2025-11-18 22:24:11 +00:00
|
|
|
});
|
2025-11-19 09:54:51 +00:00
|
|
|
|
|
|
|
|
it('ensures UI displays excluded status correctly in status component', function () {
|
|
|
|
|
$servicesStatusFile = file_get_contents(__DIR__.'/../../resources/views/components/status/services.blade.php');
|
|
|
|
|
|
2025-11-24 08:09:08 +00:00
|
|
|
// Verify that the status component uses formatContainerStatus helper to display status
|
2025-11-19 09:54:51 +00:00
|
|
|
expect($servicesStatusFile)
|
2025-11-24 08:09:08 +00:00
|
|
|
->toContain('formatContainerStatus($complexStatus)');
|
2025-11-19 09:54:51 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures UI handles excluded status in service heading buttons', function () {
|
|
|
|
|
$headingFile = file_get_contents(__DIR__.'/../../resources/views/livewire/project/service/heading.blade.php');
|
|
|
|
|
|
|
|
|
|
// Verify that the heading properly handles running/degraded/exited status with :excluded suffix
|
|
|
|
|
// The logic should use contains() to match the base status (running, degraded, exited)
|
|
|
|
|
// which will work for both regular statuses and :excluded suffixed ones
|
|
|
|
|
expect($headingFile)
|
|
|
|
|
->toContain('str($service->status)->contains(\'running\')')
|
|
|
|
|
->toContain('str($service->status)->contains(\'degraded\')')
|
|
|
|
|
->toContain('str($service->status)->contains(\'exited\')');
|
|
|
|
|
});
|
feat: add validation for YAML parsing, integer parameters, and Docker Compose custom fields
This commit adds comprehensive validation improvements and DRY principles for handling Coolify's custom Docker Compose extensions.
## Changes
### 1. Created Reusable stripCoolifyCustomFields() Function
- Added shared helper in bootstrap/helpers/docker.php
- Removes all Coolify custom fields (exclude_from_hc, content, isDirectory, is_directory)
- Handles both long syntax (arrays) and short syntax (strings) for volumes
- Well-documented with comprehensive docblock
- Follows DRY principle for consistent field stripping
### 2. Fixed Docker Compose Modal Validation
- Updated validateComposeFile() to use stripCoolifyCustomFields()
- Now removes ALL custom fields before Docker validation (previously only removed content)
- Fixes validation errors when using templates with custom fields (e.g., traccar.yaml)
- Users can now validate compose files with Coolify extensions in UI
### 3. Enhanced YAML Validation in CalculatesExcludedStatus
- Added proper exception handling with ParseException vs generic Exception
- Added structure validation (checks if parsed result and services are arrays)
- Comprehensive logging with context (error message, line number, snippet)
- Maintains safe fallback behavior (returns empty collection on error)
### 4. Added Integer Validation to ContainerStatusAggregator
- Validates maxRestartCount parameter in both aggregateFromStrings() and aggregateFromContainers()
- Corrects negative values to 0 with warning log
- Logs warnings for suspiciously high values (> 1000)
- Prevents logic errors in crash loop detection
### 5. Comprehensive Unit Tests
- tests/Unit/StripCoolifyCustomFieldsTest.php (NEW) - 9 tests, 43 assertions
- tests/Unit/ContainerStatusAggregatorTest.php - Added 6 tests for integer validation
- tests/Unit/ExcludeFromHealthCheckTest.php - Added 4 tests for YAML validation
- All tests passing with proper Log facade mocking
### 6. Documentation
- Added comprehensive Docker Compose extensions documentation to .ai/core/deployment-architecture.md
- Documents all custom fields: exclude_from_hc, content, isDirectory/is_directory
- Includes examples, use cases, implementation details, and test references
- Updated .ai/README.md with navigation links to new documentation
## Benefits
- Better UX: Users can validate compose files with custom fields
- Better Debugging: Comprehensive logging for errors
- Better Code Quality: DRY principle with reusable validation
- Better Reliability: Prevents logic errors from invalid parameters
- Better Maintainability: Easy to add new custom fields in future
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-20 17:34:49 +00:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unit tests for YAML validation in CalculatesExcludedStatus trait
|
|
|
|
|
*/
|
|
|
|
|
it('ensures YAML validation has proper exception handling for parse errors', function () {
|
|
|
|
|
$traitFile = file_get_contents(__DIR__.'/../../app/Traits/CalculatesExcludedStatus.php');
|
|
|
|
|
|
|
|
|
|
// Verify that ParseException is imported and caught separately from generic Exception
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('use Symfony\Component\Yaml\Exception\ParseException')
|
|
|
|
|
->toContain('use Illuminate\Support\Facades\Log')
|
|
|
|
|
->toContain('} catch (ParseException $e) {')
|
|
|
|
|
->toContain('} catch (\Exception $e) {');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures YAML validation logs parse errors with context', function () {
|
|
|
|
|
$traitFile = file_get_contents(__DIR__.'/../../app/Traits/CalculatesExcludedStatus.php');
|
|
|
|
|
|
|
|
|
|
// Verify that parse errors are logged with useful context (error message, line, snippet)
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('Log::warning(\'Failed to parse Docker Compose YAML for health check exclusions\'')
|
|
|
|
|
->toContain('\'error\' => $e->getMessage()')
|
|
|
|
|
->toContain('\'line\' => $e->getParsedLine()')
|
|
|
|
|
->toContain('\'snippet\' => $e->getSnippet()');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures YAML validation logs unexpected errors', function () {
|
|
|
|
|
$traitFile = file_get_contents(__DIR__.'/../../app/Traits/CalculatesExcludedStatus.php');
|
|
|
|
|
|
|
|
|
|
// Verify that unexpected errors are logged with error level
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('Log::error(\'Unexpected error parsing Docker Compose YAML\'')
|
|
|
|
|
->toContain('\'trace\' => $e->getTraceAsString()');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('ensures YAML validation checks structure after parsing', function () {
|
|
|
|
|
$traitFile = file_get_contents(__DIR__.'/../../app/Traits/CalculatesExcludedStatus.php');
|
|
|
|
|
|
|
|
|
|
// Verify that parsed result is validated to be an array
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('if (! is_array($dockerCompose)) {')
|
|
|
|
|
->toContain('Log::warning(\'Docker Compose YAML did not parse to array\'');
|
|
|
|
|
|
|
|
|
|
// Verify that services is validated to be an array
|
|
|
|
|
expect($traitFile)
|
|
|
|
|
->toContain('if (! is_array($services)) {')
|
|
|
|
|
->toContain('Log::warning(\'Docker Compose services is not an array\'');
|
|
|
|
|
});
|