Files
coolify/tests/Feature/EnvironmentVariableCopyValueTest.php
T
Andras Bacsai 680f4e5d29 fix: use GitLab API v4 for OAuth and gate env copy by update policy
Socialite's GitLab provider fetches the user from /api/v3/user, which
GitLab removed in 11.0 (410 Gone). Add App\Auth\GitlabProvider that
reads /api/v4/user with a bearer token and register it in the
socialite helper, for both gitlab.com and self-hosted base URLs.

Environment variable copyValue now checks the 'update' policy on the
variable instead of the member role of the current team, so members
can no longer copy values after switching to a team they own. Remove
the redundant member check from ShowHardcoded.

Add tests for the GitLab v4 user lookup and the team-switch copy case.
2026-09-29 22:36:56 +02:00

213 lines
8.4 KiB
PHP

<?php
use App\Livewire\Project\Shared\EnvironmentVariable\Show;
use App\Livewire\Project\Shared\EnvironmentVariable\ShowHardcoded;
use App\Models\Application;
use App\Models\Environment;
use App\Models\EnvironmentVariable;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Service;
use App\Models\SharedEnvironmentVariable;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Features\SupportLockedProperties\CannotUpdateLockedPropertyException;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
InstanceSettings::forceCreate(['id' => 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->application = Application::factory()->create(['environment_id' => $this->environment->id]);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
});
function createEnvironmentVariable(array $attributes = []): EnvironmentVariable
{
return EnvironmentVariable::create(array_merge([
'key' => 'API_KEY',
'value' => 'secret-value',
'resourceable_type' => Application::class,
'resourceable_id' => test()->application->id,
], $attributes));
}
function assertCopiedValue(EnvironmentVariable|SharedEnvironmentVariable $env, ?string $expected): void
{
Livewire::test(Show::class, ['env' => $env, 'type' => 'application'])
->call('copyValue')
->assertReturned($expected);
}
function assertCopiedComposeValue(string $value, ?string $expected): void
{
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'MYSQL_USER', 'value' => $value],
'resourceableType' => Application::class,
'resourceableId' => test()->application->id,
])
->call('copyValue')
->assertReturned($expected);
}
test('copies the plain value', function () {
assertCopiedValue(createEnvironmentVariable(), 'secret-value');
});
test('copies the referenced variable value instead of the reference', function (string $reference) {
createEnvironmentVariable(['key' => 'SERVICE_USER_CLASSICPRESS', 'value' => 'classicpress-user']);
assertCopiedValue(createEnvironmentVariable(['key' => 'MYSQL_USER', 'value' => $reference]), 'classicpress-user');
})->with(['bare' => '$SERVICE_USER_CLASSICPRESS', 'braced' => '${SERVICE_USER_CLASSICPRESS}']);
test('copies the resolved shared variable value', function () {
SharedEnvironmentVariable::create([
'key' => 'MY_SECRET',
'value' => 'resolved-secret',
'type' => 'team',
'team_id' => $this->team->id,
]);
assertCopiedValue(createEnvironmentVariable(['value' => '{{team.MY_SECRET}}']), 'resolved-secret');
});
test('copies embedded, literal and unknown references as stored', function () {
createEnvironmentVariable(['key' => 'SERVICE_PASSWORD_MYSQL', 'value' => 'generated-password']);
assertCopiedValue(
createEnvironmentVariable(['key' => 'DATABASE_URL', 'value' => 'mysql://root:$SERVICE_PASSWORD_MYSQL@db:3306']),
'mysql://root:$SERVICE_PASSWORD_MYSQL@db:3306',
);
assertCopiedValue(
createEnvironmentVariable(['key' => 'LITERAL', 'value' => '$SERVICE_PASSWORD_MYSQL', 'is_literal' => true]),
'$SERVICE_PASSWORD_MYSQL',
);
assertCopiedValue(createEnvironmentVariable(['key' => 'UNKNOWN', 'value' => '$DOES_NOT_EXIST']), '$DOES_NOT_EXIST');
});
test('copies literal values without .env-style quoting', function () {
$env = createEnvironmentVariable(['value' => 'pa$$word', 'is_literal' => true]);
expect($env->real_value)->toBe("'pa\$\$word'");
assertCopiedValue($env, 'pa$$word');
});
test('copies the value of a shared environment variable row', function () {
$shared = SharedEnvironmentVariable::create([
'key' => 'TEAM_WIDE',
'value' => 'team-wide-value',
'type' => 'team',
'team_id' => $this->team->id,
]);
assertCopiedValue($shared, 'team-wide-value');
});
test('members get no copy button and no value', function () {
$member = User::factory()->create();
$this->team->members()->attach($member, ['role' => 'member']);
$this->actingAs($member);
Livewire::test(Show::class, ['env' => createEnvironmentVariable(), 'type' => 'application'])
->assertDontSeeHtml('Copy value')
->call('copyValue')
->assertReturned(null);
});
test('members get no value after switching to a team they own', function (string $variableType) {
$member = User::factory()->create();
$this->team->members()->attach($member, ['role' => 'member']);
$ownedTeam = Team::factory()->create();
$ownedTeam->members()->attach($member, ['role' => 'owner']);
$this->actingAs($member);
$env = $variableType === 'shared'
? SharedEnvironmentVariable::create(['key' => 'TEAM_WIDE', 'value' => 'team-wide-secret', 'type' => 'team', 'team_id' => $this->team->id])
: createEnvironmentVariable();
$component = Livewire::test(Show::class, ['env' => $env, 'type' => 'application']);
session(['currentTeam' => $ownedTeam]);
$component->call('copyValue')->assertReturned(null);
})->with(['application variable' => 'application', 'shared variable' => 'shared']);
test('locked variables get no copy button and no value', function () {
Livewire::test(Show::class, ['env' => createEnvironmentVariable(['is_shown_once' => true]), 'type' => 'application'])
->assertDontSeeHtml('Copy value')
->call('copyValue')
->assertReturned(null);
});
test('compose-managed rows copy the referenced variable value', function () {
createEnvironmentVariable(['key' => 'SERVICE_USER_CLASSICPRESS', 'value' => 'classicpress-user']);
assertCopiedComposeValue('$SERVICE_USER_CLASSICPRESS', 'classicpress-user');
assertCopiedComposeValue('production', 'production');
});
test('copying a reference does not reveal a locked variable', function () {
createEnvironmentVariable(['key' => 'SERVICE_PASSWORD_MYSQL', 'value' => 'locked-password', 'is_shown_once' => true]);
assertCopiedValue(
createEnvironmentVariable(['key' => 'DB_PASSWORD', 'value' => '$SERVICE_PASSWORD_MYSQL']),
'$SERVICE_PASSWORD_MYSQL',
);
assertCopiedComposeValue('$SERVICE_PASSWORD_MYSQL', '$SERVICE_PASSWORD_MYSQL');
});
test('compose-managed rows do not accept a changed lookup from the client', function (string $property, mixed $value) {
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'MYSQL_USER', 'value' => 'production'],
'resourceableType' => Application::class,
'resourceableId' => test()->application->id,
])->set($property, $value);
})->with([
'value' => ['value', '$DATABASE_PASSWORD'],
'preview scope' => ['isPreview', true],
'resource type' => ['resourceableType', Service::class],
'resource id' => ['resourceableId', 999],
])->throws(CannotUpdateLockedPropertyException::class);
test('compose-managed rows do not copy values from another team', function () {
$otherTeam = Team::factory()->create();
$otherProject = Project::factory()->create(['team_id' => $otherTeam->id]);
$otherEnvironment = Environment::factory()->create(['project_id' => $otherProject->id]);
$otherApplication = Application::factory()->create(['environment_id' => $otherEnvironment->id]);
createEnvironmentVariable([
'key' => 'DATABASE_PASSWORD',
'value' => 'other-team-secret',
'resourceable_id' => $otherApplication->id,
]);
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'DB_PASSWORD', 'value' => '$DATABASE_PASSWORD'],
'resourceableType' => Application::class,
'resourceableId' => $otherApplication->id,
])
->call('copyValue')
->assertReturned(null);
});
test('compose-managed rows hide copying from members', function () {
$member = User::factory()->create();
$this->team->members()->attach($member, ['role' => 'member']);
$this->actingAs($member);
Livewire::test(ShowHardcoded::class, [
'env' => ['key' => 'MYSQL_USER', 'value' => '$SERVICE_USER_CLASSICPRESS'],
'resourceableType' => Application::class,
'resourceableId' => $this->application->id,
])
->assertDontSeeHtml('Copy value')
->call('copyValue')
->assertReturned(null);
});