0]); $this->user = User::factory()->create(); $this->team = Team::factory()->create(); $this->team->members()->attach($this->user, ['role' => 'owner']); $this->project = Project::factory()->create([ 'team_id' => $this->team->id, ]); $this->environment = Environment::factory()->create([ 'project_id' => $this->project->id, ]); $this->actingAs($this->user); }); it('filters production environment variables by key case-insensitively', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'DATABASE_URL', 'value' => 'postgres://example', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'api'); expect($component->instance()->environmentVariables->pluck('key')->all()) ->toBe(['API_KEY']); }); it('orders environment variables by creation order or alphabetically based on the setting', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'ZEBRA', 'value' => 'last-key', 'order' => 1, 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'ALPHA', 'value' => 'first-key', 'order' => 2, 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->assertSet('is_env_sorting_enabled', false); $userManagedKeys = $component->instance()->environmentVariables ->pluck('key') ->filter(fn (string $key) => in_array($key, ['ZEBRA', 'ALPHA'], true)) ->values() ->all(); expect($userManagedKeys)->toBe(['ZEBRA', 'ALPHA']); $component ->set('is_env_sorting_enabled', true) ->call('instantSave'); $alphabeticallyOrderedKeys = $component->instance()->environmentVariables ->pluck('key') ->filter(fn (string $key) => in_array($key, ['ZEBRA', 'ALPHA'], true)) ->values() ->all(); expect($alphabeticallyOrderedKeys) ->toBe(['ALPHA', 'ZEBRA']) ->and($application->settings->fresh()->is_env_sorting_enabled)->toBeTrue(); }); it('treats production environment variable search underscore wildcards literally', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'APIXKEY', 'value' => 'other-secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'api_key'); expect($component->instance()->environmentVariables->pluck('key')->all()) ->toBe(['API_KEY']); }); it('filters preview environment variables by key case-insensitively', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'PREVIEW_TOKEN', 'value' => 'preview-secret', 'is_preview' => true, 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'OTHER_PREVIEW_VALUE', 'value' => 'preview-other', 'is_preview' => true, 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'token'); expect($component->instance()->environmentVariablesPreview->pluck('key')->all()) ->toBe(['PREVIEW_TOKEN']); }); it('filters hardcoded Docker Compose environment variables by key case-insensitively', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: API_TOKEN: hardcoded-secret DATABASE_URL: postgres://example YAML, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->set('search', 'api'); expect($component->instance()->hardcodedEnvironmentVariables->pluck('key')->all()) ->toBe(['API_TOKEN']); }); it('keeps Compose self-referencing environment variables editable without changing Compose', function () { $dockerCompose = <<<'YAML' services: app: image: nginx environment: API_TOKEN: ${API_TOKEN} LOG_LEVEL: ${LOG_LEVEL:-info} FIXED_VALUE: production YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => $dockerCompose, ]); foreach (['API_TOKEN', 'LOG_LEVEL'] as $key) { EnvironmentVariable::create([ 'key' => $key, 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); } $rows = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->instance() ->environmentVariablePageRows; expect($rows->where('kind', 'managed')->pluck('environmentVariable.key')->all()) ->toBe(['API_TOKEN', 'LOG_LEVEL']) ->and($rows->where('kind', 'hardcoded')->pluck('environmentVariable.key')->all()) ->toBe(['FIXED_VALUE']) ->and($service->fresh()->docker_compose_raw)->toBe($dockerCompose); }); it('shows a Compose-defined value as read-only when a managed variable has the same key', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: - API_TOKEN=from-compose YAML, ]); EnvironmentVariable::create([ 'key' => 'API_TOKEN', 'value' => 'from-environment-tab', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables'); expect($component->instance()->environmentVariablePageRows) ->toHaveCount(1) ->and($component->instance()->environmentVariablePageRows->first()['kind'])->toBe('hardcoded') ->and($component->instance()->environmentVariablePageRows->first()['environmentVariable']['value'])->toBe('from-compose'); }); it('keeps a self-referencing Compose environment variable editable without changing Compose', function () { $dockerCompose = <<<'YAML' services: app: image: nginx environment: - API_TOKEN=${API_TOKEN} YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => $dockerCompose, 'docker_compose' => $dockerCompose, ]); $environmentVariable = EnvironmentVariable::create([ 'key' => 'API_TOKEN', 'value' => 'from-environment-tab', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables'); expect($component->instance()->environmentVariablePageRows) ->toHaveCount(1) ->and($component->instance()->environmentVariablePageRows->first()['kind'])->toBe('managed') ->and($component->instance()->environmentVariablePageRows->first()['environmentVariable']->is($environmentVariable))->toBeTrue() ->and($component->instance()->environmentVariablePageRows->first()['composeInfo'])->toBe([ 'services' => ['app'], 'default' => null, 'operator' => null, 'required' => false, ]) ->and($service->fresh()->docker_compose_raw)->toBe($dockerCompose) ->and($service->fresh()->docker_compose)->toBe($dockerCompose); }); it('keeps a self-referencing Compose variable with a default editable', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: LOG_LEVEL: ${LOG_LEVEL:-info} YAML, ]); EnvironmentVariable::create([ 'key' => 'LOG_LEVEL', 'value' => 'debug', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables'); expect($component->instance()->environmentVariablePageRows) ->toHaveCount(1) ->and($component->instance()->environmentVariablePageRows->first()['kind'])->toBe('managed') ->and($component->instance()->environmentVariablePageRows->first()['composeInfo'])->toBe([ 'services' => ['app'], 'default' => 'info', 'operator' => ':-', 'required' => false, ]); }); it('describes derived Compose assignments without replacing them', function () { $dockerCompose = <<<'YAML' services: api: image: nginx environment: API_URL: https://${API_HOST}/v1 YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => $dockerCompose, 'docker_compose' => $dockerCompose, ]); EnvironmentVariable::create([ 'key' => 'API_HOST', 'value' => 'api.example.com', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $rows = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->instance() ->environmentVariablePageRows; expect($rows)->toHaveCount(2) ->and($rows->firstWhere('kind', 'managed')['composeInfo'])->toBe([ 'services' => ['api'], 'default' => null, 'operator' => null, 'required' => false, ]) ->and($rows->firstWhere('kind', 'hardcoded')['environmentVariable']['compose_type'])->toBe('derived') ->and($rows->firstWhere('kind', 'hardcoded')['environmentVariable']['references'])->toBe(['API_HOST']) ->and($service->fresh()->docker_compose_raw)->toBe($dockerCompose) ->and($service->fresh()->docker_compose)->toBe($dockerCompose); Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->set('serviceFilters', ['api']) ->set('variableFilters', ['managed']) ->set('environmentFilter', 'production') ->call('focusComposeEnvironmentVariable', 'API_HOST') ->assertSet('search', 'API_HOST') ->assertSet('serviceFilters', []) ->assertSet('variableFilters', []) ->assertSet('environmentFilter', 'all') ->assertSet('page', 1); }); it('creates editable inputs referenced inside derived Compose values', function () { $dockerCompose = <<<'YAML' services: api: image: nginx environment: API_URL: ${API_SCHEME}://${API_HOST}/v1 YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => $dockerCompose, ]); $service->parse(); expect($service->environment_variables()->where('key', 'API_SCHEME')->exists())->toBeTrue() ->and($service->environment_variables()->where('key', 'API_HOST')->exists())->toBeTrue() ->and(Yaml::parse($service->fresh()->docker_compose_raw))->toBe(Yaml::parse($dockerCompose)); }); it('keeps editable and literal rows when services use the same key differently', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: api: image: nginx environment: FOO: ${FOO} worker: image: nginx environment: FOO: fixed YAML, ]); EnvironmentVariable::create([ 'key' => 'FOO', 'value' => 'editable', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $rows = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->instance() ->environmentVariablePageRows; expect($rows)->toHaveCount(2) ->and($rows->pluck('kind')->all())->toBe(['managed', 'hardcoded']); }); it('treats escaped Compose dollars as a literal value', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: SHELL_EXPRESSION: $${NOT_AN_INPUT} YAML, ]); $row = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->instance() ->environmentVariablePageRows ->first(); expect($row['kind'])->toBe('hardcoded') ->and($row['environmentVariable']['compose_type'])->toBe('literal') ->and($row['environmentVariable']['references'])->toBe([]); }); it('preserves required Compose prefills for service template compatibility', function () { $dockerCompose = <<<'YAML' services: app: image: nginx environment: API_TOKEN: ${API_TOKEN:?API_TOKEN must be set} YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => $dockerCompose, ]); $parsedCompose = $service->parse(); $environmentVariable = $service->environment_variables()->where('key', 'API_TOKEN')->firstOrFail(); expect($environmentVariable->value)->toBe('API_TOKEN must be set') ->and((bool) $environmentVariable->is_required)->toBeTrue() ->and(data_get($parsedCompose, 'services.app.environment.API_TOKEN'))->toBe('${API_TOKEN:?API_TOKEN must be set}') ->and(Yaml::parse($service->fresh()->docker_compose_raw))->toBe(Yaml::parse($dockerCompose)); }); it('synchronizes required metadata when Compose expressions change', function () { $server = Server::factory()->create(['team_id' => $this->team->id]); $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => $server->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: API_TOKEN: ${API_TOKEN:?API_TOKEN must be set} YAML, ]); $environmentVariable = EnvironmentVariable::create([ 'key' => 'API_TOKEN', 'value' => 'configured', 'is_required' => false, 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $service->parse(); expect((bool) $environmentVariable->fresh()->is_required)->toBeTrue(); $service->update([ 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: API_TOKEN: ${API_TOKEN:-optional} YAML, ]); $service->parse(); expect((bool) $environmentVariable->fresh()->is_required)->toBeFalse(); $service->update([ 'docker_compose_raw' => <<<'YAML' services: app: image: nginx YAML, ]); $service->parse(); expect((bool) $environmentVariable->fresh()->is_required)->toBeFalse(); }); it('rejects renaming a Compose-linked variable through Livewire state', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n image: nginx\n environment:\n API_TOKEN: \${API_TOKEN}\n", ]); $environmentVariable = EnvironmentVariable::create([ 'key' => 'API_TOKEN', 'value' => 'secret', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); Livewire::test(Show::class, [ 'env' => $environmentVariable, 'type' => 'service', 'composeInfo' => [ 'services' => ['app'], 'default' => null, 'operator' => null, 'required' => false, ], ])->call('loadValues') ->set('composeInfo', null) ->set('key', 'RENAMED_TOKEN') ->call('submit'); expect($environmentVariable->fresh()->key)->toBe('API_TOKEN'); }); it('creates an editable variable for bare Compose passthrough syntax without rewriting Compose', function () { $dockerCompose = <<<'YAML' services: app: image: nginx environment: - API_TOKEN - EMPTY_VALUE= YAML; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => $dockerCompose, ]); $storedDockerCompose = $service->docker_compose_raw; $service->parse(); $rows = Livewire::test(All::class, ['resource' => $service->fresh()]) ->call('loadEnvironmentVariables') ->instance() ->environmentVariablePageRows; expect($service->environment_variables()->where('key', 'API_TOKEN')->exists())->toBeTrue() ->and($service->environment_variables()->where('key', 'EMPTY_VALUE')->exists())->toBeFalse() ->and($rows)->toHaveCount(2) ->and($rows->first()['kind'])->toBe('managed') ->and($rows->first()['composeInfo'])->toBe([ 'services' => ['app'], 'default' => null, 'operator' => null, 'required' => false, ]) ->and($rows->last()['kind'])->toBe('hardcoded') ->and($rows->last()['environmentVariable']['key'])->toBe('EMPTY_VALUE') ->and($rows->last()['environmentVariable']['compose_type'])->toBe('literal') ->and(Yaml::parse($service->fresh()->docker_compose_raw))->toBe(Yaml::parse($storedDockerCompose)); }); it('preserves explicit empty values and removes overrides only on request', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n image: nginx\n environment:\n LOG_LEVEL: \${LOG_LEVEL:-info}\n", ]); $environmentVariable = EnvironmentVariable::create([ 'key' => 'LOG_LEVEL', 'value' => 'info', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(Show::class, [ 'env' => $environmentVariable, 'type' => 'service', 'composeInfo' => [ 'services' => ['app'], 'default' => 'info', 'operator' => ':-', 'required' => false, ], ])->call('loadValues'); $component->assertSet('value', 'info') ->set('value', 'debug') ->call('submit') ->set('value', '') ->call('submit'); expect($environmentVariable->fresh()->value)->toBe(''); $component->call('useComposeDefault'); expect($environmentVariable->fresh()->value)->toBeNull(); }); it('searches service environment variables without requiring preview variables', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); EnvironmentVariable::create([ 'key' => 'DATABASE_URL', 'value' => 'postgres://example', 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->set('search', 'api') ->assertDontSee('Preview Deployments Environment Variables'); expect($component->instance()->environmentVariables->pluck('key')->all()) ->toBe(['API_KEY']) ->and($component->instance()->showPreview)->toBeFalse(); }); it('pins required service environment variables only while their values are missing', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: HARDCODED_FIRST: configured YAML, ]); EnvironmentVariable::create([ 'key' => 'OPTIONAL_FIRST', 'value' => 'configured', 'order' => 1, 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $required = EnvironmentVariable::create([ 'key' => 'REQUIRED_SECOND', 'value' => '', 'order' => 2, 'is_required' => true, 'resourceable_type' => Service::class, 'resourceable_id' => $service->id, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables'); expect($component->instance()->environmentVariablePageRows->pluck('environmentVariable.key')->all()) ->toBe(['REQUIRED_SECOND', 'OPTIONAL_FIRST', 'HARDCODED_FIRST']); $required->update(['value' => 'configured']); $component->call('$refresh'); expect($component->instance()->environmentVariablePageRows->pluck('environmentVariable.key')->all()) ->toBe(['OPTIONAL_FIRST', 'REQUIRED_SECOND', 'HARDCODED_FIRST']); }); it('does not show the empty production message when search only matches hardcoded variables', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'docker_compose_raw' => <<<'YAML' services: app: image: nginx environment: API_TOKEN: hardcoded-secret DATABASE_URL: postgres://example YAML, ]); $component = Livewire::test(All::class, ['resource' => $service]) ->call('loadEnvironmentVariables') ->set('search', 'api') ->assertDontSee('No environment variables found'); expect($component->instance()->hardcodedEnvironmentVariables->pluck('key')->all()) ->toBe(['API_TOKEN']); }); it('keeps developer view unfiltered after searching', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'DATABASE_URL', 'value' => 'postgres://example', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'api') ->call('switch') ->assertSet('view', 'dev'); expect($component->get('variables')) ->toContain('API_KEY=secret') ->toContain('DATABASE_URL=postgres://example'); }); it('does not delete non-matching variables when saving developer view after searching', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); EnvironmentVariable::create([ 'key' => 'DATABASE_URL', 'value' => 'postgres://example', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'api') ->call('switch') ->call('submit'); expect($application->environment_variables()->pluck('key')->all()) ->toContain('API_KEY') ->toContain('DATABASE_URL'); }); it('hides the preview section when search filters out all preview variables', function () { $application = Application::factory()->create([ 'environment_id' => $this->environment->id, ]); EnvironmentVariable::create([ 'key' => 'API_KEY', 'value' => 'secret', 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $application->environment_variables_preview()->where('key', 'API_KEY')->delete(); EnvironmentVariable::create([ 'key' => 'PREVIEW_TOKEN', 'value' => 'preview-secret', 'is_preview' => true, 'resourceable_type' => Application::class, 'resourceable_id' => $application->id, ]); $component = Livewire::test(All::class, ['resource' => $application]) ->call('loadEnvironmentVariables') ->set('search', 'api') ->assertDontSee('PREVIEW_TOKEN'); expect($component->instance()->environmentVariables->pluck('key')->all()) ->toBe(['API_KEY']) ->and($component->instance()->environmentVariablesPreview)->toHaveCount(0); }); it('keeps a required stored value when saving only a comment', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n environment:\n LEVEL: \${LEVEL:-info}\n CHECK: \${LEVEL:?required}\n", ]); $variable = $service->environment_variables()->create(['key' => 'LEVEL', 'value' => 'info', 'is_required' => true]); $component = Livewire::test(Show::class, [ 'env' => $variable, 'type' => 'service', 'composeInfo' => dockerComposeEnvironmentVariableInfo($service->docker_compose_raw, 'LEVEL'), ])->call('loadValues')->set('comment', 'Keep this value')->call('submit'); expect($variable->fresh()->value)->toBe('info') ->and($variable->fresh()->comment)->toBe('Keep this value'); $component->call('useComposeDefault'); expect($variable->fresh()->value)->toBe('info'); }); it('preserves existing required service template values on repeated parses', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n image: alpine\n environment:\n TZ: \${TZ:?UTC}\n", ]); $variable = $service->environment_variables()->create(['key' => 'TZ', 'value' => 'UTC']); $service->parse(); $service->parse(); expect($variable->fresh()->value)->toBe('UTC'); }); it('prefills required service template values without saving fallback overrides', function () { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n image: alpine\n environment:\n TZ: \${TZ:?UTC}\n LEVEL: \${LEVEL:-info}\n worker:\n image: alpine\n environment:\n LEVEL: \${LEVEL:-warn}\n", ]); $service->parse(); expect($service->environment_variables()->where('key', 'TZ')->firstOrFail()->value)->toBe('UTC') ->and($service->environment_variables()->where('key', 'LEVEL')->firstOrFail()->value)->toBeNull(); }); it('authorizes removing a Compose override on the server', function (string $role) { $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => "services:\n app:\n environment:\n LEVEL: \${LEVEL:-info}\n", ]); $variable = $service->environment_variables()->create(['key' => 'LEVEL', 'value' => 'debug']); $component = Livewire::test(Show::class, ['env' => $variable, 'type' => 'service']); $user = User::factory()->create(); if ($role === 'member') { $this->team->members()->attach($user, ['role' => 'member']); } $this->actingAs($user); $component->call('useComposeDefault')->assertForbidden(); expect($variable->fresh()->value)->toBe('debug'); })->with(['member', 'other-team']); it('preserves migrated null Compose assignments through parsing and comment-only saves', function () { $compose = "services:\n app:\n image: nginx\n environment:\n COLON: \${COLON:-default}\n DASH: \${DASH-default}\n REQUIRED_NULL: \${REQUIRED_NULL:?required}\n PASSTHROUGH:\n"; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => $compose, ]); foreach (['COLON', 'DASH', 'REQUIRED_NULL', 'PASSTHROUGH'] as $key) { $env = $service->environment_variables()->create(['key' => $key, 'value' => null]); $env->forceFill(['preserve_compose_null' => true])->save(); } $parsed = $service->parse(); foreach (['COLON', 'DASH', 'REQUIRED_NULL', 'PASSTHROUGH'] as $key) { $env = $service->environment_variables()->where('key', $key)->firstOrFail(); expect(data_get($parsed, 'services.app.environment.'.$key))->toBe(''); Livewire::test(Show::class, ['env' => $env, 'type' => 'service']) ->call('loadValues')->set('comment', 'unchanged value')->call('submit'); expect($env->fresh()->value)->toBeNull() ->and($env->fresh()->preserve_compose_null)->toBeTrue() ->and($env->fresh()->comment)->toBe('unchanged value'); } expect($service->fresh()->docker_compose_raw)->toBe($compose); }); it('explicitly releases a migrated null variable to Compose defaults', function () { $compose = "services:\n app:\n image: nginx\n environment:\n LOG_LEVEL: \${LOG_LEVEL:-info}\n"; $service = Service::factory()->create([ 'environment_id' => $this->environment->id, 'server_id' => Server::factory()->create(['team_id' => $this->team->id])->id, 'docker_compose_raw' => $compose, ]); $env = $service->environment_variables()->create(['key' => 'LOG_LEVEL', 'value' => null]); $env->forceFill(['preserve_compose_null' => true])->save(); Livewire::test(Show::class, ['env' => $env, 'type' => 'service'])->call('useComposeDefault'); expect($env->fresh()->value)->toBeNull() ->and($env->fresh()->preserve_compose_null)->toBeFalse() ->and(data_get($service->parse(), 'services.app.environment.LOG_LEVEL'))->toBe('${LOG_LEVEL:-info}'); }); it('releases null compatibility after an explicit value edit', function () { $service = Service::factory()->create(['environment_id' => $this->environment->id]); $env = $service->environment_variables()->create(['key' => 'EDITED', 'value' => null]); $env->forceFill(['preserve_compose_null' => true])->save(); $env->update(['value' => 'configured']); expect($env->fresh()->preserve_compose_null)->toBeFalse(); $env->update(['value' => null]); expect($env->fresh()->preserve_compose_null)->toBeFalse(); });