mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Scope resource permissions and related records to the owning team across team switches, cloning, deployments, and resource operations. Merge ClickHouse volume mounts, preserve anonymous data volumes on stop, and require adopting current data before starting or restarting. Fix file storage deletion checks and add regression coverage.
199 lines
7.8 KiB
PHP
199 lines
7.8 KiB
PHP
<?php
|
|
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
use App\Livewire\Project\Database\BackupEdit;
|
|
use App\Livewire\Project\Service\BackupExecutions;
|
|
use App\Livewire\Project\Service\VolumeBackup\Index as ServiceVolumeBackupIndex;
|
|
use App\Livewire\Project\Shared\ScheduledTask\Add as AddScheduledTask;
|
|
use App\Livewire\Project\Shared\Tags;
|
|
use App\Models\Application;
|
|
use App\Models\ApplicationDeploymentQueue;
|
|
use App\Models\Environment;
|
|
use App\Models\InstanceSettings;
|
|
use App\Models\Project;
|
|
use App\Models\S3Storage;
|
|
use App\Models\ScheduledDatabaseBackup;
|
|
use App\Models\ScheduledDatabaseBackupExecution;
|
|
use App\Models\ScheduledTask;
|
|
use App\Models\Server;
|
|
use App\Models\Service;
|
|
use App\Models\ServiceDatabase;
|
|
use App\Models\StandaloneDocker;
|
|
use App\Models\Tag;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Str;
|
|
use Livewire\Livewire;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
config(['app.maintenance.driver' => 'file']);
|
|
Server::flushIdentityMap();
|
|
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0], ['is_api_enabled' => true]));
|
|
|
|
$this->teamA = Team::factory()->create();
|
|
$this->teamB = Team::factory()->create();
|
|
$this->user = User::factory()->create();
|
|
$this->user->teams()->attach($this->teamA, ['role' => 'owner']);
|
|
$this->user->teams()->attach($this->teamB, ['role' => 'owner']);
|
|
|
|
$server = Server::factory()->create(['team_id' => $this->teamA->id]);
|
|
$destination = StandaloneDocker::where('server_id', $server->id)->firstOrFail();
|
|
$project = Project::factory()->create(['team_id' => $this->teamA->id]);
|
|
$environment = Environment::factory()->create(['project_id' => $project->id]);
|
|
$this->application = Application::factory()->create([
|
|
'environment_id' => $environment->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
]);
|
|
$this->service = Service::factory()->create([
|
|
'server_id' => $server->id,
|
|
'destination_id' => $destination->id,
|
|
'destination_type' => $destination->getMorphClass(),
|
|
'environment_id' => $environment->id,
|
|
]);
|
|
$this->serviceDatabase = ServiceDatabase::create([
|
|
'service_id' => $this->service->id,
|
|
'name' => 'team-switch-db',
|
|
'image' => 'postgres:16-alpine',
|
|
'custom_type' => 'postgresql',
|
|
]);
|
|
|
|
$this->actingAs($this->user);
|
|
session(['currentTeam' => $this->teamA]);
|
|
});
|
|
|
|
function createS3ForTeamSwitchTest(Team $team, string $name): S3Storage
|
|
{
|
|
return S3Storage::create([
|
|
'name' => $name,
|
|
'region' => 'us-east-1',
|
|
'key' => 'key',
|
|
'secret' => 'secret',
|
|
'bucket' => $name.'-bucket',
|
|
'endpoint' => 'https://s3.example.com',
|
|
'is_usable' => true,
|
|
'team_id' => $team->id,
|
|
]);
|
|
}
|
|
|
|
test('tags of the session team are not listed or attached after a team switch', function () {
|
|
$foreignTag = Tag::create(['name' => 'production', 'team_id' => $this->teamB->id]);
|
|
$component = Livewire::test(Tags::class, ['resource' => $this->application]);
|
|
session(['currentTeam' => $this->teamB]);
|
|
|
|
$component->call('loadTags')
|
|
->assertSet('tags', fn ($tags) => $tags->isEmpty())
|
|
->set('newTags', 'production')
|
|
->call('submit');
|
|
Livewire::test(Tags::class, ['resource' => $this->application])
|
|
->call('addTag', (string) $foreignTag->id)
|
|
->assertNotFound();
|
|
|
|
$attached = $this->application->tags()->get();
|
|
expect($attached)->toHaveCount(1)
|
|
->and($attached->first()->id)->not->toBe($foreignTag->id)
|
|
->and($attached->first()->team_id)->toBe($this->teamA->id);
|
|
});
|
|
|
|
test('deploy by tag skips resources of another team', function () {
|
|
Queue::fake();
|
|
$tag = Tag::create(['name' => 'production', 'team_id' => $this->teamB->id]);
|
|
$this->application->tags()->attach($tag->id);
|
|
|
|
$plainTextToken = Str::random(40);
|
|
$token = $this->user->tokens()->create([
|
|
'name' => 'team-b-deploy',
|
|
'token' => hash('sha256', $plainTextToken),
|
|
'abilities' => ['deploy'],
|
|
'team_id' => $this->teamB->id,
|
|
]);
|
|
$this->app['auth']->forgetGuards();
|
|
|
|
$this->withHeaders(['Authorization' => 'Bearer '.$token->getKey().'|'.$plainTextToken])
|
|
->postJson('/api/v1/deploy', ['tag' => 'production'])
|
|
->assertOk()
|
|
->assertJsonMissingPath('details');
|
|
|
|
expect(ApplicationDeploymentQueue::count())->toBe(0);
|
|
Queue::assertNotPushed(ApplicationDeploymentJob::class);
|
|
});
|
|
|
|
test('scheduled task belongs to the resource team after a team switch', function () {
|
|
$component = Livewire::test(AddScheduledTask::class, [
|
|
'id' => (string) $this->application->id,
|
|
'type' => 'application',
|
|
'containerNames' => collect(),
|
|
]);
|
|
session(['currentTeam' => $this->teamB]);
|
|
|
|
$component->set('name', 'team-switch-task')
|
|
->set('command', 'echo ok')
|
|
->set('frequency', '0 0 * * *')
|
|
->call('submit')
|
|
->assertHasNoErrors();
|
|
|
|
expect(ScheduledTask::query()->sole()->team_id)->toBe($this->teamA->id);
|
|
});
|
|
|
|
test('service backup schedule offers only S3 storages of the service team', function () {
|
|
$ownS3 = createS3ForTeamSwitchTest($this->teamA, 'team-a-s3');
|
|
createS3ForTeamSwitchTest($this->teamB, 'team-b-s3');
|
|
$backup = ScheduledDatabaseBackup::create([
|
|
'team_id' => $this->teamA->id,
|
|
'frequency' => 'daily',
|
|
'save_s3' => true,
|
|
's3_storage_id' => $ownS3->id,
|
|
'database_id' => $this->serviceDatabase->id,
|
|
'database_type' => $this->serviceDatabase->getMorphClass(),
|
|
]);
|
|
$component = Livewire::test(ServiceVolumeBackupIndex::class, ['service' => $this->service]);
|
|
session(['currentTeam' => $this->teamB]);
|
|
|
|
$component->call('openSchedule', $backup->uuid)
|
|
->assertSet('s3s', fn ($s3s) => $s3s->pluck('id')->all() === [$ownS3->id])
|
|
->assertSee('S3 storage: team-a-s3 (bucket: team-a-s3-bucket)');
|
|
});
|
|
|
|
test('backup edit keeps the S3 storage of the backup team when given a foreign list', function () {
|
|
$ownS3 = createS3ForTeamSwitchTest($this->teamA, 'team-a-s3');
|
|
$foreignS3 = createS3ForTeamSwitchTest($this->teamB, 'team-b-s3');
|
|
$backup = ScheduledDatabaseBackup::create([
|
|
'team_id' => $this->teamA->id,
|
|
'frequency' => '0 0 * * *',
|
|
'save_s3' => true,
|
|
's3_storage_id' => $ownS3->id,
|
|
'database_id' => $this->serviceDatabase->id,
|
|
'database_type' => $this->serviceDatabase->getMorphClass(),
|
|
]);
|
|
|
|
$component = Livewire::test(BackupEdit::class, ['backup' => $backup->fresh(), 'availableS3Storages' => $this->teamB->s3s])
|
|
->call('instantSave');
|
|
expect($backup->fresh()->s3_storage_id)->toBe($ownS3->id)
|
|
->and($backup->fresh()->save_s3)->toBeTruthy();
|
|
|
|
$component->set('s3StorageId', $foreignS3->id)->call('instantSave');
|
|
expect($backup->fresh()->s3_storage_id)->toBe($ownS3->id);
|
|
});
|
|
|
|
test('service backup executions label the S3 storage of the service team after a team switch', function () {
|
|
$ownS3 = createS3ForTeamSwitchTest($this->teamA, 'team-a-s3');
|
|
$schedule = ScheduledDatabaseBackup::create([
|
|
'team_id' => $this->teamA->id,
|
|
'frequency' => 'daily',
|
|
'save_s3' => true,
|
|
's3_storage_id' => $ownS3->id,
|
|
'database_id' => $this->serviceDatabase->id,
|
|
'database_type' => $this->serviceDatabase->getMorphClass(),
|
|
]);
|
|
ScheduledDatabaseBackupExecution::create(['scheduled_database_backup_id' => $schedule->id, 'status' => 'success']);
|
|
$component = Livewire::test(BackupExecutions::class, ['service' => $this->service]);
|
|
session(['currentTeam' => $this->teamB]);
|
|
|
|
$component->call('$refresh')
|
|
->assertViewHas('executions', fn ($executions) => str_contains($executions->first()['s3_tooltip'], 'team-a-s3'));
|
|
});
|