create(); $this->team = $user->teams()->first(); $this->privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]); }); it('does not mount the traffic volume for caddy when traffic analytics is disabled', function () { $server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]); $server->proxy->set('type', 'CADDY'); $server->save(); $server->settings->is_traffic_analytics_enabled = false; $server->settings->save(); $config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh())); expect($config['services']['caddy']['volumes']) ->not->toContain($server->proxyPath().':/traffic'); }); it('mounts the traffic volume for caddy when traffic analytics is enabled', function () { $server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]); $server->proxy->set('type', 'CADDY'); $server->save(); $server->settings->is_traffic_analytics_enabled = true; $server->settings->save(); $config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh())); expect($config['services']['caddy']['volumes']) ->toContain($server->proxyPath().':/traffic'); }); it('uses a default caddy image that supports per-app traffic attribution', function () { $server = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id]); $server->proxy->set('type', 'CADDY'); $server->save(); $config = Yaml::parse(generateDefaultProxyConfiguration($server->fresh())); // caddy-docker-proxy 2.13 ships Caddy 2.11, which knows log_append; the old 2.8 default shipped Caddy 2.7.6. expect($config['services']['caddy']['image'])->toBe('lucaslorentz/caddy-docker-proxy:2.13-alpine') ->and($server->fresh()->caddySupportsLogAppend())->toBeTrue(); }); function caddyTrafficServer(object $test, bool $analyticsEnabled = true, ?string $ip = null): Server { $server = Server::factory()->create(array_filter([ 'team_id' => $test->team->id, 'private_key_id' => $test->privateKey->id, 'ip' => $ip, ])); $server->proxy->set('type', 'CADDY'); $server->save(); $server->settings->is_traffic_analytics_enabled = $analyticsEnabled; $server->settings->save(); return $server->fresh(); } it('mounts the Sentinel traffic log directory into Caddy in production', function () { $server = caddyTrafficServer($this); $config = Yaml::parse(generateDefaultProxyConfiguration($server)); expect(StartSentinel::trafficLogDirectory($server))->toBe('/data/coolify/proxy/caddy') ->and($config['services']['caddy']['volumes'])->toContain('/data/coolify/proxy/caddy:/traffic'); }); it('mounts the Sentinel traffic log directory into Caddy in development', function (?string $ip, string $directory) { config()->set('app.env', 'local'); $server = caddyTrafficServer($this, ip: $ip); $volumes = Yaml::parse(generateDefaultProxyConfiguration($server))['services']['caddy']['volumes']; $trafficVolumes = array_values(array_filter($volumes, fn (string $volume): bool => str_ends_with($volume, ':/traffic'))); // Caddy writes /traffic/access.log, Sentinel reads /access.log. expect($trafficVolumes)->toBe([StartSentinel::trafficLogDirectory($server).':/traffic']) ->and($trafficVolumes[0])->toBe("{$directory}:/traffic"); })->with([ // A dev KVM server has its own Docker daemon and its own /data/coolify, like production. 'dev KVM server' => ['10.221.1.10', '/data/coolify/proxy/caddy'], // The testing-host server uses the host Docker daemon, so it mounts the dev data volume. 'dev testing-host server' => [Server::DEV_TESTING_HOST_IP, '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/proxy/caddy'], ]); it('uses the configured dev data volume for Caddy, Traefik, and Sentinel on the testing-host server', function () { config()->set('app.env', 'local'); config()->set('constants.coolify.dev_data_volume', 'coolify-dev-feature_coolify_data'); $caddy = caddyTrafficServer($this, ip: Server::DEV_TESTING_HOST_IP); $caddyVolumes = Yaml::parse(generateDefaultProxyConfiguration($caddy))['services']['caddy']['volumes']; $traefik = Server::factory()->create(['team_id' => $this->team->id, 'private_key_id' => $this->privateKey->id, 'ip' => Server::DEV_TESTING_HOST_IP]); $traefik->proxy->set('type', 'TRAEFIK'); $traefik->save(); $traefikVolumes = Yaml::parse(generateDefaultProxyConfiguration($traefik->fresh()))['services']['traefik']['volumes']; expect(StartSentinel::trafficLogDirectory($caddy))->toBe('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/caddy') ->and($caddyVolumes)->toContain('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/caddy:/traffic') ->and($traefikVolumes)->toContain('/var/lib/docker/volumes/coolify-dev-feature_coolify_data/_data/proxy/:/traefik'); }); it('falls back to the legacy dev volumes for an invalid volume name', function (?string $volume) { config()->set('app.env', 'local'); config()->set('constants.coolify.dev_data_volume', $volume); config()->set('constants.coolify.dev_backups_volume', $volume); $server = caddyTrafficServer($this, ip: Server::DEV_TESTING_HOST_IP); expect(devHostDockerPath($server, '/data/coolify'))->toBe('/var/lib/docker/volumes/coolify_dev_coolify_data/_data') ->and(devHostDockerPath($server, '/data/coolify/backups/x'))->toBe('/var/lib/docker/volumes/coolify_dev_backups_data/_data/x'); })->with([ 'empty' => [''], 'null' => [null], 'path traversal' => ['../../etc'], 'shell characters' => ['vol;rm -rf /'], ]); it('replaces a stale Caddy traffic mount and removes it when analytics is disabled', function () { config()->set('app.env', 'local'); $server = caddyTrafficServer($this); $config = ['services' => ['caddy' => ['volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', '/data/coolify/proxy/caddy:/traffic', ]]]]; $enabled = applyTrafficAnalyticsToProxyConfigArray($server, $config); $server->settings->is_traffic_analytics_enabled = false; $server->settings->save(); $disabled = applyTrafficAnalyticsToProxyConfigArray($server->fresh(), $enabled); expect($enabled['services']['caddy']['volumes'])->toBe([ '/var/run/docker.sock:/var/run/docker.sock:ro', StartSentinel::trafficLogDirectory($server).':/traffic', ])->and($disabled['services']['caddy']['volumes'])->toBe([ '/var/run/docker.sock:/var/run/docker.sock:ro', ]); });