Files
api/services/nginx/app/tests/Unit/Coolify/CoolifyManagerTest.php
T

755 lines
35 KiB
PHP

<?php
app_require('classes/coolify_api_client.php');
app_require('classes/coolify_manager.php');
use classes\coolify_api_client;
use classes\coolify_manager;
class CoolifyManagerHetznerTargetSetFake
{
public array $targets;
public function __construct(array $targets)
{
$this->targets = array_values($targets);
}
public function getLoadBalancer(int|string $id): array
{
return [
'id' => $id,
'targets' => array_map(
static fn(string $ip): array => ['type' => 'ip', 'ip' => ['ip' => $ip]],
$this->targets
),
'services' => [],
];
}
public function addIpTarget(int|string $loadBalancerId, string $ip): array
{
if (!in_array($ip, $this->targets, true)) {
$this->targets[] = $ip;
}
return [];
}
public function removeIpTarget(int|string $loadBalancerId, string $ip): array
{
$this->targets = array_values(array_filter($this->targets, static fn(string $target): bool => $target !== $ip));
return [];
}
public function addService(int|string $loadBalancerId, string $protocol, int $listenPort, int $destinationPort, array $options = []): array
{
return [];
}
public function updateService(int|string $loadBalancerId, string $protocol, int $listenPort, int $destinationPort, array $options = []): array
{
return [];
}
}
function coolifyManagerTestLoadBalancerService(string $protocol, int $listenPort, int $destinationPort): array
{
return [
'protocol' => $protocol,
'listen_port' => $listenPort,
'destination_port' => $destinationPort,
'proxyprotocol' => false,
'health_check' => [
'protocol' => 'tcp',
'port' => $listenPort,
'interval' => 15,
'timeout' => 10,
'retries' => 3,
],
];
}
it('normalizes Coolify API base URLs to the v1 API root', function (): void {
expect(coolify_api_client::normalizeBaseUrl('https://coolify.example.com'))->toBe('https://coolify.example.com/api/v1');
expect(coolify_api_client::normalizeBaseUrl('https://coolify.example.com/api/v1'))->toBe('https://coolify.example.com/api/v1');
expect(coolify_api_client::normalizeBaseUrl(' https://coolify.example.com/ '))->toBe('https://coolify.example.com/api/v1');
});
it('parses generated env files for Coolify service env bulk updates', function (): void {
$env = implode("\n", [
'# generated',
'MARIADB_ROOT_PASSWORD=root-secret',
'MARIADB_PASSWORD=app-secret',
'',
'REDIS_PRIMARY_USERNAME=',
]);
expect(coolify_manager::parseEnvFile($env))->toBe([
'MARIADB_ROOT_PASSWORD' => 'root-secret',
'MARIADB_PASSWORD' => 'app-secret',
'REDIS_PRIMARY_USERNAME' => '',
]);
});
it('prefers public Coolify server hosts over Docker-local addresses', function (): void {
expect(coolify_manager::publicServerHostFromCoolifyServer([
'ip' => 'host.docker.internal',
'public_ip' => '94.130.142.41',
'name' => 'node3.truckwash.io',
]))->toBe('94.130.142.41');
expect(coolify_manager::publicServerHostFromCoolifyServer([
'ip' => 'host.docker.internal',
'name' => 'node3.truckwash.io',
]))->toBe('node3.truckwash.io');
expect(coolify_manager::publicServerHostFromCoolifyServer([
'ip' => 'host.docker.internal',
'name' => 'node3.truckwash.io',
], null, false))->toBeNull();
expect(coolify_manager::publicServerHostFromCoolifyServer([
'ip' => '10.0.0.10',
'name' => 'Production Server',
]))->toBe('10.0.0.10');
expect(coolify_manager::publicServerHostFromCoolifyServer([
'ip' => 'host.docker.internal',
'name' => 'Production Server',
]))->toBeNull();
expect(coolify_manager::publicDnsServerNameFromCoolifyServer([
'ip' => 'host.docker.internal',
'name' => 'node3.truckwash.io',
]))->toBe('node3.truckwash.io');
expect(coolify_manager::publicDnsServerNameFromCoolifyServer([
'ip' => 'host.docker.internal',
'name' => 'Production Server',
]))->toBeNull();
});
it('blocks planned downtime operations against active replication primaries', function (): void {
expect(coolify_manager::blocksPrimaryMutation(['role' => 'primary'], 'deploy'))->toBeTrue();
expect(coolify_manager::blocksPrimaryMutation(['role' => 'primary'], 'restart'))->toBeTrue();
expect(coolify_manager::blocksPrimaryMutation(['role' => 'replica'], 'restart'))->toBeFalse();
expect(coolify_manager::blocksPrimaryMutation(['role' => 'primary'], 'failover'))->toBeFalse();
});
it('allows failed Coolify replica targets to be removed after the service disappears', function (): void {
expect(coolify_manager::targetAllowsReplicaRemoval([
'role' => 'replica',
'deployment_status' => 'reconcile_failed',
'last_reconcile_status' => 'reconcile_failed',
]))->toBeTrue();
expect(coolify_manager::targetAllowsReplicaRemoval([
'role' => 'replica',
'deployment_status' => 'deploying',
'last_reconcile_json' => json_encode(['message' => 'Coolify API request failed: HTTP 404']),
]))->toBeTrue();
expect(coolify_manager::targetAllowsReplicaRemoval([
'role' => 'replica',
'deployment_status' => 'provisioned',
'last_reconcile_status' => 'ok',
]))->toBeFalse();
expect(coolify_manager::targetAllowsReplicaRemoval([
'role' => 'primary',
'deployment_status' => 'reconcile_failed',
]))->toBeFalse();
});
it('retries Coolify maintenance while linked replication provisioning is still incomplete', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'replicationHostStillNeedsProvisioning');
$method->setAccessible(true);
expect($method->invoke(null, [
'role' => 'replica',
'status' => 'provisioning',
'last_status_json' => json_encode([
'status' => 'provisioning',
'replication_percent' => 99.9,
'blockers' => ['MinIO replica has not caught up.'],
]),
]))->toBeTrue();
expect($method->invoke(null, [
'role' => 'replica',
'status' => 'ok',
'last_status_json' => json_encode([
'status' => 'ok',
'replication_percent' => 100,
'blockers' => [],
]),
]))->toBeFalse();
});
it('plans Hetzner load balancer target and service drift without mutating state', function (): void {
$manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [
'targets' => [
['type' => 'ip', 'ip' => ['ip' => '94.130.142.41']],
],
'services' => [
coolifyManagerTestLoadBalancerService('http', 80, 80),
],
], [
['hostname' => 'node1.truckwash.io', 'target_ip' => '94.130.142.41', 'enabled' => true],
['hostname' => 'node2.truckwash.io', 'target_ip' => '65.21.214.30', 'enabled' => true],
]);
$actionTypes = array_map(static fn(array $action): string => (string)$action['type'], $plan['actions']);
expect($plan['has_drift'])->toBeTrue()
->and($plan['missing_targets'])->toContain('65.21.214.30')
->and($actionTypes)->toContain('add_target')
->and($actionTypes)->toContain('add_service')
->and($plan['missing_services'][0])->toMatchArray([
'protocol' => 'tcp',
'listen_port' => 443,
'destination_port' => 443,
]);
});
it('plans Hetzner load balancer service health check drift updates', function (): void {
$manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [
'targets' => [
['type' => 'ip', 'ip' => ['ip' => '94.130.142.41']],
],
'services' => [
[
'protocol' => 'http',
'listen_port' => 80,
'destination_port' => 80,
'proxyprotocol' => false,
'health_check' => [
'protocol' => 'http',
'port' => 80,
'interval' => 15,
'timeout' => 10,
'retries' => 3,
'http' => [
'domain' => '',
'path' => '/',
'response' => '',
'status_codes' => ['2??', '3??'],
'tls' => false,
],
],
],
coolifyManagerTestLoadBalancerService('tcp', 443, 443),
],
], [
['hostname' => 'node1.truckwash.io', 'target_ip' => '94.130.142.41', 'enabled' => true],
]);
expect($plan['actions'])->toHaveCount(1)
->and($plan['actions'][0])->toMatchArray([
'type' => 'update_service',
'reason' => 'health_check_drift',
'protocol' => 'http',
'listen_port' => 80,
'destination_port' => 80,
'health_check' => [
'protocol' => 'tcp',
'port' => 80,
'interval' => 15,
'timeout' => 10,
'retries' => 3,
],
]);
});
it('does not plan removal of the last Hetzner load balancer target', function (): void {
$manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [
'targets' => [
['type' => 'ip', 'ip' => ['ip' => '94.130.142.41']],
],
'services' => [
coolifyManagerTestLoadBalancerService('http', 80, 80),
coolifyManagerTestLoadBalancerService('tcp', 443, 443),
],
], [
['hostname' => 'node1.truckwash.io', 'target_ip' => '94.130.142.41', 'enabled' => false],
]);
expect($plan['actions'][0])
->toHaveKey('type', 'skip_remove_target')
->toHaveKey('reason', 'last_reachable_target_guard');
});
it('plans removal only for disabled or deleted Hetzner load balancer targets', function (): void {
$manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'planLoadBalancerReconcile');
$method->setAccessible(true);
$plan = $method->invoke($manager, [
'targets' => [
['type' => 'ip', 'ip' => ['ip' => '94.130.142.41']],
['type' => 'ip', 'ip' => ['ip' => '65.21.214.30']],
],
'services' => [
coolifyManagerTestLoadBalancerService('http', 80, 80),
coolifyManagerTestLoadBalancerService('tcp', 443, 443),
],
], [
['hostname' => 'node1.truckwash.io', 'target_ip' => '94.130.142.41', 'enabled' => true],
['hostname' => 'node2.truckwash.io', 'target_ip' => '65.21.214.30', 'enabled' => true, 'deleted_at' => '2026-05-19 10:00:00'],
]);
expect($plan['actions'])
->toHaveCount(1)
->and($plan['actions'][0])
->toHaveKey('type', 'remove_target')
->toHaveKey('target_ip', '65.21.214.30');
});
it('builds gateway API auto-provision context for connected Coolify servers', function (): void {
$manager = new coolify_manager();
$contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext');
$contextMethod->setAccessible(true);
$ipMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteServerPublicIp');
$ipMethod->setAccessible(true);
$server = [
'uuid' => 'server-node1',
'name' => 'node1.truckwash.io',
'public_ip' => '94.130.142.41',
'settings' => ['is_reachable' => true, 'is_usable' => true],
];
expect($ipMethod->invoke(null, $server))->toBe('94.130.142.41');
$context = $contextMethod->invoke($manager, [
'id' => 42,
'channel_slug' => 'internal',
'deploy_context_json' => json_encode([
'coolify_project_uuid' => 'project-internal',
'coolify_base_directory' => 'services/nginx/app',
'coolify_dockerfile_location' => 'services/php/Dockerfile',
'coolify_ports_exposes' => '9000',
'coolify_start_command' => 'php-fpm',
'coolify_destination_uuid' => 'source-destination',
'coolify_git_commit_sha' => 'source-commit',
'coolify_enable_ssl' => false,
]),
], $server, '94.130.142.41', 'api-v2.truckwash.io', 'https://api-v2.truckwash.io');
expect($context)->toMatchArray([
'coolify_project_uuid' => 'project-internal',
'coolify_auto_create' => true,
'coolify_enable_ssl' => true,
'coolify_deploy_now' => true,
'coolify_build_pack' => 'dockerfile',
'coolify_dockerfile_location' => '/Dockerfile.coolify-api',
'coolify_ports_exposes' => '80',
'coolify_port' => '80',
'coolify_domain' => 'api-v2.truckwash.io',
'coolify_public_url' => 'https://api-v2.truckwash.io',
'coolify_server_uuid' => 'server-node1',
'server_uuid' => 'server-node1',
'coolify_destination_uuid' => '',
'destination_uuid' => '',
'coolify_service_name' => 'release-internal-api-node1-truckwash-io',
'gateway_route_autoprovision' => true,
'gateway_route_source_target_id' => 42,
'gateway_route_target_ip' => '94.130.142.41',
]);
expect($context)->not->toHaveKey('coolify_git_commit_sha');
expect($context)->not->toHaveKey('coolify_base_directory');
expect($context)->not->toHaveKey('coolify_start_command');
});
it('builds gateway frontend auto-provision context with the release Dockerfile', function (): void {
$manager = new coolify_manager();
$contextMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteProvisionDeployContext');
$contextMethod->setAccessible(true);
$server = [
'uuid' => 'server-node3',
'name' => 'node3.truckwash.io',
'public_ip' => '23.88.23.183',
'settings' => ['is_reachable' => true, 'is_usable' => true],
];
$context = $contextMethod->invoke($manager, [
'id' => 43,
'channel_slug' => 'internal',
'app' => 'frontend',
'deploy_context_json' => json_encode([
'coolify_project_uuid' => 'project-internal',
'coolify_build_pack' => 'static',
'coolify_install_command' => 'npm ci',
'coolify_build_command' => 'npm run build',
'coolify_publish_directory' => 'dist',
'coolify_is_static' => true,
'coolify_is_spa' => true,
]),
], $server, '23.88.23.183', 'api-v2.truckwash.io', 'https://api-v2.truckwash.io/internal/frontend');
expect($context)->toMatchArray([
'coolify_project_uuid' => 'project-internal',
'coolify_build_pack' => 'dockerfile',
'coolify_dockerfile_location' => '/Dockerfile.coolify-frontend',
'coolify_ports_exposes' => '80',
'coolify_port' => '80',
'coolify_public_url' => 'https://api-v2.truckwash.io/internal/frontend',
'coolify_server_uuid' => 'server-node3',
'coolify_service_name' => 'release-internal-frontend-node3-truckwash-io',
'gateway_route_autoprovision' => true,
'gateway_route_source_target_id' => 43,
'gateway_route_target_ip' => '23.88.23.183',
]);
expect($context)->not->toHaveKey('coolify_install_command');
expect($context)->not->toHaveKey('coolify_publish_directory');
expect($context)->not->toHaveKey('coolify_is_static');
expect($context)->not->toHaveKey('coolify_is_spa');
});
it('adds explicit Coolify application route labels for gateway API domains', function (): void {
$payloadMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteApplicationPayload');
$payloadMethod->setAccessible(true);
$publicUrlMethod = new ReflectionMethod(coolify_manager::class, 'gatewayRouteTargetPublicUrl');
$publicUrlMethod->setAccessible(true);
$payload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io', 'api-app-uuid', 8080, base64_encode(implode("\n", [
'custom.keep=true',
'traefik.http.routers.https-0-api-app-uuid.entryPoints=old',
'traefik.http.routers.https-0-api-app-uuid.tls.certresolver=dns-cloudflare',
'traefik.http.services.https-0-api-app-uuid.loadbalancer.server.port=9090',
])));
$labels = explode("\n", base64_decode($payload['custom_labels'], true));
expect($payload['domains'])->toBe('https://api-v2.truckwash.io:8080')
->and($payload['is_force_https_enabled'])->toBeTrue()
->and($payload['force_domain_override'])->toBeTrue()
->and($labels)->toContain('custom.keep=true')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/`)')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.entryPoints=https')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.tls.certresolver=letsencrypt')
->and($labels)->toContain('traefik.http.routers.https-0-api-app-uuid.tls.domains[0].main=api-v2.truckwash.io')
->and($labels)->toContain('traefik.http.services.https-0-api-app-uuid.loadbalancer.server.port=8080');
expect($publicUrlMethod->invoke(null, 'api-v2.truckwash.io', [
'channel_slug' => 'stable',
'channel_default_channel' => 1,
'app' => 'api',
]))->toBe('https://api-v2.truckwash.io');
expect($publicUrlMethod->invoke(null, 'api-v2.truckwash.io', [
'channel_slug' => 'internal',
'channel_default_channel' => 0,
'app' => 'api',
]))->toBe('https://api-v2.truckwash.io/internal/api');
expect($publicUrlMethod->invoke(null, 'api-v2.truckwash.io', [
'channel_slug' => 'internal',
'channel_default_channel' => 0,
'app' => 'frontend',
]))->toBe('https://api-v2.truckwash.io/internal/frontend');
$pathPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/api', 'api-app-uuid', 8080, '');
$pathLabels = explode("\n", base64_decode($pathPayload['custom_labels'], true));
expect($pathPayload['domains'])->toBe('https://api-v2.truckwash.io:8080/internal/api')
->and($pathLabels)->toContain('traefik.http.routers.https-0-api-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/internal/api`)')
->and($pathLabels)->toContain('traefik.http.middlewares.https-0-api-app-uuid-stripprefix.stripprefix.prefixes=/internal/api')
->and($pathLabels)->toContain('traefik.http.routers.https-0-api-app-uuid.middlewares=https-0-api-app-uuid-stripprefix,gzip');
$frontendPayload = $payloadMethod->invoke(null, 'https://api-v2.truckwash.io/internal/frontend', 'frontend-app-uuid', 80, '');
$frontendLabels = explode("\n", base64_decode($frontendPayload['custom_labels'], true));
expect($frontendPayload['domains'])->toBe('https://api-v2.truckwash.io:80/internal/frontend')
->and($frontendLabels)->toContain('traefik.http.routers.https-0-frontend-app-uuid.rule=Host(`api-v2.truckwash.io`) && PathPrefix(`/internal/frontend`)')
->and($frontendLabels)->toContain('traefik.http.middlewares.https-0-frontend-app-uuid-stripprefix.stripprefix.prefixes=/internal/frontend')
->and($frontendLabels)->toContain('traefik.http.routers.https-0-frontend-app-uuid.middlewares=https-0-frontend-app-uuid-stripprefix,gzip');
});
it('isolates and restores Hetzner load balancer IP targets for gateway certificate bootstrap', function (): void {
$manager = new coolify_manager();
$method = new ReflectionMethod(coolify_manager::class, 'setLoadBalancerIpTargets');
$method->setAccessible(true);
$client = new CoolifyManagerHetznerTargetSetFake([
'94.130.142.41',
'65.21.214.30',
'23.88.23.183',
]);
$method->invoke($manager, $client, '6366569', ['65.21.214.30']);
$isolated = $client->targets;
sort($isolated);
$method->invoke($manager, $client, '6366569', [
'94.130.142.41',
'65.21.214.30',
'23.88.23.183',
]);
$restored = $client->targets;
sort($restored);
expect($isolated)->toBe(['65.21.214.30'])
->and($restored)->toBe([
'23.88.23.183',
'65.21.214.30',
'94.130.142.41',
]);
});
it('requires gateway ping probes to return the API ping contract', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'gatewayProbePingContract');
$method->setAccessible(true);
expect($method->invoke(null, json_encode([
'success' => true,
'data' => ['message' => 'pong'],
])))->toMatchArray(['ok' => true, 'message' => 'pong']);
expect($method->invoke(null, '<b>Fatal error</b>'))->toMatchArray([
'ok' => false,
'reason' => 'invalid_json',
]);
});
it('normalizes gateway probe paths for release gateway health checks', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'normalizeGatewayProbePath');
$method->setAccessible(true);
expect($method->invoke(null, 'internal/api/ping'))->toBe('/internal/api/ping')
->and($method->invoke(null, '//internal//api//ping//'))->toBe('/internal/api/ping')
->and($method->invoke(null, 'https://api-v2.truckwash.io/internal/api/ping'))->toBe('/internal/api/ping')
->and($method->invoke(null, ''))->toBe('');
});
it('returns structured errors for failed gateway certificate bootstrap and verification', function (): void {
$method = new ReflectionMethod(coolify_manager::class, 'gatewayRouteHealthErrors');
$method->setAccessible(true);
$errors = $method->invoke(null, [
'ok' => false,
'reason' => 'load_balancer_enforce_required',
'results' => [
['target_ip' => '94.130.142.41', 'ok' => false],
['target_ip' => '65.21.214.30', 'ok' => true],
['target_ip' => '23.88.23.183', 'ok' => false],
],
], [
'ok' => false,
'failed_target_ips' => ['94.130.142.41', '23.88.23.183'],
'results' => [],
]);
expect($errors)->toHaveCount(2)
->and($errors[0])->toMatchArray([
'type' => 'certificate_bootstrap_failed',
'reason' => 'load_balancer_enforce_required',
'failed_target_ips' => ['94.130.142.41', '23.88.23.183'],
])
->and($errors[1])->toMatchArray([
'type' => 'gateway_route_verification_failed',
'failed_target_ips' => ['94.130.142.41', '23.88.23.183'],
]);
});
it('defines Coolify schema, route permissions, and replication integration hooks', function (): void {
$schema = file_get_contents(app_path('classes/coolify_schema_bootstrap.php'));
$manager = file_get_contents(app_path('classes/coolify_manager.php'));
$route = file_get_contents(app_path('routes/superuserCoolifyRoute.php'));
$replication = file_get_contents(app_path('classes/replication_manager.php'));
$status = file_get_contents(app_path('classes/superuser_system_status_service.php'));
$cron = file_get_contents(app_path('cron/Cron.php'));
$openapi = file_get_contents(app_path('openapi.yaml'));
$coolifyConfig = file_get_contents(app_path('modules/coolify/coolify_c.php'));
$tokenConfig = file_get_contents(app_path('modules/coolify/config/coolify_hetzner_cloud_api_token_c.php'));
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS coolify_instances');
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS coolify_targets');
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS coolify_operations');
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS coolify_audit_logs');
expect($schema)->toContain('CREATE TABLE IF NOT EXISTS coolify_instance_gateways');
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'lb_automation_enabled'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'lb_automation_mode'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'hetzner_load_balancer_id'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'hetzner_cloud_api_token'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'public_gateway_host', 'api-v2.truckwash.io'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'public_gateway_probe_path'");
expect($schema)->toContain('94.130.142.41');
expect($schema)->toContain('65.21.214.30');
expect($schema)->toContain('23.88.23.183');
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'enabled'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_token'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_service_uuid'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_frontend_repository'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_backend_repository'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_labels'");
expect($schema)->toContain("ensureModuleConfigDefault('Coolify', 'github_runner_count_per_repo'");
expect($route)->toContain('/superuser/coolify');
expect($route)->toContain('/superuser/coolify/load-balancer');
expect($route)->toContain('/superuser/coolify/load-balancer/reconcile');
expect($route)->toContain('/superuser/coolify/load-balancer/routes/deploy');
expect($route)->toContain('/superuser/coolify/load-balancer/api/deploy');
expect($route)->toContain('/superuser/coolify/gateways');
expect($route)->toContain('/superuser/coolify/github-runners');
expect($route)->toContain('/superuser/coolify/github-runners/deploy');
expect($route)->toContain('/superuser/coolify/gateways/{id}/test');
expect($route)->toContain('/superuser/coolify/instances/{id}/test');
expect($route)->toContain('/superuser/coolify/instances/{id}/placement');
expect($route)->toContain('/superuser/coolify/targets/{id}/reconcile');
expect($route)->toContain('/superuser/coolify/targets/{id}/deploy');
expect($route)->toContain('/superuser/coolify/targets/{id}/restart');
expect($route)->toContain('/superuser/coolify/targets/{id}/failover');
expect($route)->toContain("requirePermission('superuser_coolify_view')");
expect($route)->toContain("requirePermission('superuser_coolify_manage')");
expect($route)->toContain("requirePermission('superuser_coolify_reconcile')");
expect($route)->toContain("requirePermission('superuser_coolify_failover')");
expect($manager)->toContain("Coolify-managed targets must be deployed as replicas first");
expect($manager)->toContain('ensureFailoverEnabled($kind)');
expect($manager)->toContain("deployment_provider' => 'coolify'");
expect($manager)->toContain('discoverInstancePlacement');
expect($manager)->toContain('applyCoolifyDeploymentDefaults($input, $instance)');
expect($manager)->toContain('resolveCoolifyServerHost');
expect($manager)->toContain('publicServerHostFromCoolifyServer');
expect($manager)->toContain('applyCoolifyPortDefaults');
expect($manager)->toContain('syncReplicationHostPortsForTarget');
expect($manager)->toContain('usedPublicPortsForCoolifyServer');
expect($manager)->toContain('nextAvailablePublicPorts');
expect($manager)->toContain('syncReplicationHostEndpointForTarget');
expect($manager)->toContain('knownPublicHostForCoolifyServer');
expect($manager)->toContain('publicDnsServerNameFromCoolifyServer');
expect($manager)->toContain('resolvedPublicDnsServerHostFromCoolifyServer');
expect($manager)->toContain('recordCreatedResource');
expect($manager)->toContain("'start_requested'");
expect($manager)->toContain('primaryCredentials');
expect($manager)->toContain('primary_admin_password');
expect($manager)->toContain('replication_transfer_limit');
expect($manager)->toContain('startOrRestartService');
expect($manager)->toContain('already running');
expect($manager)->toContain('restart_requested');
expect($manager)->toContain('isolated_stack');
expect($manager)->toContain('skip_replication_provisioning');
expect($manager)->toContain('targetSkipsReplicationProvisioning');
expect($manager)->toContain('targetComposeRole');
expect($manager)->toContain('production_data_attached');
expect($manager)->toContain('deferredProvisionResult');
expect($manager)->toContain('isTransientProvisionBlock');
expect($manager)->toContain('provision_deferred');
expect($manager)->toContain('shouldRetryProvisioning');
expect($manager)->toContain('shouldRetryProvisioning($target, $host)');
expect($manager)->toContain('hasRunningReplicationProvisionOperation');
expect($manager)->toContain('replicationHostStillNeedsProvisioning');
expect($manager)->toContain('syncDeploymentStateForReplicationHost');
expect($manager)->toContain('syncLabelForReplicationHost');
expect($manager)->toContain('syncTargetsForReplicationHost');
expect($manager)->toContain('targetAllowsReplicaRemoval');
expect($manager)->toContain('markTargetsRemovedForReplicationHost');
expect($manager)->toContain('replicationHostIsReady');
expect($manager)->toContain("'provisioned'");
expect($manager)->not->toContain('is_container_label_escape_enabled');
expect($manager)->not->toContain("\$payload['type'] = 'docker-compose';");
expect($manager)->toContain('encodedDockerCompose');
expect($manager)->toContain('base64_encode');
expect($manager)->toContain('if (!$update)');
expect($manager)->toContain("'project_uuid' => \$target['project_uuid']");
expect($manager)->toContain('/api/v1/services');
expect($manager)->toContain('/envs/bulk');
expect($manager)->toContain('loadBalancerSummary');
expect($manager)->toContain('reconcileLoadBalancer');
expect($manager)->toContain('deployGatewayApplicationRoutes');
expect($manager)->toContain('deployGatewayApiCode');
expect($manager)->toContain('deployGithubRunners');
expect($manager)->toContain('githubRunnerSummary');
expect($manager)->toContain('gateway_api_code_deploy');
expect($manager)->toContain('deploy_gateway_route_after_code');
expect($manager)->toContain('loadBalancerReleaseGatewayTargets');
expect($manager)->toContain('provisionMissingGatewayRouteTargets');
expect($manager)->toContain('provision_gateway_');
expect($manager)->toContain('gateway_route_autoprovision');
expect($manager)->toContain('upsertDeploymentTarget');
expect($manager)->toContain('startDeployment');
expect($manager)->toContain("'commit_mode' => \$sourceCommitSha === '' ? 'latest' : 'specific'");
expect($manager)->toContain("\$deploymentInput['commit_sha'] = \$sourceCommitSha;");
expect($manager)->toContain('verifyGatewayRoutes');
expect($manager)->toContain('bootstrapGatewayCertificates');
expect($manager)->toContain('setLoadBalancerIpTargets');
expect($manager)->toContain('probeGatewayPublicHost');
expect($manager)->toContain('GATEWAY_CERT_BOOTSTRAP_ATTEMPTS');
expect($manager)->toContain('certificate_bootstrap');
expect($manager)->toContain('certificate_bootstrap_failed');
expect($manager)->toContain('gateway_route_verification_failed');
expect($manager)->toContain('recordGatewayProbe');
expect($manager)->toContain("Gateway route and Let's Encrypt certificate verification is still failing");
expect($manager)->toContain('CURLOPT_SSL_VERIFYHOST, 2');
expect($manager)->toContain('CURLOPT_SSL_VERIFYPEER, true');
expect($manager)->toContain('CURLOPT_CERTINFO');
expect($manager)->toContain('CURLINFO_SSL_VERIFYRESULT');
expect($manager)->toContain("Gateway TLS certificate was not issued by Let's Encrypt.");
expect($manager)->toContain('gatewayProbePath');
expect($manager)->toContain('public_gateway_probe_path');
expect($manager)->toContain('loadBalancerReleaseApiTargets');
expect($manager)->toContain('gatewayProbePingContract');
expect($manager)->toContain('Gateway ping response did not match the expected API contract.');
expect($manager)->not->toContain('CURLOPT_SSL_VERIFYHOST, 0');
expect($manager)->not->toContain('CURLOPT_SSL_VERIFYPEER, false');
expect($manager)->toContain('information_schema.tables');
expect($manager)->not->toContain('SHOW TABLES LIKE ?');
expect($manager)->toContain('gatewayRouteApplicationPayload');
expect($manager)->toContain('gateway_application_routes_deployed');
expect($manager)->toContain('target_already_defined');
expect($manager)->toContain('REQUIRED_LOAD_BALANCER_SERVICES');
expect($manager)->toContain('skip_remove_target');
$composer = json_decode((string)file_get_contents(app_path('composer.json')), true);
expect($composer['autoload']['exclude-from-classmap'] ?? [])->toContain('modules/*/vendor/');
$client = file_get_contents(app_path('classes/coolify_api_client.php'));
expect($client)->toContain("request('GET', '/health', null, false)");
expect($client)->toContain('/github-apps');
expect($client)->toContain('/applications/private-github-app');
expect($client)->toContain("request('GET', '/applications/' . rawurlencode(\$uuid))");
expect($client)->toContain("'/deploy?uuid=' . rawurlencode(\$uuid)");
expect($client)->toContain('/applications/\' . rawurlencode($uuid) . \'/restart');
expect($client)->toContain('CURL_HTTP_VERSION_1_1');
expect($client)->toContain('validationErrorSummary');
expect($replication)->toContain('deployment_provider');
expect($replication)->toContain('coolify_manager::deploymentMetadataForReplicationHost');
expect($replication)->toContain('coolify_manager::syncDeploymentStateForReplicationHost');
expect($replication)->toContain('databaseEngineKnown');
expect($status)->toContain("'key' => 'coolify'");
expect($status)->toContain('probeCoolifyModule');
expect($cron)->toContain('CoolifyAvailabilityMonitorCron');
expect($cron)->toContain('CoolifyLoadBalancerReconcileCron');
expect($coolifyConfig)->toContain('[redacted]');
expect($coolifyConfig)->toContain('secret_set');
expect($coolifyConfig)->toContain('github_runner_token');
expect($coolifyConfig)->toContain('github_runner_service_uuid');
expect($tokenConfig)->toContain('replication_secret_box::encrypt');
expect($openapi)->toContain('/superuser/coolify:');
expect($openapi)->toContain('operationId: getSuperuserCoolifyLoadBalancer');
expect($openapi)->toContain('operationId: reconcileSuperuserCoolifyLoadBalancer');
expect($openapi)->toContain('operationId: deploySuperuserCoolifyGatewayRoutes');
expect($openapi)->toContain('operationId: deploySuperuserCoolifyGatewayApiCode');
expect($openapi)->toContain('operationId: listSuperuserCoolifyGateways');
expect($openapi)->toContain('operationId: getSuperuserCoolifyGithubRunners');
expect($openapi)->toContain('operationId: deploySuperuserCoolifyGithubRunners');
expect($openapi)->toContain('operationId: testSuperuserCoolifyGateway');
expect($openapi)->toContain('operationId: discoverSuperuserCoolifyInstancePlacement');
expect($openapi)->toContain('operationId: createSuperuserCoolifyTarget');
expect($openapi)->toContain('SuperuserCoolifyTarget');
expect($openapi)->toContain('SuperuserCoolifyLoadBalancer');
expect($openapi)->toContain('SuperuserCoolifyGithubRunnerSummary');
expect($openapi)->toContain('SuperuserCoolifyGateway');
});