From cbe734c15bae53a4c3a5d967d352f26115c460bb Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Sun, 25 Oct 2026 23:58:51 +0100 Subject: [PATCH] fix(env): keep the value of locked environment variables on update Closes #12299 Co-Authored-By: Claude Opus 5.5 --- .../Shared/EnvironmentVariable/Show.php | 15 ++- .../LockedEnvironmentVariableUpdateTest.php | 118 ++++++++++++++++++ 2 files changed, 128 insertions(+), 5 deletions(-) create mode 100644 tests/Feature/LockedEnvironmentVariableUpdateTest.php diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 71ecb1631a..73174728af 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -227,11 +227,14 @@ class Show extends Component $this->env->is_shared = $this->is_shared; } $this->env->key = $this->key; - $this->env->value = $this->value; + // The form never holds a locked value, so keep the stored one and keep the lock. + if (! $this->env->is_shown_once) { + $this->env->value = $this->value; + $this->env->is_shown_once = $this->is_shown_once; + } $this->env->comment = $this->comment; $this->env->is_multiline = $this->is_multiline; $this->env->is_literal = $this->is_literal; - $this->env->is_shown_once = $this->is_shown_once; $this->env->save(); $this->valuesLoaded = true; } else { @@ -359,9 +362,11 @@ class Show extends Component } } - if (! $this->isSharedVariable && $this->is_required && str($this->value)->isEmpty()) { - $oldValue = $this->env->getOriginal('value'); - $this->value = $oldValue; + $submittedValue = $this->env->is_shown_once ? $this->env->value : $this->value; + if (! $this->isSharedVariable && $this->is_required && str($submittedValue)->isEmpty()) { + if (! $this->env->is_shown_once) { + $this->value = $this->env->getOriginal('value'); + } $this->dispatch('error', 'Required environment variables cannot be empty.'); return; diff --git a/tests/Feature/LockedEnvironmentVariableUpdateTest.php b/tests/Feature/LockedEnvironmentVariableUpdateTest.php new file mode 100644 index 0000000000..75d4cc5289 --- /dev/null +++ b/tests/Feature/LockedEnvironmentVariableUpdateTest.php @@ -0,0 +1,118 @@ + 0]); + + $this->user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->team->members()->attach($this->user, ['role' => 'owner']); + $project = Project::factory()->create(['team_id' => $this->team->id]); + $environment = Environment::factory()->create(['project_id' => $project->id]); + $this->application = Application::factory()->create(['environment_id' => $environment->id]); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +function createLockedVariableForUpdateTest(bool $required = false, ?string $value = 'locked-secret'): EnvironmentVariable +{ + return EnvironmentVariable::create([ + 'key' => 'API_TOKEN', + 'value' => $value, + 'is_shown_once' => true, + 'is_required' => $required, + 'is_runtime' => false, + 'resourceable_type' => Application::class, + 'resourceable_id' => test()->application->id, + ]); +} + +test('changing a flag of a locked variable keeps its value', function (bool $required) { + $env = createLockedVariableForUpdateTest(required: $required); + + Livewire::test(Show::class, ['env' => $env, 'type' => 'application']) + ->call('loadValues') + ->assertSet('value', null) + ->set('is_runtime', true) + ->call('submit') + ->assertDispatched('success') + ->assertNotDispatched('error') + ->assertSet('value', null); + + $env->refresh(); + expect($env->value)->toBe('locked-secret') + ->and($env->is_runtime)->toBeTrue() + ->and((bool) $env->is_shown_once)->toBeTrue(); +})->with(['not required' => false, 'required' => true]); + +test('a locked required variable with an empty stored value is still rejected', function () { + $env = createLockedVariableForUpdateTest(required: true, value: null); + + Livewire::test(Show::class, ['env' => $env, 'type' => 'application']) + ->call('loadValues') + ->set('is_runtime', true) + ->call('submit') + ->assertDispatched('error', 'Required environment variables cannot be empty.'); + + expect($env->refresh()->is_runtime)->toBeFalse(); +}); + +test('submit cannot replace the value of a locked variable or unlock it', function () { + $env = createLockedVariableForUpdateTest(); + + Livewire::test(Show::class, ['env' => $env, 'type' => 'application']) + ->call('loadValues') + ->set('value', 'replaced') + ->set('is_shown_once', false) + ->call('submit') + ->assertDispatched('success'); + + $env->refresh(); + expect($env->value)->toBe('locked-secret') + ->and((bool) $env->is_shown_once)->toBeTrue(); +}); + +test('a failed update of a locked variable does not expose its value', function () { + $env = createLockedVariableForUpdateTest(required: true); + + Livewire::test(Show::class, ['env' => $env, 'type' => 'application']) + ->call('loadValues') + ->set('comment', str_repeat('a', 257)) + ->call('submit') + ->assertHasErrors('comment') + ->assertSet('value', null); +}); + +test('changing the comment of a locked shared variable keeps its value', function () { + $env = SharedEnvironmentVariable::create([ + 'key' => 'TEAM_TOKEN', + 'value' => 'locked-shared-secret', + 'is_shown_once' => true, + 'type' => 'team', + 'team_id' => $this->team->id, + ]); + + Livewire::test(Show::class, ['env' => $env, 'type' => 'team']) + ->call('loadValues') + ->set('comment', 'rotated monthly') + ->call('submit') + ->assertDispatched('success'); + + $env->refresh(); + expect($env->value)->toBe('locked-shared-secret') + ->and($env->comment)->toBe('rotated monthly'); +});