From d525c12457e5fa862cac719257869e50865d3b63 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:40:54 +0200 Subject: [PATCH] fix(env): keep dev view env saves independent of search Search environment variables case-insensitively by key, add accessible search labeling, and ensure switching to developer view after searching loads the full variable set so non-matching entries are not removed on save. --- .../Shared/EnvironmentVariable/All.php | 48 +++--- .../shared/environment-variable/all.blade.php | 10 +- .../Feature/EnvironmentVariableSearchTest.php | 163 ++++++++++++++++++ 3 files changed, 193 insertions(+), 28 deletions(-) create mode 100644 tests/Feature/EnvironmentVariableSearchTest.php diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 1ddf024a7..65f0bd437 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -7,6 +7,8 @@ use App\Models\EnvironmentVariable; use App\Support\ValidationPatterns; use App\Traits\EnvironmentVariableProtection; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Illuminate\Support\Collection; +use Illuminate\Support\Str; use Livewire\Component; class All extends Component @@ -75,11 +77,24 @@ class All extends Component public function getEnvironmentVariablesProperty() { - $query = $this->resource->environment_variables() - ->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END"); + return $this->getEnvironmentVariables(false); + } - if ($this->search !== '') { - $query->where('key', 'like', "%{$this->search}%"); + public function getEnvironmentVariablesPreviewProperty() + { + return $this->getEnvironmentVariables(true); + } + + private function getEnvironmentVariables(bool $isPreview, bool $withSearch = true): Collection + { + $query = $isPreview + ? $this->resource->environment_variables_preview() + : $this->resource->environment_variables(); + + $query->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END"); + + if ($withSearch && $this->searchTerm() !== '') { + $query->whereRaw('LOWER(key) LIKE ?', ['%'.Str::lower($this->searchTerm()).'%']); } if ($this->is_env_sorting_enabled) { @@ -91,22 +106,9 @@ class All extends Component return $query->get(); } - public function getEnvironmentVariablesPreviewProperty() + private function searchTerm(): string { - $query = $this->resource->environment_variables_preview() - ->orderByRaw("CASE WHEN is_required = true AND (value IS NULL OR value = '') THEN 0 ELSE 1 END"); - - if ($this->search !== '') { - $query->where('key', 'like', "%{$this->search}%"); - } - - if ($this->is_env_sorting_enabled) { - $query->orderBy('key'); - } else { - $query->orderBy('order'); - } - - return $query->get(); + return trim($this->search); } public function getHardcodedEnvironmentVariablesProperty() @@ -156,9 +158,9 @@ class All extends Component return ! in_array($var['key'], $managedKeys); }); - if ($this->search !== '') { + if ($this->searchTerm() !== '') { $hardcodedVars = $hardcodedVars->filter(function ($var) { - return str($var['key'])->contains($this->search, true); + return str($var['key'])->contains($this->searchTerm(), true); }); } @@ -173,9 +175,9 @@ class All extends Component public function getDevView() { - $this->variables = $this->formatEnvironmentVariables($this->environmentVariables); + $this->variables = $this->formatEnvironmentVariables($this->getEnvironmentVariables(false, false)); if ($this->showPreview) { - $this->variablesPreview = $this->formatEnvironmentVariables($this->environmentVariablesPreview); + $this->variablesPreview = $this->formatEnvironmentVariables($this->getEnvironmentVariables(true, false)); } } diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php index 5e308d980..77ac92ffe 100644 --- a/resources/views/livewire/project/shared/environment-variable/all.blade.php +++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php @@ -50,17 +50,17 @@
- +
- -
\ No newline at end of file +
diff --git a/tests/Feature/EnvironmentVariableSearchTest.php b/tests/Feature/EnvironmentVariableSearchTest.php new file mode 100644 index 000000000..5f49776c9 --- /dev/null +++ b/tests/Feature/EnvironmentVariableSearchTest.php @@ -0,0 +1,163 @@ + 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]) + ->set('search', 'api'); + + 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]) + ->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]) + ->set('search', 'api'); + + 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]) + ->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]) + ->set('search', 'api') + ->call('switch') + ->call('submit'); + + expect($application->environment_variables()->pluck('key')->all()) + ->toContain('API_KEY') + ->toContain('DATABASE_URL'); +});