0]); config(['app.maintenance.store' => 'array', 'cache.default' => 'array']); Process::fake(); Queue::fake(); Server::flushIdentityMap(); $team = Team::factory()->create(); $privateKey = PrivateKey::factory()->create(['team_id' => $team->id]); $server = Server::factory()->create(['team_id' => $team->id, 'private_key_id' => $privateKey->id]); $this->destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail(); $project = Project::factory()->create(['team_id' => $team->id]); $this->environment = Environment::factory()->create(['project_id' => $project->id]); $this->executor = new class { public array $commands = []; public function execute(array $commands, $database, Activity $activity): Activity { $this->commands = $commands; return $activity; } }; app()->instance(DatabaseStartCommandExecutor::class, $this->executor); }); /** * @return array{command: string, healthcheck: list, environment: list} */ function legacyPasswordQuotingStartedService(object $executor, string $uuid): array { foreach ($executor->commands as $command) { if (preg_match("#^echo '([^']+)' \| base64 -d \| tee \S+/docker-compose\.yml#", $command, $matches)) { $service = Yaml::parse(base64_decode($matches[1]))['services'][$uuid]; return ['command' => $service['command'], 'healthcheck' => $service['healthcheck']['test'], 'environment' => $service['environment']]; } } throw new RuntimeException('No docker-compose.yml was written.'); } dataset('legacy-password-quoting-engines', [ 'keydb' => ['create_standalone_keydb', StartKeydb::class, 'keydb_password', 'keydb-server --requirepass %s --appendonly yes'], 'dragonfly' => ['create_standalone_dragonfly', StartDragonfly::class, 'dragonfly_password', 'dragonfly --requirepass %s'], ]); it('keeps the unquoted v4.3.23 start command for a database created before the upgrade', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'ab\c']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, 'ab\c')); })->with('legacy-password-quoting-engines'); it('quotes the exact stored password for a database created after the upgrade', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'ab\c']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($database->fresh()->legacy_password_quoting)->toBeFalse() ->and($service['command'])->toBe(sprintf($commandFormat, "'ab\\c'")) ->and($service['healthcheck'][3])->toBe('ab\c'); })->with('legacy-password-quoting-engines'); it('quotes legacy passwords that v4.3.23 passed unchanged or could not start with', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'a(b)c']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, "'a(b)c'")); })->with('legacy-password-quoting-engines'); it('uses the exact quoting after the password of a legacy database changes', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'ab\c']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $database = $database->fresh(); $database->update(['name' => 'renamed']); expect($database->fresh()->legacy_password_quoting)->toBeTrue(); $database->update([$passwordColumn => 'new\pass']); $start::run($database->fresh(), new Activity); expect($database->fresh()->legacy_password_quoting)->toBeFalse() ->and(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, "'new\\pass'")); })->with('legacy-password-quoting-engines'); it('quotes a remote secret password of a legacy database exactly', function (string $create, string $start, string $passwordColumn, string $commandFormat) { Http::fake(['https://api.doppler.com/*' => Http::response(['REDIS_PASSWORD' => 'ab\\c'])]); $token = IntegrationToken::query()->create([ 'team_id' => $this->environment->project->team_id, 'provider' => 'doppler', 'name' => 'Doppler', 'token' => 'the-secret-token', 'capabilities' => ['secrets'], ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $database->secretManagerLink()->create(['integration_token_id' => $token->id]); $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{vault.REDIS_PASSWORD}}']); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, "'ab\\c'")); })->with('legacy-password-quoting-engines'); it('marks only databases that exist before the migration as legacy', function () { $existingKeydb = create_standalone_keydb($this->environment->id, $this->destination); $existingDragonfly = create_standalone_dragonfly($this->environment->id, $this->destination); $migration = require database_path('migrations/2026_10_02_120000_add_legacy_password_quoting_to_keydb_and_dragonfly.php'); $migration->down(); expect(Schema::hasColumn('standalone_keydbs', 'legacy_password_quoting'))->toBeFalse(); $migration->up(); $newKeydb = create_standalone_keydb($this->environment->id, $this->destination); $newDragonfly = create_standalone_dragonfly($this->environment->id, $this->destination); expect($existingKeydb->fresh()->legacy_password_quoting)->toBeTrue() ->and($existingDragonfly->fresh()->legacy_password_quoting)->toBeTrue() ->and($newKeydb->fresh()->legacy_password_quoting)->toBeFalse() ->and($newDragonfly->fresh()->legacy_password_quoting)->toBeFalse(); }); it('keeps the exact v4.3.23 start command when Docker Compose split or cut the unquoted legacy password', function (string $create, string $start, string $passwordColumn, string $commandFormat, string $password) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => $password]); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, $password)); })->with('legacy-password-quoting-engines')->with([ 'semicolon' => 'ab;cd', 'space' => 'a b', 'tab' => "a\tb", 'pipe' => 'x|y', 'ampersand' => 'p&q', 'redirects' => 'a', ]); it('quotes a plain legacy password, which gives Docker Compose the same argument as v4.3.23', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'abc']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, "'abc'")); })->with('legacy-password-quoting-engines'); it('quotes shell control characters in the password of a database created after the upgrade', function (string $create, string $start, string $passwordColumn, string $commandFormat, string $password) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => $password]); $start::run($database->fresh(), new Activity); expect(legacyPasswordQuotingStartedService($this->executor, $database->uuid)['command']) ->toBe(sprintf($commandFormat, escapeshellarg($password))); })->with('legacy-password-quoting-engines')->with([ 'semicolon' => 'ab;cd', 'space' => 'a b', 'pipe' => 'x|y', ]); it('runs with a plain REDIS_PASSWORD variable instead of the stored password without changing either', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, "'envpass'")) ->and($service['healthcheck'][3])->toBe('envpass') ->and($service['environment'])->toContain('REDIS_PASSWORD=envpass') ->and($env->fresh()->value)->toBe('envpass') ->and($database->fresh()->{$passwordColumn})->toBe('storedpass'); })->with('legacy-password-quoting-engines'); it('runs with the stored password again after the REDIS_PASSWORD variable is removed', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']); $start::run($database->fresh(), new Activity); $env->delete(); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, "'storedpass'")) ->and($service['healthcheck'][3])->toBe('storedpass') ->and($service['environment'])->toContain('REDIS_PASSWORD=storedpass'); })->with('legacy-password-quoting-engines'); it('runs with the stored password when the REDIS_PASSWORD variable is empty', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, "'storedpass'")) ->and($service['healthcheck'][3])->toBe('storedpass'); })->with('legacy-password-quoting-engines'); it('keeps the exact v4.3.23 command of a legacy database with a different plain REDIS_PASSWORD variable', function (string $create, string $start, string $passwordColumn, string $commandFormat) { $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored;pass']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'envpass']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, 'stored;pass')) ->and($service['healthcheck'][3])->toBe('stored;pass') ->and($service['environment'])->toContain('REDIS_PASSWORD=envpass') ->and($env->fresh()->value)->toBe('envpass') ->and($database->fresh()->{$passwordColumn})->toBe('stored;pass') ->and($database->fresh()->legacy_password_quoting)->toBeTrue(); })->with('legacy-password-quoting-engines'); it('runs with a remote secret REDIS_PASSWORD without storing it', function (string $create, string $start, string $passwordColumn, string $commandFormat) { Http::fake(['https://api.doppler.com/*' => Http::response(['REDIS_PASSWORD' => 'secretpass'])]); $token = IntegrationToken::query()->create([ 'team_id' => $this->environment->project->team_id, 'provider' => 'doppler', 'name' => 'Doppler', 'token' => 'the-secret-token', 'capabilities' => ['secrets'], ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $database->secretManagerLink()->create(['integration_token_id' => $token->id]); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{vault.REDIS_PASSWORD}}']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, "'secretpass'")) ->and($service['healthcheck'][3])->toBe('secretpass') ->and($env->fresh()->value)->toBe('{{vault.REDIS_PASSWORD}}') ->and($database->fresh()->{$passwordColumn})->toBe('storedpass'); })->with('legacy-password-quoting-engines'); it('runs with a shared REDIS_PASSWORD variable without storing it', function (string $create, string $start, string $passwordColumn, string $commandFormat) { SharedEnvironmentVariable::query()->create([ 'key' => 'REDIS_PASSWORD', 'value' => 'sharedpass', 'type' => 'team', 'team_id' => $this->environment->project->team_id, ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{team.REDIS_PASSWORD}}']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, "'sharedpass'")) ->and($service['healthcheck'][3])->toBe('sharedpass') ->and($env->fresh()->value)->toBe('{{team.REDIS_PASSWORD}}') ->and($database->fresh()->{$passwordColumn})->toBe('storedpass'); })->with('legacy-password-quoting-engines'); it('keeps the stored password of a legacy database with a shared REDIS_PASSWORD variable', function (string $create, string $start, string $passwordColumn, string $commandFormat) { SharedEnvironmentVariable::query()->create([ 'key' => 'REDIS_PASSWORD', 'value' => 'sharedpass', 'type' => 'team', 'team_id' => $this->environment->project->team_id, ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored;pass']); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{team.REDIS_PASSWORD}}']); $start::run($database->fresh(), new Activity); $service = legacyPasswordQuotingStartedService($this->executor, $database->uuid); expect($service['command'])->toBe(sprintf($commandFormat, 'stored;pass')) ->and($service['healthcheck'][3])->toBe('stored;pass') ->and($database->fresh()->{$passwordColumn})->toBe('stored;pass') ->and($database->fresh()->legacy_password_quoting)->toBeTrue(); })->with('legacy-password-quoting-engines'); it('shows the password the server uses in the connection URLs', function (string $create, string $start, string $passwordColumn, string $commandFormat) { SharedEnvironmentVariable::query()->create([ 'key' => 'REDIS_PASSWORD', 'value' => 'shared/pass', 'type' => 'team', 'team_id' => $this->environment->project->team_id, ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'stored/pass', 'is_public' => true, 'public_port' => 16379]); expect($database->fresh()->internal_db_url)->toContain(':stored%2Fpass@') ->and($database->fresh()->external_db_url)->toContain(':stored%2Fpass@'); $env = $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => 'env/pass']); expect($database->fresh()->internal_db_url)->toContain(':env%2Fpass@') ->and($database->fresh()->external_db_url)->toContain(':env%2Fpass@'); $env->update(['value' => '{{team.REDIS_PASSWORD}}']); expect($database->fresh()->internal_db_url)->toContain(':shared%2Fpass@') ->and($database->fresh()->external_db_url)->toContain(':shared%2Fpass@'); DB::table($database->getTable())->where('id', $database->id)->update(['legacy_password_quoting' => true]); expect($database->fresh()->internal_db_url)->toContain(':stored%2Fpass@') ->and($database->fresh()->external_db_url)->toContain(':stored%2Fpass@'); })->with('legacy-password-quoting-engines'); it('shows the unresolved remote secret reference in the connection URL without fetching it', function (string $create, string $start, string $passwordColumn, string $commandFormat) { Http::fake(); $token = IntegrationToken::query()->create([ 'team_id' => $this->environment->project->team_id, 'provider' => 'doppler', 'name' => 'Doppler', 'token' => 'the-secret-token', 'capabilities' => ['secrets'], ]); $database = $create($this->environment->id, $this->destination, [$passwordColumn => 'storedpass']); $database->secretManagerLink()->create(['integration_token_id' => $token->id]); $database->runtime_environment_variables()->create(['key' => 'REDIS_PASSWORD', 'value' => '{{vault.REDIS_PASSWORD}}']); expect($database->fresh()->internal_db_url)->toContain(':'.rawurlencode('{{vault.REDIS_PASSWORD}}').'@') ->not->toContain('storedpass'); Http::assertNothingSent(); })->with('legacy-password-quoting-engines');