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('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, 'Fatal error'))->toMatchArray([ 'ok' => false, 'reason' => 'invalid_json', ]); }); 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('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($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/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('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('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('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($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: testSuperuserCoolifyGateway'); expect($openapi)->toContain('operationId: discoverSuperuserCoolifyInstancePlacement'); expect($openapi)->toContain('operationId: createSuperuserCoolifyTarget'); expect($openapi)->toContain('SuperuserCoolifyTarget'); expect($openapi)->toContain('SuperuserCoolifyLoadBalancer'); expect($openapi)->toContain('SuperuserCoolifyGateway'); });