coolify/tests/Feature/TeamScopedDestinationTest.php
Andras Bacsai a478ac66eb refactor: scope destination and resource lookups by current team
Use find_destination_for_current_team helper across resource creation
flows and the destination controller. Pass full destination objects to
database creation helpers instead of UUIDs so team relationships are
resolved consistently before the resource is created or linked.

Add feature tests covering destination, backup storage, and resource
proof lookups across teams.
2026-04-19 11:55:12 +02:00

297 lines
12 KiB
PHP

<?php
use App\Livewire\Destination\Show as DestinationShow;
use App\Livewire\Project\New\DockerCompose;
use App\Livewire\Project\New\DockerImage;
use App\Livewire\Project\New\GithubPrivateRepository;
use App\Livewire\Project\New\GithubPrivateRepositoryDeployKey;
use App\Livewire\Project\New\PublicGitRepository;
use App\Livewire\Project\New\SimpleDockerfile;
use App\Models\Application;
use App\Models\Environment;
use App\Models\InstanceSettings;
use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneDocker;
use App\Models\StandalonePostgresql;
use App\Models\SwarmDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Queue;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
Queue::fake();
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
$this->userA = User::factory()->create();
$this->teamA = Team::factory()->create();
$this->userA->teams()->attach($this->teamA, ['role' => 'owner']);
$this->serverA = Server::factory()->create(['team_id' => $this->teamA->id]);
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
$this->destinationA = StandaloneDocker::factory()->create([
'server_id' => $this->serverA->id,
'name' => 'dest-a-'.fake()->unique()->word(),
'network' => 'coolify-a-'.fake()->unique()->word(),
]);
$this->userB = User::factory()->create();
$this->teamB = Team::factory()->create();
$this->userB->teams()->attach($this->teamB, ['role' => 'owner']);
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
$this->destinationB = StandaloneDocker::factory()->create([
'server_id' => $this->serverB->id,
'name' => 'dest-b-'.fake()->unique()->word(),
'network' => 'coolify-b-'.fake()->unique()->word(),
]);
$this->swarmDestinationB = SwarmDocker::create([
'uuid' => fake()->uuid(),
'name' => 'swarm-b-'.fake()->unique()->word(),
'network' => 'swarm-b-'.fake()->unique()->word(),
'server_id' => $this->serverB->id,
]);
$this->actingAs($this->userA);
session(['currentTeam' => $this->teamA]);
});
describe('find_destination_for_current_team helper', function () {
test('returns null for other team destination UUID', function () {
expect(find_destination_for_current_team($this->destinationB->uuid))->toBeNull();
});
test('returns null for other team swarm destination UUID', function () {
expect(find_destination_for_current_team($this->swarmDestinationB->uuid))->toBeNull();
});
test('returns own team destination', function () {
$found = find_destination_for_current_team($this->destinationA->uuid);
expect($found)->not->toBeNull();
expect($found->id)->toBe($this->destinationA->id);
});
test('returns null for blank uuid', function () {
expect(find_destination_for_current_team(null))->toBeNull();
expect(find_destination_for_current_team(''))->toBeNull();
});
});
describe('SimpleDockerfile destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
request()->headers->set('referer', route('project.resource.create', $routeParams).'?destination='.$this->destinationB->uuid);
$before = Application::count();
expect(fn () => Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(SimpleDockerfile::class, $routeParams)
->set('dockerfile', "FROM nginx\nCMD [\"nginx\"]\n")
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
expect(Application::count())->toBe($before);
});
});
describe('DockerImage destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
expect(fn () => Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(DockerImage::class, $routeParams)
->set('imageName', 'nginx')
->set('imageTag', 'latest')
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
expect(Application::count())->toBe($before);
});
test('submit with other team swarm destination throws', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
expect(fn () => Livewire::withUrlParams(['destination' => $this->swarmDestinationB->uuid])
->test(DockerImage::class, $routeParams)
->set('imageName', 'nginx')
->set('imageTag', 'latest')
->call('submit'))
->toThrow(Exception::class, 'Destination not found.');
});
});
describe('DockerCompose destination + server_id team scope', function () {
test('submit with other team destination throws and creates no service', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Service::count();
Livewire::withUrlParams([
'destination' => $this->destinationB->uuid,
'server_id' => $this->serverB->id,
])
->test(DockerCompose::class, $routeParams)
->set('dockerComposeRaw', "services:\n app:\n image: nginx\n")
->call('submit');
expect(Service::count())->toBe($before);
});
});
describe('PublicGitRepository destination team scope', function () {
test('submit with other team destination creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(PublicGitRepository::class, $routeParams)
->set('repository_url', 'https://github.com/coollabsio/coolify')
->set('git_repository', 'coollabsio/coolify')
->set('git_branch', 'main')
->set('port', 3000)
->set('build_pack', 'nixpacks')
->set('git_source', 'other')
->call('submit');
} catch (Throwable $e) {
// submit wraps errors via handleError; count assertion below is source of truth
}
expect(Application::count())->toBe($before);
});
});
describe('GithubPrivateRepository destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(GithubPrivateRepository::class, $routeParams)
->call('submit');
} catch (Throwable $e) {
// expected
}
expect(Application::count())->toBe($before);
});
});
describe('GithubPrivateRepositoryDeployKey destination team scope', function () {
test('submit with other team destination throws and creates no application', function () {
$routeParams = [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
];
$before = Application::count();
try {
Livewire::withUrlParams(['destination' => $this->destinationB->uuid])
->test(GithubPrivateRepositoryDeployKey::class, $routeParams)
->call('submit');
} catch (Throwable $e) {
// expected
}
expect(Application::count())->toBe($before);
});
});
describe('Resource/Create database destination team scope', function () {
test('mount with other team destination does not create database', function () {
$before = StandalonePostgresql::count();
$url = route('project.resource.create', [
'project_uuid' => $this->projectA->uuid,
'environment_uuid' => $this->environmentA->uuid,
]).'?type=postgresql&destination='.$this->destinationB->uuid.'&server_id='.$this->serverB->id.'&database_image=postgres:16-alpine';
$this->get($url);
expect(StandalonePostgresql::count())->toBe($before);
});
});
describe('StandaloneDocker/SwarmDocker ownedByCurrentTeam scope', function () {
test('StandaloneDocker::ownedByCurrentTeam excludes other team destinations', function () {
expect(StandaloneDocker::ownedByCurrentTeam()->where('uuid', $this->destinationB->uuid)->first())->toBeNull();
});
test('SwarmDocker::ownedByCurrentTeam excludes other team destinations', function () {
expect(SwarmDocker::ownedByCurrentTeam()->where('uuid', $this->swarmDestinationB->uuid)->first())->toBeNull();
});
test('StandaloneDocker::ownedByCurrentTeam returns own destination', function () {
$found = StandaloneDocker::ownedByCurrentTeam()->where('uuid', $this->destinationA->uuid)->first();
expect($found)->not->toBeNull();
expect($found->id)->toBe($this->destinationA->id);
});
test('StandaloneDocker::ownedByCurrentTeamAPI scopes by explicit team id', function () {
expect(StandaloneDocker::ownedByCurrentTeamAPI($this->teamA->id)->where('uuid', $this->destinationB->uuid)->first())->toBeNull();
expect(StandaloneDocker::ownedByCurrentTeamAPI($this->teamB->id)->where('uuid', $this->destinationB->uuid)->first()?->id)->toBe($this->destinationB->id);
});
test('SwarmDocker::ownedByCurrentTeamAPI scopes by explicit team id', function () {
expect(SwarmDocker::ownedByCurrentTeamAPI($this->teamA->id)->where('uuid', $this->swarmDestinationB->uuid)->first())->toBeNull();
expect(SwarmDocker::ownedByCurrentTeamAPI($this->teamB->id)->where('uuid', $this->swarmDestinationB->uuid)->first()?->id)->toBe($this->swarmDestinationB->id);
});
});
describe('Destination/Show team scope', function () {
test('mount with other team destination UUID redirects to index', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->destinationB->uuid]);
expect($component->get('destination'))->toBeNull();
$component->assertRedirect(route('destination.index'));
});
test('mount with own destination UUID loads it', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->destinationA->uuid]);
expect($component->get('destination'))->not->toBeNull();
expect($component->get('destination')->id)->toBe($this->destinationA->id);
});
test('mount with other team swarm destination UUID redirects to index', function () {
$component = Livewire::test(DestinationShow::class, ['destination_uuid' => $this->swarmDestinationB->uuid]);
expect($component->get('destination'))->toBeNull();
$component->assertRedirect(route('destination.index'));
});
});