mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-04 18:52:49 +00:00
343 lines
17 KiB
PHP
343 lines
17 KiB
PHP
<?php
|
|
|
|
use App\Livewire\SharedVariables\Canvas as SharedVariablesCanvas;
|
|
use App\Models\Application;
|
|
use App\Models\Environment;
|
|
use App\Models\EnvironmentVariable;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\Server;
|
|
use App\Models\SharedEnvironmentVariable;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Arr;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config([
|
|
'cache.default' => 'array',
|
|
'session.driver' => 'array',
|
|
'queue.default' => 'sync',
|
|
'app.maintenance.driver' => 'file',
|
|
]);
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true]));
|
|
Server::flushIdentityMap();
|
|
|
|
$this->team = Team::factory()->create(['show_boarding' => false]);
|
|
$this->admin = User::factory()->create();
|
|
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
|
|
|
|
$this->server = Server::factory()->create(['team_id' => $this->team->id, 'name' => 'web-server']);
|
|
$this->project = Project::factory()->create(['team_id' => $this->team->id, 'name' => 'Shop']);
|
|
$this->environment = Environment::factory()->create(['project_id' => $this->project->id, 'name' => 'staging']);
|
|
$this->storefront = sharedVariableCanvasApp($this->environment, $this->server, 'storefront');
|
|
$this->token = SharedEnvironmentVariable::create([
|
|
'key' => 'API_TOKEN', 'value' => 'secret-value', 'type' => 'team', 'team_id' => $this->team->id,
|
|
]);
|
|
});
|
|
|
|
function sharedVariableCanvasApp(Environment $environment, Server $server, string $name): Application
|
|
{
|
|
$destination = StandaloneDocker::query()->where('server_id', $server->id)->firstOrFail();
|
|
|
|
return Application::factory()->create([
|
|
'name' => $name,
|
|
'build_pack' => 'dockerimage',
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
}
|
|
|
|
function sharedVariableCanvasEnv(Application $application, string $key, string $value): EnvironmentVariable
|
|
{
|
|
return EnvironmentVariable::create([
|
|
'key' => $key, 'value' => $value,
|
|
'resourceable_type' => $application->getMorphClass(), 'resourceable_id' => $application->id,
|
|
]);
|
|
}
|
|
|
|
function sharedVariableCanvasEnvs(Application $application)
|
|
{
|
|
return EnvironmentVariable::query()->where('resourceable_type', $application->getMorphClass())->where('resourceable_id', $application->id);
|
|
}
|
|
|
|
function sharedVariableCanvasAs(object $test, User $user, Team $team): object
|
|
{
|
|
return $test->actingAs($user)->withSession(['currentTeam' => $team])
|
|
->withHeader('X-Canvas-Team', (string) $team->id);
|
|
}
|
|
|
|
test('the team canvas shows the variables, the resources and which resources use them, without values', function () {
|
|
sharedVariableCanvasEnv($this->storefront, 'API_TOKEN', '{{team.API_TOKEN}}');
|
|
$worker = sharedVariableCanvasApp($this->environment, $this->server, 'worker');
|
|
sharedVariableCanvasEnv($worker, 'DATABASE_URL', 'postgres://{{ team.API_TOKEN }}@db');
|
|
sharedVariableCanvasEnv($worker, 'OLD', '{{team.REMOVED_KEY}}');
|
|
sharedVariableCanvasEnv($worker, 'PROJECT_ONLY', '{{project.API_TOKEN}}');
|
|
|
|
$response = sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->getJson('/canvas/api/shared-variables/team/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('layout', 'connect')
|
|
->assertJsonPath('can_edit', true)
|
|
->assertJsonPath('title_link', route('shared-variables.team.index'));
|
|
|
|
$variableId = 'variable:'.$this->token->id;
|
|
expect($response->getContent())->not->toContain('secret-value')
|
|
->and(collect($response->json('nodes'))->mapWithKeys(fn ($node) => [$node['id'] => [$node['name'], $node['zone']]])->all())->toBe([
|
|
$variableId => ['API_TOKEN', 'sources'],
|
|
'missing:REMOVED_KEY' => ['REMOVED_KEY', 'sources'],
|
|
$this->storefront->uuid => ['storefront', 'resources'],
|
|
$worker->uuid => ['worker', 'resources'],
|
|
])
|
|
->and(collect($response->json('links'))->mapWithKeys(fn ($link) => [$link['id'] => Arr::only($link, ['label', 'removable', 'missing'])])->all())->toBe([
|
|
"{$variableId}|{$this->storefront->uuid}" => ['label' => null, 'removable' => true, 'missing' => false],
|
|
"{$variableId}|{$worker->uuid}" => ['label' => 'DATABASE_URL', 'removable' => false, 'missing' => false],
|
|
"missing:REMOVED_KEY|{$worker->uuid}" => ['label' => 'OLD', 'removable' => true, 'missing' => true],
|
|
]);
|
|
|
|
// Resources carry their project and environment for the canvas filter.
|
|
$storefront = collect($response->json('nodes'))->firstWhere('id', $this->storefront->uuid);
|
|
expect($storefront['project'])->toBe(['uuid' => $this->project->uuid, 'name' => 'Shop'])
|
|
->and($storefront['environment'])->toBe(['uuid' => $this->environment->uuid, 'name' => 'staging']);
|
|
});
|
|
|
|
test('an admin adds a shared variable to a resource from the canvas', function () {
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson('/canvas/api/shared-variables/team/canvas/links', [
|
|
'source' => 'variable:'.$this->token->id,
|
|
'target' => $this->storefront->uuid,
|
|
])
|
|
->assertCreated()
|
|
->assertJsonPath('id', 'variable:'.$this->token->id.'|'.$this->storefront->uuid)
|
|
->assertJsonPath('keys', ['API_TOKEN']);
|
|
|
|
$variables = sharedVariableCanvasEnvs($this->storefront)->get();
|
|
expect($variables)->toHaveCount(2)
|
|
->and($variables->pluck('value')->unique()->all())->toBe(['{{team.API_TOKEN}}'])
|
|
->and($variables->pluck('is_preview')->sort()->values()->all())->toBe([false, true])
|
|
->and($variables->firstWhere('is_preview', false)->real_value)->toBe('secret-value');
|
|
|
|
// Adding it again, or over a variable with the same name, is refused.
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson('/canvas/api/shared-variables/team/canvas/links', ['source' => 'variable:'.$this->token->id, 'target' => $this->storefront->uuid])
|
|
->assertStatus(422)
|
|
->assertJsonPath('message', 'storefront already uses API_TOKEN.');
|
|
$other = sharedVariableCanvasApp($this->environment, $this->server, 'other');
|
|
sharedVariableCanvasEnv($other, 'API_TOKEN', 'local-value');
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson('/canvas/api/shared-variables/team/canvas/links', ['source' => 'variable:'.$this->token->id, 'target' => $other->uuid])
|
|
->assertStatus(422)
|
|
->assertJsonPath('message', 'other already has a variable named API_TOKEN. Rename or remove it first.');
|
|
});
|
|
|
|
test('removing a link deletes the reference and its preview copy, but not a longer value', function () {
|
|
sharedVariableCanvasEnv($this->storefront, 'API_TOKEN', '{{team.API_TOKEN}}');
|
|
$linkId = 'variable:'.$this->token->id.'|'.$this->storefront->uuid;
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->count())->toBe(2);
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->deleteJson('/canvas/api/shared-variables/team/canvas/links/'.rawurlencode($linkId))
|
|
->assertOk();
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->count())->toBe(0);
|
|
|
|
sharedVariableCanvasEnv($this->storefront, 'DATABASE_URL', 'postgres://{{team.API_TOKEN}}@db');
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->deleteJson('/canvas/api/shared-variables/team/canvas/links/'.rawurlencode($linkId))
|
|
->assertStatus(422);
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->where('is_preview', false)->pluck('key')->all())->toBe(['DATABASE_URL']);
|
|
});
|
|
|
|
test('a reference to a deleted shared variable can be removed', function () {
|
|
sharedVariableCanvasEnv($this->storefront, 'OLD', '{{team.REMOVED_KEY}}');
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->deleteJson('/canvas/api/shared-variables/team/canvas/links/'.rawurlencode('missing:REMOVED_KEY|'.$this->storefront->uuid))
|
|
->assertOk();
|
|
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->exists())->toBeFalse();
|
|
});
|
|
|
|
test('a member sees the canvas but cannot add or remove shared variables', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
sharedVariableCanvasEnv($this->storefront, 'API_TOKEN', '{{team.API_TOKEN}}');
|
|
|
|
sharedVariableCanvasAs($this, $member, $this->team)
|
|
->getJson('/canvas/api/shared-variables/team/canvas')
|
|
->assertOk()
|
|
->assertJsonPath('can_edit', false);
|
|
sharedVariableCanvasAs($this, $member, $this->team)
|
|
->postJson('/canvas/api/shared-variables/team/canvas/links', ['source' => 'variable:'.$this->token->id, 'target' => $this->storefront->uuid])
|
|
->assertForbidden();
|
|
sharedVariableCanvasAs($this, $member, $this->team)
|
|
->deleteJson('/canvas/api/shared-variables/team/canvas/links/'.rawurlencode('variable:'.$this->token->id.'|'.$this->storefront->uuid))
|
|
->assertForbidden();
|
|
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->count())->toBe(2);
|
|
});
|
|
|
|
test('a project canvas only links project variables to resources of that project', function () {
|
|
$projectVariable = SharedEnvironmentVariable::create([
|
|
'key' => 'SHOP_NAME', 'value' => 'shop', 'type' => 'project', 'team_id' => $this->team->id, 'project_id' => $this->project->id,
|
|
]);
|
|
$otherProject = Project::factory()->create(['team_id' => $this->team->id]);
|
|
$outside = sharedVariableCanvasApp(Environment::factory()->create(['project_id' => $otherProject->id]), $this->server, 'outside');
|
|
$base = "/canvas/api/shared-variables/project/{$this->project->uuid}/canvas";
|
|
|
|
$response = sharedVariableCanvasAs($this, $this->admin, $this->team)->getJson($base)->assertOk();
|
|
expect(collect($response->json('nodes'))->pluck('name')->all())->toBe(['SHOP_NAME', 'storefront']);
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson("{$base}/links", ['source' => 'variable:'.$projectVariable->id, 'target' => $outside->uuid])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('target');
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson("{$base}/links", ['source' => 'variable:'.$this->token->id, 'target' => $this->storefront->uuid])
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('source');
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->postJson("{$base}/links", ['source' => 'variable:'.$projectVariable->id, 'target' => $this->storefront->uuid])
|
|
->assertCreated();
|
|
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->where('is_preview', false)->first()->value)->toBe('{{project.SHOP_NAME}}');
|
|
});
|
|
|
|
test('a server canvas only shows resources on that server', function () {
|
|
$other = Server::factory()->create(['team_id' => $this->team->id]);
|
|
sharedVariableCanvasApp($this->environment, $other, 'elsewhere');
|
|
|
|
$response = sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->getJson("/canvas/api/shared-variables/server/{$this->server->uuid}/canvas")
|
|
->assertOk();
|
|
|
|
expect(collect($response->json('nodes'))->where('type', '!=', 'variable')->pluck('name')->all())->toBe(['storefront']);
|
|
});
|
|
|
|
test('another team cannot open or change the canvas of a scope it does not own', function () {
|
|
$otherTeam = Team::factory()->create(['show_boarding' => false]);
|
|
$otherTeam->members()->attach($this->admin->id, ['role' => 'owner']);
|
|
|
|
// The session is switched to the other team after the canvas was opened.
|
|
sharedVariableCanvasAs($this, $this->admin, $otherTeam)
|
|
->getJson("/canvas/api/shared-variables/project/{$this->project->uuid}/canvas")
|
|
->assertNotFound();
|
|
sharedVariableCanvasAs($this, $this->admin, $otherTeam)
|
|
->postJson('/canvas/api/shared-variables/team/canvas/links', ['source' => 'variable:'.$this->token->id, 'target' => $this->storefront->uuid])
|
|
->assertStatus(422);
|
|
sharedVariableCanvasAs($this, $this->admin, $otherTeam)
|
|
->get(route('shared-variables.project.canvas', ['project_uuid' => $this->project->uuid]))
|
|
->assertNotFound();
|
|
|
|
expect(sharedVariableCanvasEnvs($this->storefront)->exists())->toBeFalse();
|
|
});
|
|
|
|
test('the canvas page opens for every scope', function () {
|
|
$routes = [
|
|
route('shared-variables.team.canvas'),
|
|
route('shared-variables.project.canvas', ['project_uuid' => $this->project->uuid]),
|
|
route('shared-variables.environment.canvas', ['project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid]),
|
|
route('shared-variables.server.canvas', ['server_uuid' => $this->server->uuid]),
|
|
];
|
|
$endpoints = [
|
|
'/shared-variables/team/canvas',
|
|
"/shared-variables/project/{$this->project->uuid}/canvas",
|
|
"/shared-variables/environment/{$this->environment->uuid}/canvas",
|
|
"/shared-variables/server/{$this->server->uuid}/canvas",
|
|
];
|
|
|
|
foreach ($routes as $i => $url) {
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)->get($url)
|
|
->assertOk()
|
|
->assertSee('data-endpoint="'.$endpoints[$i].'"', false);
|
|
}
|
|
});
|
|
|
|
test('every shared variable row links to the connect envs canvas with the variable selected', function () {
|
|
$projectVariable = SharedEnvironmentVariable::create([
|
|
'key' => 'SHOP_NAME', 'value' => 'shop', 'type' => 'project', 'team_id' => $this->team->id, 'project_id' => $this->project->id,
|
|
]);
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)->get(route('shared-variables.team.index'))
|
|
->assertOk()
|
|
->assertSee(route('shared-variables.team.canvas').'?select=variable%3A'.$this->token->id, false);
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)->get(route('shared-variables.project.show', ['project_uuid' => $this->project->uuid]))
|
|
->assertOk()
|
|
->assertSee(route('shared-variables.project.canvas', ['project_uuid' => $this->project->uuid]).'?select=variable%3A'.$projectVariable->id, false);
|
|
});
|
|
|
|
test('each shared variables page reopens its canvas when the user left it on the canvas view', function (string $scope) {
|
|
$parameters = match ($scope) {
|
|
'team' => [],
|
|
'project' => ['project_uuid' => $this->project->uuid],
|
|
'environment' => ['project_uuid' => $this->project->uuid, 'environment_uuid' => $this->environment->uuid],
|
|
'server' => ['server_uuid' => $this->server->uuid],
|
|
};
|
|
$listRoute = $scope === 'team' ? 'shared-variables.team.index' : "shared-variables.{$scope}.show";
|
|
$cookie = "coolify_{$this->admin->id}_{$scope}_variables_view";
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->get(route("shared-variables.{$scope}.canvas", $parameters))
|
|
->assertOk()
|
|
->assertCookie($cookie, 'canvas');
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->withCookie($cookie, 'canvas')
|
|
->get(route($listRoute, $parameters))
|
|
->assertRedirect(route("shared-variables.{$scope}.canvas", $parameters));
|
|
|
|
sharedVariableCanvasAs($this, $this->admin, $this->team)
|
|
->withCookie($cookie, 'canvas')
|
|
->get(route($listRoute, [...$parameters, 'view' => 'list']))
|
|
->assertOk()
|
|
->assertCookie($cookie, 'list');
|
|
})->with(['team', 'project', 'environment', 'server']);
|
|
|
|
test('an admin adds a shared variable from the canvas modal and the canvas reloads', function () {
|
|
$this->actingAs($this->admin);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(SharedVariablesCanvas::class, ['project_uuid' => $this->project->uuid])
|
|
->dispatch('saveKey', ['key' => 'SHOP_URL', 'value' => 'https://shop.test', 'is_multiline' => false, 'is_literal' => false])
|
|
->assertRedirect(route('shared-variables.project.canvas', ['project_uuid' => $this->project->uuid]));
|
|
|
|
$variable = SharedEnvironmentVariable::query()->where('key', 'SHOP_URL')->firstOrFail();
|
|
expect($variable->type)->toBe('project')
|
|
->and($variable->project_id)->toBe($this->project->id)
|
|
->and($variable->team_id)->toBe($this->team->id);
|
|
});
|
|
|
|
test('a member cannot add a shared variable from the canvas, also after switching to an owned team', function () {
|
|
$member = User::factory()->create();
|
|
$this->team->members()->attach($member->id, ['role' => 'member']);
|
|
$ownTeam = Team::factory()->create();
|
|
$member->teams()->attach($ownTeam, ['role' => 'owner']);
|
|
$this->actingAs($member);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
$component = Livewire::test(SharedVariablesCanvas::class);
|
|
$member->load('teams');
|
|
session(['currentTeam' => $ownTeam]);
|
|
|
|
$component
|
|
->dispatch('saveKey', ['key' => 'SHOP_URL', 'value' => 'x', 'is_multiline' => false, 'is_literal' => false])
|
|
->assertDispatched('error');
|
|
|
|
expect(SharedEnvironmentVariable::query()->where('key', 'SHOP_URL')->exists())->toBeFalse();
|
|
});
|
|
|
|
test('the server canvas rejects the predefined server variables', function () {
|
|
$this->actingAs($this->admin);
|
|
session(['currentTeam' => $this->team]);
|
|
|
|
Livewire::test(SharedVariablesCanvas::class, ['server_uuid' => $this->server->uuid])
|
|
->dispatch('saveKey', ['key' => 'COOLIFY_SERVER_NAME', 'value' => 'x', 'is_multiline' => false, 'is_literal' => false])
|
|
->assertDispatched('error');
|
|
|
|
expect($this->server->environment_variables()->where('key', 'COOLIFY_SERVER_NAME')->where('value', 'x')->exists())->toBeFalse();
|
|
});
|