From 1802522c608619df5f00e411a565d10a7d39c1f3 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Wed, 3 Jun 2026 13:43:26 +0200
Subject: [PATCH] fix(env-vars): treat search wildcards literally
Escape SQL LIKE wildcard characters in environment variable searches and hide production or preview sections when the filtered results are empty.
---
.../Shared/EnvironmentVariable/All.php | 25 +++---
.../shared/environment-variable/all.blade.php | 34 +++----
...ronmentVariableMultilineToggleViewTest.php | 9 ++
.../Feature/EnvironmentVariableSearchTest.php | 88 +++++++++++++++++++
4 files changed, 127 insertions(+), 29 deletions(-)
diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index 2a1a47b4d..a19837e16 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -39,7 +39,12 @@ class All extends Component
'environmentVariableDeleted' => 'refreshEnvs',
];
- public function updatedSearch()
+ public function updatedSearch(): void
+ {
+ $this->clearEnvironmentVariableCaches();
+ }
+
+ private function clearEnvironmentVariableCaches(): void
{
unset($this->environmentVariables);
unset($this->environmentVariablesPreview);
@@ -95,7 +100,9 @@ private function getEnvironmentVariables(bool $isPreview, bool $withSearch = tru
$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()).'%']);
+ $escapedSearch = addcslashes(Str::lower($this->searchTerm()), '%_\\');
+
+ $query->whereRaw("LOWER(key) LIKE ? ESCAPE '\\'", ['%'.$escapedSearch.'%']);
}
if ($this->is_env_sorting_enabled) {
@@ -322,12 +329,7 @@ private function handleSingleSubmit($data)
$environment->order = $maxOrder + 1;
$environment->save();
- // Clear computed property cache to force refresh
- unset($this->environmentVariables);
- unset($this->environmentVariablesPreview);
- unset($this->hardcodedEnvironmentVariables);
- unset($this->hardcodedEnvironmentVariablesPreview);
- unset($this->hasEnvironmentVariables);
+ $this->clearEnvironmentVariableCaches();
$this->dispatch('success', 'Environment variable added.');
}
@@ -456,12 +458,7 @@ private function updateOrCreateVariables($isPreview, $variables)
public function refreshEnvs()
{
$this->resource->refresh();
- // Clear computed property cache to force refresh
- unset($this->environmentVariables);
- unset($this->environmentVariablesPreview);
- unset($this->hardcodedEnvironmentVariables);
- unset($this->hardcodedEnvironmentVariablesPreview);
- unset($this->hasEnvironmentVariables);
+ $this->clearEnvironmentVariableCaches();
$this->getDevView();
}
}
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 f49765286..991327265 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -70,23 +70,27 @@ class="absolute inset-0 w-4 h-4 text-coollabs dark:text-warning animate-spin" fi
@if ($this->isSearchActive && ! $this->hasEnvironmentVariables)
No environment variables found.
@else
-
-
Production Environment Variables
-
Environment (secrets) variables for Production.
-
- @forelse ($this->environmentVariables as $env)
-
- @empty
- No environment variables found.
- @endforelse
- @if (($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') && $this->hardcodedEnvironmentVariables->isNotEmpty())
- @foreach ($this->hardcodedEnvironmentVariables as $index => $env)
-
+ @if ($this->environmentVariables->isNotEmpty() || $this->hardcodedEnvironmentVariables->isNotEmpty())
+
+
Production Environment Variables
+
Environment (secrets) variables for Production.
+
+ @foreach ($this->environmentVariables as $env)
+
@endforeach
+ @if (($resource->type() === 'service' || $resource?->build_pack === 'dockercompose') && $this->hardcodedEnvironmentVariables->isNotEmpty())
+ @foreach ($this->hardcodedEnvironmentVariables as $index => $env)
+
+ @endforeach
+ @endif
@endif
- @if ($resource->type() === 'application' && $resource->environment_variables_preview->count() > 0 && $showPreview)
+ @if (
+ $resource->type() === 'application' &&
+ $showPreview &&
+ ($this->environmentVariablesPreview->isNotEmpty() || $this->hardcodedEnvironmentVariablesPreview->isNotEmpty())
+ )
Preview Deployments Environment Variables
Environment (secrets) variables for Preview Deployments.
diff --git a/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php b/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php
index 632f6f943..de97e6095 100644
--- a/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php
+++ b/tests/Feature/EnvironmentVariableMultilineToggleViewTest.php
@@ -45,3 +45,12 @@
->toContain('
No environment variables found.
')
->toContain('@else');
});
+
+it('only renders the production section when production variables are visible', function () {
+ $view = file_get_contents(resource_path('views/livewire/project/shared/environment-variable/all.blade.php'));
+
+ expect($view)
+ ->toContain('@if ($this->environmentVariables->isNotEmpty() || $this->hardcodedEnvironmentVariables->isNotEmpty())')
+ ->not->toContain('@forelse ($this->environmentVariables as $env)')
+ ->not->toContain('@empty');
+});
diff --git a/tests/Feature/EnvironmentVariableSearchTest.php b/tests/Feature/EnvironmentVariableSearchTest.php
index 5f49776c9..613144d53 100644
--- a/tests/Feature/EnvironmentVariableSearchTest.php
+++ b/tests/Feature/EnvironmentVariableSearchTest.php
@@ -56,6 +56,44 @@
->toBe(['API_KEY']);
});
+it('treats production environment variable search 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,
+ ]);
+
+ EnvironmentVariable::create([
+ 'key' => 'PERCENT%KEY',
+ 'value' => 'percent-secret',
+ 'resourceable_type' => Application::class,
+ 'resourceable_id' => $application->id,
+ ]);
+
+ $component = Livewire::test(All::class, ['resource' => $application])
+ ->set('search', 'api_key');
+
+ expect($component->instance()->environmentVariables->pluck('key')->all())
+ ->toBe(['API_KEY']);
+
+ $component->set('search', '%KEY');
+
+ expect($component->instance()->environmentVariables->pluck('key')->all())
+ ->toBe(['PERCENT%KEY']);
+});
+
it('filters preview environment variables by key case-insensitively', function () {
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
@@ -104,6 +142,26 @@
->toBe(['API_TOKEN']);
});
+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,
+ ]);
+
+ Livewire::test(All::class, ['resource' => $service])
+ ->set('search', 'api')
+ ->assertSee('Production Environment Variables')
+ ->assertSee('API_TOKEN')
+ ->assertDontSee('No environment variables found.');
+});
+
it('keeps developer view unfiltered after searching', function () {
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
@@ -161,3 +219,33 @@
->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,
+ ]);
+
+ Livewire::test(All::class, ['resource' => $application])
+ ->set('search', 'api')
+ ->assertSee('Production Environment Variables')
+ ->assertSee('API_KEY')
+ ->assertDontSee('Preview Deployments Environment Variables')
+ ->assertDontSee('PREVIEW_TOKEN');
+});