create(); $server = Server::factory()->create(['team_id' => $team->id]); $destination = StandaloneDocker::query()->where('server_id', $server->id)->first() ?? StandaloneDocker::factory()->create(['server_id' => $server->id]); $project = Project::factory()->create(['team_id' => $team->id]); $environment = Environment::factory()->create(['project_id' => $project->id]); $this->application = Application::factory()->create([ 'build_pack' => 'dockercompose', 'environment_id' => $environment->id, 'destination_id' => $destination->id, 'destination_type' => $destination->getMorphClass(), 'docker_compose_raw' => <<<'YAML' services: db: image: postgres:16-alpine environment: POSTGRES_USER: ${SERVICE_USER_DB} POSTGRES_PASSWORD: ${SERVICE_PASSWORD_DB} YAML, ]); }); function composePreviewMagicParse(Application $application, int $pullRequestId = 0): void { $previewId = null; if ($pullRequestId !== 0) { $previewId = ApplicationPreview::query()->firstOrCreate([ 'application_id' => $application->id, 'pull_request_id' => $pullRequestId, ], [ 'pull_request_html_url' => "https://github.com/example/repository/pull/{$pullRequestId}", ])->id; } $application->fresh()->parse(pull_request_id: $pullRequestId, preview_id: $previewId); } /** * @return array{production: array, preview: array} */ function composePreviewMagicValues(Application $application): array { $keys = ['SERVICE_PASSWORD_DB', 'SERVICE_USER_DB']; return [ 'production' => $application->environment_variables()->whereIn('key', $keys)->pluck('value', 'key')->sortKeys()->all(), 'preview' => $application->environment_variables_preview()->whereIn('key', $keys)->pluck('value', 'key')->sortKeys()->all(), ]; } it('generates the magic variables of a first pull request parse only in the preview variables', function () { composePreviewMagicParse($this->application, 7); $values = composePreviewMagicValues($this->application); expect($values['production'])->toBe([]) ->and(array_keys($values['preview']))->toBe(['SERVICE_PASSWORD_DB', 'SERVICE_USER_DB']) ->and($values['preview']['SERVICE_PASSWORD_DB'])->not->toBeEmpty(); // The production deployment generates its own values and keeps the preview values. composePreviewMagicParse($this->application); $afterProduction = composePreviewMagicValues($this->application); expect($afterProduction['preview'])->toBe($values['preview']) ->and($afterProduction['production']['SERVICE_PASSWORD_DB'])->not->toBeEmpty() ->and($afterProduction['production']['SERVICE_PASSWORD_DB'])->not->toBe($values['preview']['SERVICE_PASSWORD_DB']); }); it('does not copy a generated production magic secret to the preview variables', function (string $key) { EnvironmentVariable::create([ 'key' => $key, 'value' => 'production-secret', 'is_preview' => false, 'resourceable_type' => Application::class, 'resourceable_id' => $this->application->id, ]); expect($this->application->environment_variables_preview()->where('key', $key)->exists())->toBeFalse(); })->with([ 'SERVICE_PASSWORD_DB', 'SERVICE_PASSWORD_64_DB', 'SERVICE_PASSWORDWITHSYMBOLS_DB', 'SERVICE_USER_DB', 'SERVICE_LOWERCASEUSER_DB', 'SERVICE_BASE64_DB', 'SERVICE_BASE64_64_DB', 'SERVICE_BASE64_128_DB', 'SERVICE_REALBASE64_DB', 'SERVICE_HEX_32_DB', ]); it('still copies a normal or service url production variable to the preview variables', function (string $key) { EnvironmentVariable::create([ 'key' => $key, 'value' => 'production-value', 'is_preview' => false, 'resourceable_type' => Application::class, 'resourceable_id' => $this->application->id, ]); expect($this->application->environment_variables_preview()->where('key', $key)->value('value'))->toBe('production-value'); })->with(['DATABASE_HOST', 'SERVICE_URL_DB', 'SERVICE_FQDN_DB', 'SERVICE_PASSWORD_DB_MAIN']); it('gives a private pull request its own magic values instead of the production values', function () { composePreviewMagicParse($this->application); $production = composePreviewMagicValues($this->application)['production']; composePreviewMagicParse($this->application, 7); $values = composePreviewMagicValues($this->application); expect($values['production'])->toBe($production) ->and(array_keys($values['preview']))->toBe(['SERVICE_PASSWORD_DB', 'SERVICE_USER_DB']) ->and($values['preview']['SERVICE_PASSWORD_DB'])->not->toBeEmpty() ->and($values['preview']['SERVICE_PASSWORD_DB'])->not->toBe($production['SERVICE_PASSWORD_DB']) ->and($values['preview']['SERVICE_USER_DB'])->not->toBe($production['SERVICE_USER_DB']); // The next pull request parse keeps the preview values, so the preview database keeps its password. composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application))->toBe($values); }); it('keeps an existing preview copy of a production magic value for a private pull request', function () { composePreviewMagicParse($this->application); $production = composePreviewMagicValues($this->application)['production']; // A preview copy that an earlier version created; existing preview databases use this password. $this->application->environment_variables_preview()->create([ 'key' => 'SERVICE_PASSWORD_DB', 'value' => $production['SERVICE_PASSWORD_DB'], 'is_preview' => true, ]); composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application)['preview']['SERVICE_PASSWORD_DB'])->toBe($production['SERVICE_PASSWORD_DB']); }); it('keeps a preview magic variable that the user created', function () { EnvironmentVariable::create([ 'key' => 'SERVICE_PASSWORD_DB', 'value' => 'user-preview-password', 'is_preview' => true, 'resourceable_type' => Application::class, 'resourceable_id' => $this->application->id, ]); composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application)['preview']['SERVICE_PASSWORD_DB'])->toBe('user-preview-password'); }); it('gives a public pull request its own magic values instead of the production values', function () { composePreviewMagicParse($this->application); $production = composePreviewMagicValues($this->application)['production']; $this->application->settings->update(['is_pr_deployments_public_enabled' => true]); composePreviewMagicParse($this->application, 7); $values = composePreviewMagicValues($this->application); expect($values['production'])->toBe($production) ->and($values['preview']['SERVICE_PASSWORD_DB'])->not->toBeEmpty() ->and($values['preview']['SERVICE_PASSWORD_DB'])->not->toBe($production['SERVICE_PASSWORD_DB']) ->and($values['preview']['SERVICE_USER_DB'])->not->toBe($production['SERVICE_USER_DB']); // The next pull request parse keeps the preview values, so the preview database keeps its password. composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application))->toBe($values); }); it('keeps a changed preview magic value of a public pull request', function () { composePreviewMagicParse($this->application); $this->application->settings->update(['is_pr_deployments_public_enabled' => true]); $this->application->environment_variables_preview()->create([ 'key' => 'SERVICE_PASSWORD_DB', 'value' => 'preview-password', 'is_preview' => true, ]); composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application)['preview']['SERVICE_PASSWORD_DB'])->toBe('preview-password'); }); it('replaces an existing preview copy of a production magic value for a public pull request', function () { composePreviewMagicParse($this->application); $production = composePreviewMagicValues($this->application)['production']; $this->application->environment_variables_preview()->create([ 'key' => 'SERVICE_PASSWORD_DB', 'value' => $production['SERVICE_PASSWORD_DB'], 'is_preview' => true, ]); $this->application->settings->update(['is_pr_deployments_public_enabled' => true]); composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application)['preview']['SERVICE_PASSWORD_DB']) ->not->toBeEmpty() ->not->toBe($production['SERVICE_PASSWORD_DB']); }); it('keeps the production magic value of a public pull request preview from before v4.5.1', function () { composePreviewMagicParse($this->application); $production = composePreviewMagicValues($this->application)['production']; $this->application->environment_variables_preview()->create([ 'key' => 'SERVICE_PASSWORD_DB', 'value' => $production['SERVICE_PASSWORD_DB'], 'is_preview' => true, ]); ApplicationPreview::query()->create([ 'application_id' => $this->application->id, 'pull_request_id' => 7, 'pull_request_html_url' => 'https://github.com/example/repository/pull/7', 'keeps_production_values' => true, ]); $this->application->settings->update(['is_pr_deployments_public_enabled' => true]); composePreviewMagicParse($this->application, 7); expect(composePreviewMagicValues($this->application)['preview']['SERVICE_PASSWORD_DB'])->toBe($production['SERVICE_PASSWORD_DB']); });