getDefinition(); expect($definition->hasOption('bunny'))->toBeFalse() ->and($definition->hasOption('github-releases'))->toBeFalse() ->and($definition->hasOption('release'))->toBeFalse() ->and($definition->hasOption('nightly'))->toBeFalse() ->and($definition->hasOption('templates'))->toBeFalse(); }); it('loads service templates from the Coollabs CDN', function () { expect(config('constants.services.official')) ->toBe('https://cdn.coollabs.io/coolify/service-templates-latest.json'); }); it('only removes validated Coolify CDN temporary directories', function () { $command = new class extends SyncCdn { public function removeDirectory(string $path): void { $this->removeTemporaryDirectory($path); } }; $invalidDirectory = sys_get_temp_dir().'/unrelated-directory-'.uniqid(); $validDirectory = sys_get_temp_dir().'/coollabs-cdn-files-'.uniqid(); mkdir($invalidDirectory); mkdir($validDirectory); $command->removeDirectory(''); $command->removeDirectory($invalidDirectory); $command->removeDirectory($validDirectory); expect($invalidDirectory)->toBeDirectory() ->and($validDirectory)->not->toBeDirectory(); rmdir($invalidDirectory); }); it('selects the environment and release files to sync to GitHub', function (string $targetDirectory, string $environment, array $selectedBasenames) { Http::fake([ 'api.github.com/repos/coollabsio/coolify/releases*' => Http::response([], 200), ]); $binDir = sys_get_temp_dir().'/sync-cdn-bin-'.uniqid(); $logFile = sys_get_temp_dir().'/sync-cdn-'.uniqid().'.log'; mkdir($binDir, 0755, true); createFakeSyncCdnBinary($binDir, 'gh', <<<'SH' #!/bin/sh printf 'gh %s\n' "$*" >> "$SYNC_CDN_TEST_LOG" if [ "$1" = "repo" ] && [ "$2" = "clone" ]; then mkdir -p "$4" fi exit 0 SH); createFakeSyncCdnBinary($binDir, 'git', <<<'SH' #!/bin/sh printf 'git %s\n' "$*" >> "$SYNC_CDN_TEST_LOG" if [ "$1" = "status" ]; then printf 'M json/releases.json\n' fi if [ "$1" = "diff" ]; then if [ -f json/coolify/nightly/releases.json ]; then printf 'json/coolify/nightly/releases.json\n' else printf 'json/coolify/releases.json\n' fi fi exit 0 SH); $originalPath = getenv('PATH') ?: ''; putenv("PATH={$binDir}:{$originalPath}"); putenv("SYNC_CDN_TEST_LOG={$logFile}"); $allBasenames = [ 'releases.json', 'versions.json', 'docker-compose.yml', 'docker-compose.prod.yml', '.env.production', 'install.sh', 'upgrade.sh', 'upgrade-postgres.sh', ...($environment === 'production' ? ['patch.sh'] : []), 'service-templates-latest.json', ]; $allTargets = array_map(fn (string $file) => "$targetDirectory/$file", $allBasenames); $selectedTargets = array_map(fn (string $file) => "$targetDirectory/$file", $selectedBasenames); try { $this->artisan('sync:cdn') ->expectsChoice('Which environment would you like to sync?', $environment, [ 'production' => 'Production', 'nightly' => 'Nightly', ]) ->expectsChoice('Which files would you like to sync?', $selectedTargets, $allTargets) ->assertExitCode(0); } finally { putenv("PATH={$originalPath}"); putenv('SYNC_CDN_TEST_LOG'); } $log = file_get_contents($logFile); expect($log) ->toContain('gh pr create --repo coollabsio/coollabs-cdn') ->not->toContain('coollabsio/coolify-cdn'); foreach ($selectedTargets as $selectedTarget) { expect($log)->toContain($selectedTarget); } foreach (array_diff($allTargets, $selectedTargets) as $unselectedTarget) { expect($log)->not->toContain($unselectedTarget); } $pullRequestCommand = substr($log, strrpos($log, 'gh pr create')); expect($pullRequestCommand) ->toContain("$targetDirectory/releases.json") ->not->toContain("$targetDirectory/versions.json"); Http::assertSentCount(1); })->with([ 'select production files' => ['json/coolify', 'production', ['releases.json', 'versions.json']], 'select nightly with all files selected by default' => ['json/coolify/nightly', 'nightly', [ 'releases.json', 'versions.json', 'docker-compose.yml', 'docker-compose.prod.yml', '.env.production', 'install.sh', 'upgrade.sh', 'upgrade-postgres.sh', 'service-templates-latest.json', ]], ]); it('purges the CDN files from the Cloudflare cache', function (string $environment, array $cdnDirectories) { config()->set('constants.cloudflare.api_token', 'test-token'); config()->set('constants.cloudflare.zone_id', 'test-zone'); Http::fake([ 'https://api.cloudflare.com/*' => Http::response(['success' => true], 200), ]); $this->artisan('sync:cdn --purge') ->expectsChoice('Which environment would you like to sync?', $environment, [ 'production' => 'Production', 'nightly' => 'Nightly', ]) ->assertExitCode(0); Http::assertSentCount(1); Http::assertSent(fn ($request) => $request->method() === 'POST' && $request->url() === 'https://api.cloudflare.com/client/v4/zones/test-zone/purge_cache' && $request->hasHeader('Authorization', 'Bearer test-token') && $request['files'] === collect($cdnDirectories)->flatMap(fn (string $cdnDirectory) => array_map(fn (string $file) => "https://cdn.coollabs.io/$cdnDirectory/$file", [ 'releases.json', 'versions.json', 'docker-compose.yml', 'docker-compose.prod.yml', '.env.production', 'install.sh', 'upgrade.sh', 'upgrade-postgres.sh', ...($environment === 'production' ? ['patch.sh'] : []), 'service-templates-latest.json', ]))->all()); })->with([ 'production' => ['production', ['coolify']], 'nightly with legacy path' => ['nightly', ['coolify/nightly', 'coolify-nightly']], ]); it('fails the Cloudflare purge without credentials or on an API error', function (?string $apiToken, array $response) { config()->set('constants.cloudflare.api_token', $apiToken); config()->set('constants.cloudflare.zone_id', 'test-zone'); Http::fake([ 'https://api.cloudflare.com/*' => Http::response($response, 400), ]); $this->artisan('sync:cdn --purge') ->expectsChoice('Which environment would you like to sync?', 'production', [ 'production' => 'Production', 'nightly' => 'Nightly', ]) ->assertExitCode(1); if ($apiToken === null) { Http::assertNothingSent(); } })->with([ 'missing token' => [null, []], 'api error' => ['test-token', ['success' => false, 'errors' => [['code' => 10000, 'message' => 'Authentication error']]]], ]);