coolify/tests/Unit/StripCoolifyCustomFieldsTest.php

230 lines
7.5 KiB
PHP
Raw Permalink Normal View History

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
<?php
use function PHPUnit\Framework\assertEquals;
test('removes exclude_from_hc from service level', function () {
$yaml = [
'services' => [
'web' => [
'image' => 'nginx:latest',
'exclude_from_hc' => true,
'ports' => ['80:80'],
],
],
];
$result = stripCoolifyCustomFields($yaml);
assertEquals('nginx:latest', $result['services']['web']['image']);
assertEquals(['80:80'], $result['services']['web']['ports']);
expect($result['services']['web'])->not->toHaveKey('exclude_from_hc');
});
test('removes content from volume level', function () {
$yaml = [
'services' => [
'app' => [
'image' => 'php:8.4',
'volumes' => [
[
'type' => 'bind',
'source' => './config.xml',
'target' => '/app/config.xml',
'content' => '<?xml version="1.0"?><config></config>',
],
],
],
],
];
$result = stripCoolifyCustomFields($yaml);
expect($result['services']['app']['volumes'][0])->toHaveKeys(['type', 'source', 'target']);
expect($result['services']['app']['volumes'][0])->not->toHaveKey('content');
});
test('removes isDirectory from volume level', function () {
$yaml = [
'services' => [
'app' => [
'image' => 'node:20',
'volumes' => [
[
'type' => 'bind',
'source' => './data',
'target' => '/app/data',
'isDirectory' => true,
],
],
],
],
];
$result = stripCoolifyCustomFields($yaml);
expect($result['services']['app']['volumes'][0])->toHaveKeys(['type', 'source', 'target']);
expect($result['services']['app']['volumes'][0])->not->toHaveKey('isDirectory');
});
test('removes is_directory from volume level', function () {
$yaml = [
'services' => [
'app' => [
'image' => 'python:3.12',
'volumes' => [
[
'type' => 'bind',
'source' => './logs',
'target' => '/var/log/app',
'is_directory' => true,
],
],
],
],
];
$result = stripCoolifyCustomFields($yaml);
expect($result['services']['app']['volumes'][0])->toHaveKeys(['type', 'source', 'target']);
expect($result['services']['app']['volumes'][0])->not->toHaveKey('is_directory');
});
test('removes all custom fields together', function () {
$yaml = [
'services' => [
'web' => [
'image' => 'nginx:latest',
'exclude_from_hc' => true,
'volumes' => [
[
'type' => 'bind',
'source' => './config.xml',
'target' => '/etc/nginx/config.xml',
'content' => '<config></config>',
'isDirectory' => false,
],
[
'type' => 'bind',
'source' => './data',
'target' => '/var/www/data',
'is_directory' => true,
],
],
],
'worker' => [
'image' => 'worker:latest',
'exclude_from_hc' => true,
],
],
];
$result = stripCoolifyCustomFields($yaml);
// Verify service-level custom fields removed
expect($result['services']['web'])->not->toHaveKey('exclude_from_hc');
expect($result['services']['worker'])->not->toHaveKey('exclude_from_hc');
// Verify volume-level custom fields removed
expect($result['services']['web']['volumes'][0])->not->toHaveKey('content');
expect($result['services']['web']['volumes'][0])->not->toHaveKey('isDirectory');
expect($result['services']['web']['volumes'][1])->not->toHaveKey('is_directory');
// Verify standard fields preserved
assertEquals('nginx:latest', $result['services']['web']['image']);
assertEquals('worker:latest', $result['services']['worker']['image']);
});
test('preserves standard Docker Compose fields', function () {
$yaml = [
'services' => [
'db' => [
'image' => 'postgres:16',
'environment' => [
'POSTGRES_DB' => 'mydb',
'POSTGRES_USER' => 'user',
],
'ports' => ['5432:5432'],
'volumes' => [
'db-data:/var/lib/postgresql/data',
],
'healthcheck' => [
'test' => ['CMD', 'pg_isready'],
'interval' => '5s',
],
'restart' => 'unless-stopped',
'networks' => ['backend'],
],
],
'networks' => [
'backend' => [
'driver' => 'bridge',
],
],
'volumes' => [
'db-data' => null,
],
];
$result = stripCoolifyCustomFields($yaml);
// All standard fields should be preserved
expect($result)->toHaveKeys(['services', 'networks', 'volumes']);
expect($result['services']['db'])->toHaveKeys([
'image', 'environment', 'ports', 'volumes',
'healthcheck', 'restart', 'networks',
]);
assertEquals('postgres:16', $result['services']['db']['image']);
assertEquals(['5432:5432'], $result['services']['db']['ports']);
});
test('handles missing services gracefully', function () {
$yaml = [
'version' => '3.8',
];
$result = stripCoolifyCustomFields($yaml);
expect($result)->toBe($yaml);
});
test('handles missing volumes in service gracefully', function () {
$yaml = [
'services' => [
'app' => [
'image' => 'nginx:latest',
'exclude_from_hc' => true,
],
],
];
$result = stripCoolifyCustomFields($yaml);
expect($result['services']['app'])->not->toHaveKey('exclude_from_hc');
expect($result['services']['app'])->not->toHaveKey('volumes');
assertEquals('nginx:latest', $result['services']['app']['image']);
});
test('handles traccar.yaml example with multiline content', function () {
$yaml = [
'services' => [
'traccar' => [
'image' => 'traccar/traccar:latest',
'volumes' => [
[
'type' => 'bind',
'source' => './srv/traccar/conf/traccar.xml',
'target' => '/opt/traccar/conf/traccar.xml',
'content' => "<?xml version='1.0' encoding='UTF-8'?>\n<!DOCTYPE properties SYSTEM 'http://java.sun.com/dtd/properties.dtd'>\n<properties>\n <entry key='config.default'>./conf/default.xml</entry>\n</properties>",
],
],
],
],
];
$result = stripCoolifyCustomFields($yaml);
expect($result['services']['traccar']['volumes'][0])->toHaveKeys(['type', 'source', 'target']);
expect($result['services']['traccar']['volumes'][0])->not->toHaveKey('content');
assertEquals('./srv/traccar/conf/traccar.xml', $result['services']['traccar']['volumes'][0]['source']);
});