mirror of
https://github.com/tiennm99/coolify.git
synced 2026-10-14 16:17:10 +00:00
Merge branch 'next' into improve-resource-route-handling
This commit is contained in:
commit
a121386ab4
570 files changed
+18896
-5156
No files matched your search
@@ -0,0 +1,218 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0], ['is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test App',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test DB',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'testdb',
|
||||
'image' => 'postgres:15',
|
||||
'status' => 'running',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
// Create real tokens with team_id (createToken reads from session)
|
||||
session(['currentTeam' => $this->team]);
|
||||
$this->adminRootToken = $this->admin->createToken('admin-root', ['root']);
|
||||
$this->adminReadToken = $this->admin->createToken('admin-read', ['read']);
|
||||
$this->memberRootToken = $this->member->createToken('member-root', ['root']);
|
||||
});
|
||||
|
||||
// --- Unauthenticated Access ---
|
||||
|
||||
test('unauthenticated request to api returns 401', function () {
|
||||
$this->getJson('/api/v1/projects')->assertStatus(401);
|
||||
});
|
||||
|
||||
// --- Admin with root token ---
|
||||
|
||||
test('admin with root token can list projects', function () {
|
||||
$this->withToken($this->adminRootToken->plainTextToken)
|
||||
->getJson('/api/v1/projects')
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin with root token can view project', function () {
|
||||
$this->withToken($this->adminRootToken->plainTextToken)
|
||||
->getJson("/api/v1/projects/{$this->project->uuid}")
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin with root token can view application', function () {
|
||||
$this->withToken($this->adminRootToken->plainTextToken)
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}")
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin with root token can view server', function () {
|
||||
$this->withToken($this->adminRootToken->plainTextToken)
|
||||
->getJson("/api/v1/servers/{$this->server->uuid}")
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin with root token can view database', function () {
|
||||
$this->withToken($this->adminRootToken->plainTextToken)
|
||||
->getJson("/api/v1/databases/{$this->database->uuid}")
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
// --- Member with root token (policy should deny mutations) ---
|
||||
|
||||
test('member with root token is blocked by middleware', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->getJson("/api/v1/projects/{$this->project->uuid}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('member with root token cannot delete project', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->deleteJson("/api/v1/projects/{$this->project->uuid}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('member with root token cannot update application', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->patchJson("/api/v1/applications/{$this->application->uuid}", [
|
||||
'name' => 'New Name',
|
||||
])
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('member with root token cannot delete application', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->deleteJson("/api/v1/applications/{$this->application->uuid}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('member with root token cannot delete server', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->deleteJson("/api/v1/servers/{$this->server->uuid}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('member with root token cannot update server', function () {
|
||||
$this->withToken($this->memberRootToken->plainTextToken)
|
||||
->patchJson("/api/v1/servers/{$this->server->uuid}", [
|
||||
'name' => 'New Server Name',
|
||||
])
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
// --- Token ability checks ---
|
||||
|
||||
test('read-only token cannot create project', function () {
|
||||
$this->withToken($this->adminReadToken->plainTextToken)
|
||||
->postJson('/api/v1/projects', [
|
||||
'name' => 'New Project',
|
||||
])
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
test('read-only token cannot delete application', function () {
|
||||
$this->withToken($this->adminReadToken->plainTextToken)
|
||||
->deleteJson("/api/v1/applications/{$this->application->uuid}")
|
||||
->assertStatus(403);
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot view project via api', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
$otherToken = $otherUser->createToken('other-root', ['root']);
|
||||
|
||||
$this->withToken($otherToken->plainTextToken)
|
||||
->getJson("/api/v1/projects/{$this->project->uuid}")
|
||||
->assertStatus(404);
|
||||
});
|
||||
|
||||
test('user from different team cannot view application via api', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
$otherToken = $otherUser->createToken('other-root', ['root']);
|
||||
|
||||
$this->withToken($otherToken->plainTextToken)
|
||||
->getJson("/api/v1/applications/{$this->application->uuid}")
|
||||
->assertStatus(404);
|
||||
});
|
||||
|
||||
test('user from different team cannot view server via api', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
$otherToken = $otherUser->createToken('other-root', ['root']);
|
||||
|
||||
$this->withToken($otherToken->plainTextToken)
|
||||
->getJson("/api/v1/servers/{$this->server->uuid}")
|
||||
->assertStatus(404);
|
||||
});
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0], ['is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->user = User::factory()->create();
|
||||
$this->team->members()->attach($this->user->id, ['role' => 'owner']);
|
||||
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
describe('POST /api/v1/projects', function () {
|
||||
test('read-only token cannot create a project', function () {
|
||||
$token = $this->user->createToken('read-only', ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/projects', [
|
||||
'name' => 'Test Project',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
|
||||
test('write token can create a project', function () {
|
||||
$token = $this->user->createToken('write-token', ['write']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/projects', [
|
||||
'name' => 'Test Project',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonStructure(['uuid']);
|
||||
});
|
||||
|
||||
test('root token can create a project', function () {
|
||||
$token = $this->user->createToken('root-token', ['root']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/projects', [
|
||||
'name' => 'Test Project',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201);
|
||||
$response->assertJsonStructure(['uuid']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/servers', function () {
|
||||
test('read-only token cannot create a server', function () {
|
||||
$token = $this->user->createToken('read-only', ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/servers', [
|
||||
'name' => 'Test Server',
|
||||
'ip' => '1.2.3.4',
|
||||
'private_key_uuid' => 'fake-uuid',
|
||||
]);
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /api/v1/servers/{uuid}/validate', function () {
|
||||
test('read-only token cannot trigger server validation', function () {
|
||||
$token = $this->user->createToken('read-only', ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->getJson('/api/v1/servers/fake-uuid/validate');
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /api/v1/cloud-tokens/{uuid}/validate', function () {
|
||||
test('read-only token cannot validate cloud provider token', function () {
|
||||
$token = $this->user->createToken('read-only', ['read']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
'Content-Type' => 'application/json',
|
||||
])->postJson('/api/v1/cloud-tokens/fake-uuid/validate');
|
||||
|
||||
$response->assertStatus(403);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
use App\Models\CloudInitScript;
|
||||
use App\Models\CloudProviderToken;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PersonalAccessToken;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use App\Policies\CloudInitScriptPolicy;
|
||||
use App\Policies\CloudProviderTokenPolicy;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
DB::table('private_keys')->insert([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Team SSH Key',
|
||||
'description' => 'Key for testing',
|
||||
'private_key' => 'test-key-content',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->privateKey = PrivateKey::where('team_id', $this->team->id)->first();
|
||||
});
|
||||
|
||||
// --- Private Key Policy ---
|
||||
|
||||
test('admin can create private key', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', PrivateKey::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create private key', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', PrivateKey::class))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can view private key', function () {
|
||||
expect($this->admin->can('view', $this->privateKey))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member can view own team private key', function () {
|
||||
expect($this->member->can('view', $this->privateKey))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can update private key', function () {
|
||||
expect($this->admin->can('update', $this->privateKey))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update private key', function () {
|
||||
expect($this->member->can('update', $this->privateKey))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can delete private key', function () {
|
||||
expect($this->admin->can('delete', $this->privateKey))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete private key', function () {
|
||||
expect($this->member->can('delete', $this->privateKey))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot view private key', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('view', $this->privateKey))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Cloud Provider Token Policy ---
|
||||
|
||||
test('admin can create cloud provider token', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', CloudProviderToken::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create cloud provider token', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', CloudProviderToken::class))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can view any cloud provider tokens', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('viewAny', CloudProviderToken::class))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Cloud Init Script Policy ---
|
||||
|
||||
test('admin can create cloud init script', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', CloudInitScript::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create cloud init script', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', CloudInitScript::class))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can view any cloud init scripts', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('viewAny', CloudInitScript::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('cloud provider and cloud init policies are explicitly registered', function () {
|
||||
expect(Gate::getPolicyFor(CloudProviderToken::class))->toBeInstanceOf(CloudProviderTokenPolicy::class)
|
||||
->and(Gate::getPolicyFor(CloudInitScript::class))->toBeInstanceOf(CloudInitScriptPolicy::class);
|
||||
});
|
||||
|
||||
// --- Personal Access Token (API Token) Policy ---
|
||||
|
||||
test('any user can create personal access token', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', PersonalAccessToken::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can use root permissions for api tokens', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('useRootPermissions', PersonalAccessToken::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot use root permissions for api tokens', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('useRootPermissions', PersonalAccessToken::class))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot use write permissions for api tokens', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('useWritePermissions', PersonalAccessToken::class))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function setupDashboardUser(string $role): array
|
||||
{
|
||||
$team = Team::factory()->create();
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user->teams()->attach($team, ['role' => $role]);
|
||||
|
||||
return [$user, $team];
|
||||
}
|
||||
|
||||
function createProjectForTeam(Team $team): void
|
||||
{
|
||||
Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
}
|
||||
|
||||
function createServerWithKeyForTeam(Team $team): void
|
||||
{
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
}
|
||||
|
||||
test('admin sees add project button on dashboard', function () {
|
||||
[$user, $team] = setupDashboardUser('admin');
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
createProjectForTeam($team);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertSee('New Project');
|
||||
});
|
||||
|
||||
test('member does not see add project button on dashboard', function () {
|
||||
[$user, $team] = setupDashboardUser('member');
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
createProjectForTeam($team);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('New Project');
|
||||
});
|
||||
|
||||
test('admin sees add server button on dashboard', function () {
|
||||
[$user, $team] = setupDashboardUser('admin');
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
createServerWithKeyForTeam($team);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertSee('New Server');
|
||||
});
|
||||
|
||||
test('member does not see add server button on dashboard', function () {
|
||||
[$user, $team] = setupDashboardUser('member');
|
||||
|
||||
$this->actingAs($user);
|
||||
session(['currentTeam' => $team]);
|
||||
|
||||
createServerWithKeyForTeam($team);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('New Server');
|
||||
});
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test DB',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'testdb',
|
||||
'image' => 'postgres:15',
|
||||
'status' => 'running',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->databaseUrl = "/project/{$this->project->uuid}/environment/{$this->environment->uuid}/database/{$this->database->uuid}";
|
||||
});
|
||||
|
||||
// --- Database Policy: view ---
|
||||
|
||||
test('admin can view database', function () {
|
||||
expect($this->admin->can('view', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member can view database', function () {
|
||||
expect($this->member->can('view', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Database Policy: update ---
|
||||
|
||||
test('admin can update database', function () {
|
||||
expect($this->admin->can('update', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update database', function () {
|
||||
expect($this->member->can('update', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Database Policy: delete ---
|
||||
|
||||
test('admin can delete database', function () {
|
||||
expect($this->admin->can('delete', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete database', function () {
|
||||
expect($this->member->can('delete', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Database Policy: manage ---
|
||||
|
||||
test('admin can manage database', function () {
|
||||
expect($this->admin->can('manage', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage database', function () {
|
||||
expect($this->member->can('manage', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Database Policy: manageBackups ---
|
||||
|
||||
test('admin can manage database backups', function () {
|
||||
expect($this->admin->can('manageBackups', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage database backups', function () {
|
||||
expect($this->member->can('manageBackups', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Database Policy: manageEnvironment ---
|
||||
|
||||
test('admin can manage database environment variables', function () {
|
||||
expect($this->admin->can('manageEnvironment', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage database environment variables', function () {
|
||||
expect($this->member->can('manageEnvironment', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Database page access ---
|
||||
|
||||
test('admin can access database page', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get($this->databaseUrl)->assertSuccessful();
|
||||
});
|
||||
|
||||
test('member can access database page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get($this->databaseUrl)->assertSuccessful();
|
||||
});
|
||||
|
||||
// --- Terminal access ---
|
||||
|
||||
test('admin can access terminal for database', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot access terminal for database', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot view database', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('view', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot update database', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('update', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot manage database', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('manage', $this->database))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Destination\Show;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
// Team A owns the destination
|
||||
$this->teamA = Team::factory()->create();
|
||||
$this->userA = User::factory()->create();
|
||||
$this->userA->teams()->attach($this->teamA, ['role' => 'admin']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Key A',
|
||||
'private_key' => 'test-key-a',
|
||||
'team_id' => $this->teamA->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->teamA->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
// Use the StandaloneDocker created by the Server factory
|
||||
$this->destination = $this->server->standaloneDockers()->first();
|
||||
|
||||
// Team B is a different team
|
||||
$this->teamB = Team::factory()->create();
|
||||
$this->userB = User::factory()->create();
|
||||
$this->userB->teams()->attach($this->teamB, ['role' => 'admin']);
|
||||
});
|
||||
|
||||
test('team member can view their own destination', function () {
|
||||
$this->actingAs($this->userA);
|
||||
session(['currentTeam' => $this->teamA]);
|
||||
|
||||
Livewire::test(Show::class, ['destination_uuid' => $this->destination->uuid])
|
||||
->assertSet('name', $this->destination->name);
|
||||
});
|
||||
|
||||
test('cross-team user cannot view destination', function () {
|
||||
$this->actingAs($this->userB);
|
||||
session(['currentTeam' => $this->teamB]);
|
||||
|
||||
Livewire::test(Show::class, ['destination_uuid' => $this->destination->uuid])
|
||||
->assertStatus(403);
|
||||
});
|
||||
@@ -0,0 +1,274 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\All as EnvironmentVariableAll;
|
||||
use App\Livewire\Project\Shared\EnvironmentVariable\Show as EnvironmentVariableShow;
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0], ['is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test App',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->unlockedEnv = EnvironmentVariable::create([
|
||||
'key' => 'UNLOCKED_VAR',
|
||||
'value' => 'secret-unlocked-value',
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'is_preview' => false,
|
||||
'is_shown_once' => false,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
]);
|
||||
|
||||
$this->lockedEnv = EnvironmentVariable::create([
|
||||
'key' => 'LOCKED_VAR',
|
||||
'value' => 'secret-locked-value',
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $this->application->id,
|
||||
'is_preview' => false,
|
||||
'is_shown_once' => true,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
// --- Livewire Show component: locked env values ---
|
||||
|
||||
test('admin sees unlocked env value in Show component', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $this->unlockedEnv,
|
||||
'type' => 'application',
|
||||
]);
|
||||
|
||||
expect($component->get('value'))->toBe('secret-unlocked-value');
|
||||
});
|
||||
|
||||
test('admin cannot see locked env value in Show component', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $this->lockedEnv,
|
||||
'type' => 'application',
|
||||
]);
|
||||
|
||||
expect($component->get('value'))->toBeNull();
|
||||
expect($component->get('real_value'))->toBeNull();
|
||||
});
|
||||
|
||||
test('member cannot see any env value in Show component', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $this->unlockedEnv,
|
||||
'type' => 'application',
|
||||
]);
|
||||
|
||||
expect($component->get('value'))->toBeNull();
|
||||
expect($component->get('real_value'))->toBeNull();
|
||||
});
|
||||
|
||||
test('member has isValueHidden flag set to true', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $this->unlockedEnv,
|
||||
'type' => 'application',
|
||||
]);
|
||||
|
||||
expect($component->get('isValueHidden'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin has isValueHidden flag set to false', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableShow::class, [
|
||||
'env' => $this->unlockedEnv,
|
||||
'type' => 'application',
|
||||
]);
|
||||
|
||||
expect($component->get('isValueHidden'))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Livewire All component: dev view ---
|
||||
|
||||
test('admin dev view shows unlocked env value', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableAll::class, [
|
||||
'resource' => $this->application,
|
||||
]);
|
||||
|
||||
expect($component->get('variables'))->toContain('UNLOCKED_VAR=secret-unlocked-value');
|
||||
});
|
||||
|
||||
test('admin dev view hides locked env value', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableAll::class, [
|
||||
'resource' => $this->application,
|
||||
]);
|
||||
|
||||
expect($component->get('variables'))->toContain('LOCKED_VAR=(Locked Secret, delete and add again to change)');
|
||||
expect($component->get('variables'))->not->toContain('secret-locked-value');
|
||||
});
|
||||
|
||||
test('member dev view hides all env values', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$component = Livewire::test(EnvironmentVariableAll::class, [
|
||||
'resource' => $this->application,
|
||||
]);
|
||||
|
||||
expect($component->get('variables'))->not->toContain('secret-unlocked-value');
|
||||
expect($component->get('variables'))->not->toContain('secret-locked-value');
|
||||
expect($component->get('variables'))->toContain('UNLOCKED_VAR=(Hidden');
|
||||
});
|
||||
|
||||
// --- API: locked env values hidden ---
|
||||
|
||||
test('API hides locked env value even with read:sensitive token', function () {
|
||||
session(['currentTeam' => $this->team]);
|
||||
$token = $this->admin->createToken('admin-sensitive', ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$envs = collect($response->json());
|
||||
$locked = $envs->firstWhere('key', 'LOCKED_VAR');
|
||||
$unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
|
||||
|
||||
expect($locked)->not->toBeNull();
|
||||
expect($locked)->not->toHaveKey('value');
|
||||
expect($locked)->not->toHaveKey('real_value');
|
||||
|
||||
expect($unlocked)->not->toBeNull();
|
||||
expect($unlocked)->toHaveKey('value');
|
||||
});
|
||||
|
||||
test('API hides locked env value with root token', function () {
|
||||
session(['currentTeam' => $this->team]);
|
||||
$token = $this->admin->createToken('admin-root', ['root']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$envs = collect($response->json());
|
||||
$locked = $envs->firstWhere('key', 'LOCKED_VAR');
|
||||
|
||||
expect($locked)->not->toBeNull();
|
||||
expect($locked)->not->toHaveKey('value');
|
||||
expect($locked)->not->toHaveKey('real_value');
|
||||
});
|
||||
|
||||
// --- API: member role hides env values ---
|
||||
|
||||
test('API hides env values for member even with read:sensitive token', function () {
|
||||
session(['currentTeam' => $this->team]);
|
||||
$token = $this->member->createToken('member-sensitive', ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$envs = collect($response->json());
|
||||
$unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
|
||||
|
||||
expect($unlocked)->not->toBeNull();
|
||||
expect($unlocked)->not->toHaveKey('value');
|
||||
expect($unlocked)->not->toHaveKey('real_value');
|
||||
});
|
||||
|
||||
test('API shows env values for admin with read:sensitive token', function () {
|
||||
session(['currentTeam' => $this->team]);
|
||||
$token = $this->admin->createToken('admin-sensitive-2', ['read', 'read:sensitive']);
|
||||
|
||||
$response = $this->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token->plainTextToken,
|
||||
])->getJson("/api/v1/applications/{$this->application->uuid}/envs");
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$envs = collect($response->json());
|
||||
$unlocked = $envs->firstWhere('key', 'UNLOCKED_VAR');
|
||||
|
||||
expect($unlocked)->not->toBeNull();
|
||||
expect($unlocked)->toHaveKey('value');
|
||||
});
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::create(['id' => 0, 'is_api_enabled' => true]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
$this->member = User::factory()->create();
|
||||
$this->admin = User::factory()->create();
|
||||
|
||||
$this->team->members()->attach($this->member->id, ['role' => 'member']);
|
||||
$this->team->members()->attach($this->admin->id, ['role' => 'admin']);
|
||||
|
||||
session(['currentTeam' => $this->team]);
|
||||
});
|
||||
|
||||
function apiRequest($test, string $token, string $method = 'get', string $url = '/api/v1/version')
|
||||
{
|
||||
return $test->withHeaders([
|
||||
'Authorization' => 'Bearer '.$token,
|
||||
'Content-Type' => 'application/json',
|
||||
])->{$method.'Json'}($url);
|
||||
}
|
||||
|
||||
describe('member with legacy elevated token is rejected', function () {
|
||||
test('member with legacy write token gets 403 with descriptive message', function () {
|
||||
$token = $this->member->createToken('legacy-write', ['read', 'write']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertJsonFragment([
|
||||
'message' => 'This API token has permissions (write) that exceed your current role as a team member. Members are restricted to read-only API access. Please revoke this token and create a new one with only read permissions.',
|
||||
]);
|
||||
});
|
||||
|
||||
test('member with legacy deploy token gets 403', function () {
|
||||
$token = $this->member->createToken('legacy-deploy', ['read', 'deploy']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertSee('deploy');
|
||||
$response->assertSee('revoke this token');
|
||||
});
|
||||
|
||||
test('member with legacy root token gets 403', function () {
|
||||
$token = $this->member->createToken('legacy-root', ['root']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertSee('root');
|
||||
});
|
||||
|
||||
test('member with legacy read:sensitive token gets 403', function () {
|
||||
$token = $this->member->createToken('legacy-sensitive', ['read', 'read:sensitive']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertSee('read:sensitive');
|
||||
});
|
||||
|
||||
test('member with legacy write:sensitive token gets 403', function () {
|
||||
$token = $this->member->createToken('legacy-ws', ['read', 'write:sensitive']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$response->assertSee('write:sensitive');
|
||||
});
|
||||
|
||||
test('member with multiple disallowed abilities lists them all', function () {
|
||||
$token = $this->member->createToken('legacy-multi', ['read', 'write', 'deploy', 'read:sensitive']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(403);
|
||||
$json = $response->json();
|
||||
expect($json['message'])->toContain('write');
|
||||
expect($json['message'])->toContain('deploy');
|
||||
expect($json['message'])->toContain('read:sensitive');
|
||||
});
|
||||
});
|
||||
|
||||
describe('member with read-only token passes through', function () {
|
||||
test('member with read token can access read endpoints', function () {
|
||||
$token = $this->member->createToken('read-only', ['read']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('admin with elevated token passes through', function () {
|
||||
test('admin with write token is not blocked', function () {
|
||||
$token = $this->admin->createToken('admin-write', ['read', 'write']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('admin with root token is not blocked', function () {
|
||||
$token = $this->admin->createToken('admin-root', ['root']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
|
||||
test('admin with deploy token is not blocked', function () {
|
||||
$token = $this->admin->createToken('admin-deploy', ['read', 'deploy']);
|
||||
|
||||
$response = apiRequest($this, $token->plainTextToken);
|
||||
|
||||
$response->assertStatus(200);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\NavbarDeleteTeam;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->owner = User::factory()->create();
|
||||
$this->personalTeam = $this->owner->teams()->first();
|
||||
$this->owner->teams()->updateExistingPivot($this->personalTeam->id, ['role' => 'owner']);
|
||||
|
||||
$this->teamToDelete = Team::create(['name' => 'Deletable Team', 'personal_team' => false]);
|
||||
$this->teamToDelete->members()->attach($this->owner->id, ['role' => 'owner']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->teamToDelete->members()->attach($this->member->id, ['role' => 'member']);
|
||||
});
|
||||
|
||||
test('owner can delete team via navbar', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->teamToDelete]);
|
||||
|
||||
Livewire::test(NavbarDeleteTeam::class)
|
||||
->call('delete', 'password')
|
||||
->assertRedirect(route('team.index'));
|
||||
|
||||
expect(Team::find($this->teamToDelete->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('member cannot delete team via navbar', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->teamToDelete]);
|
||||
|
||||
Livewire::test(NavbarDeleteTeam::class)
|
||||
->call('delete', 'password')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect(Team::find($this->teamToDelete->id))->not->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,384 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Notifications\Discord as DiscordNotification;
|
||||
use App\Livewire\Notifications\Email as EmailNotification;
|
||||
use App\Livewire\Notifications\Pushover as PushoverNotification;
|
||||
use App\Livewire\Notifications\Slack as SlackNotification;
|
||||
use App\Livewire\Notifications\Telegram as TelegramNotification;
|
||||
use App\Livewire\Notifications\Webhook as WebhookNotification;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
});
|
||||
|
||||
// --- Discord ---
|
||||
|
||||
test('member cannot send test notification on discord', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->discordNotificationSettings;
|
||||
$settings->update([
|
||||
'discord_enabled' => true,
|
||||
'discord_webhook_url' => 'https://discord.com/api/webhooks/test',
|
||||
]);
|
||||
|
||||
Livewire::test(DiscordNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update discord notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(DiscordNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update discord notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->discordNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Slack ---
|
||||
|
||||
test('member cannot send test notification on slack', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->slackNotificationSettings;
|
||||
$settings->update([
|
||||
'slack_enabled' => true,
|
||||
'slack_webhook_url' => 'https://hooks.slack.com/services/test',
|
||||
]);
|
||||
|
||||
Livewire::test(SlackNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update slack notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(SlackNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update slack notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->slackNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Telegram ---
|
||||
|
||||
test('member cannot send test notification on telegram', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->telegramNotificationSettings;
|
||||
$settings->update([
|
||||
'telegram_enabled' => true,
|
||||
'telegram_token' => 'test-token',
|
||||
'telegram_chat_id' => '123456',
|
||||
]);
|
||||
|
||||
Livewire::test(TelegramNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update telegram notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TelegramNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update telegram notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->telegramNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Email ---
|
||||
|
||||
test('member cannot send test email notification', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->emailNotificationSettings;
|
||||
$settings->update([
|
||||
'smtp_enabled' => true,
|
||||
'smtp_host' => 'localhost',
|
||||
'smtp_port' => 587,
|
||||
'smtp_from_address' => 'test@test.com',
|
||||
'smtp_from_name' => 'Test',
|
||||
]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->call('sendTestEmail')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update email notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('member cannot update smtp email transport directly', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->set('smtpFromAddress', 'member@example.com')
|
||||
->set('smtpFromName', 'Member')
|
||||
->set('smtpHost', 'smtp.example.com')
|
||||
->set('smtpPort', '587')
|
||||
->set('smtpEncryption', 'starttls')
|
||||
->set('smtpPassword', 'member-smtp-password')
|
||||
->call('submitSmtp')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('member cannot update resend email transport directly', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->set('smtpFromAddress', 'member@example.com')
|
||||
->set('smtpFromName', 'Member')
|
||||
->set('resendApiKey', 'member-resend-api-key')
|
||||
->call('submitResend')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('member cannot copy instance email settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(EmailNotification::class)
|
||||
->call('copyFromInstanceSettings')
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update email notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->emailNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Pushover ---
|
||||
|
||||
test('member cannot send test notification on pushover', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->pushoverNotificationSettings;
|
||||
$settings->update([
|
||||
'pushover_enabled' => true,
|
||||
'pushover_app_token' => 'test-token',
|
||||
'pushover_user_key' => 'test-user-key',
|
||||
]);
|
||||
|
||||
Livewire::test(PushoverNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update pushover notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PushoverNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update pushover notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->pushoverNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Webhook ---
|
||||
|
||||
test('member cannot send test notification on webhook', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->webhookNotificationSettings;
|
||||
$settings->update([
|
||||
'webhook_enabled' => true,
|
||||
'webhook_url' => 'https://example.com/webhook',
|
||||
]);
|
||||
|
||||
Livewire::test(WebhookNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot update webhook notification settings', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(WebhookNotification::class)
|
||||
->call('syncData', true)
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('admin can update webhook notification settings', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = $this->team->webhookNotificationSettings;
|
||||
|
||||
expect($this->admin->can('update', $settings))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Send test policy checks ---
|
||||
|
||||
test('admin can send test on all notification channels', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($this->admin->can('sendTest', $this->team->discordNotificationSettings))->toBeTrue();
|
||||
expect($this->admin->can('sendTest', $this->team->slackNotificationSettings))->toBeTrue();
|
||||
expect($this->admin->can('sendTest', $this->team->telegramNotificationSettings))->toBeTrue();
|
||||
expect($this->admin->can('sendTest', $this->team->emailNotificationSettings))->toBeTrue();
|
||||
expect($this->admin->can('sendTest', $this->team->pushoverNotificationSettings))->toBeTrue();
|
||||
expect($this->admin->can('sendTest', $this->team->webhookNotificationSettings))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot send test on any notification channel', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($this->member->can('sendTest', $this->team->discordNotificationSettings))->toBeFalse();
|
||||
expect($this->member->can('sendTest', $this->team->slackNotificationSettings))->toBeFalse();
|
||||
expect($this->member->can('sendTest', $this->team->telegramNotificationSettings))->toBeFalse();
|
||||
expect($this->member->can('sendTest', $this->team->emailNotificationSettings))->toBeFalse();
|
||||
expect($this->member->can('sendTest', $this->team->pushoverNotificationSettings))->toBeFalse();
|
||||
expect($this->member->can('sendTest', $this->team->webhookNotificationSettings))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot view notification secrets', function (string $component, string $settingsRelation, array $secrets) {
|
||||
$settings = $this->team->{$settingsRelation};
|
||||
$settings->update($secrets);
|
||||
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$componentTest = Livewire::test($component);
|
||||
|
||||
foreach ($secrets as $column => $value) {
|
||||
$property = str($column)->camel()->toString();
|
||||
|
||||
$componentTest
|
||||
->assertSet($property, null)
|
||||
->assertDontSee($value);
|
||||
}
|
||||
|
||||
$componentTest->assertSee('Hidden (only admins can view)');
|
||||
})->with([
|
||||
'discord webhook' => [DiscordNotification::class, 'discordNotificationSettings', [
|
||||
'discord_webhook_url' => 'https://discord.com/api/webhooks/secret-member',
|
||||
]],
|
||||
'slack webhook' => [SlackNotification::class, 'slackNotificationSettings', [
|
||||
'slack_webhook_url' => 'https://hooks.slack.com/services/secret-member',
|
||||
]],
|
||||
'telegram token and chat id' => [TelegramNotification::class, 'telegramNotificationSettings', [
|
||||
'telegram_token' => 'telegram-secret-token',
|
||||
'telegram_chat_id' => 'telegram-secret-chat',
|
||||
]],
|
||||
'pushover credentials' => [PushoverNotification::class, 'pushoverNotificationSettings', [
|
||||
'pushover_user_key' => 'pushover-secret-user',
|
||||
'pushover_api_token' => 'pushover-secret-token',
|
||||
]],
|
||||
'generic webhook' => [WebhookNotification::class, 'webhookNotificationSettings', [
|
||||
'webhook_url' => 'https://example.com/secret-webhook',
|
||||
]],
|
||||
'email credentials' => [EmailNotification::class, 'emailNotificationSettings', [
|
||||
'smtp_password' => 'smtp-secret-password',
|
||||
'resend_api_key' => 'resend-secret-api-key',
|
||||
]],
|
||||
]);
|
||||
|
||||
test('admin can view notification secrets', function (string $component, string $settingsRelation, array $secrets) {
|
||||
$settings = $this->team->{$settingsRelation};
|
||||
$settings->update($secrets);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$componentTest = Livewire::test($component);
|
||||
|
||||
foreach ($secrets as $column => $value) {
|
||||
$property = str($column)->camel()->toString();
|
||||
|
||||
$componentTest->assertSet($property, $value);
|
||||
}
|
||||
})->with([
|
||||
'discord webhook' => [DiscordNotification::class, 'discordNotificationSettings', [
|
||||
'discord_webhook_url' => 'https://discord.com/api/webhooks/secret-admin',
|
||||
]],
|
||||
'slack webhook' => [SlackNotification::class, 'slackNotificationSettings', [
|
||||
'slack_webhook_url' => 'https://hooks.slack.com/services/secret-admin',
|
||||
]],
|
||||
'telegram token and chat id' => [TelegramNotification::class, 'telegramNotificationSettings', [
|
||||
'telegram_token' => 'telegram-admin-token',
|
||||
'telegram_chat_id' => 'telegram-admin-chat',
|
||||
]],
|
||||
'pushover credentials' => [PushoverNotification::class, 'pushoverNotificationSettings', [
|
||||
'pushover_user_key' => 'pushover-admin-user',
|
||||
'pushover_api_token' => 'pushover-admin-token',
|
||||
]],
|
||||
'generic webhook' => [WebhookNotification::class, 'webhookNotificationSettings', [
|
||||
'webhook_url' => 'https://example.com/admin-webhook',
|
||||
]],
|
||||
'email credentials' => [EmailNotification::class, 'emailNotificationSettings', [
|
||||
'smtp_password' => 'smtp-admin-password',
|
||||
'resend_api_key' => 'resend-admin-api-key',
|
||||
]],
|
||||
]);
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Show;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
});
|
||||
|
||||
test('admin can create environment in project', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Show::class, ['project_uuid' => $this->project->uuid])
|
||||
->set('name', 'staging')
|
||||
->call('submit')
|
||||
->assertRedirect();
|
||||
|
||||
expect(Environment::where('name', 'staging')->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create environment in project', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Show::class, ['project_uuid' => $this->project->uuid])
|
||||
->set('name', 'staging')
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect(Environment::where('name', 'staging')->exists())->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\CanCreateResources;
|
||||
use App\Livewire\Project\New\DockerCompose;
|
||||
use App\Livewire\Project\New\PublicGitRepository;
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config([
|
||||
'app.maintenance.store' => 'array',
|
||||
'cache.default' => 'array',
|
||||
]);
|
||||
|
||||
InstanceSettings::query()->forceCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('member cannot pass create resources middleware', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$middleware = new CanCreateResources;
|
||||
$request = Request::create('/project/new', 'GET');
|
||||
|
||||
expect(fn () => $middleware->handle($request, fn () => response('ok')))
|
||||
->toThrow(HttpException::class, 'You do not have permission to create resources.');
|
||||
});
|
||||
|
||||
test('admin can pass create resources middleware', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$middleware = new CanCreateResources;
|
||||
$request = Request::create('/project/new', 'GET');
|
||||
$response = $middleware->handle($request, fn () => response('ok'));
|
||||
|
||||
expect($response->getStatusCode())->toBe(200);
|
||||
});
|
||||
|
||||
test('member cannot create docker compose service through livewire action', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(DockerCompose::class)
|
||||
->set('parameters', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
])
|
||||
->set('query', ['destination' => $this->destination->uuid])
|
||||
->set('dockerComposeRaw', <<<'YAML'
|
||||
services:
|
||||
app:
|
||||
image: alpine
|
||||
YAML)
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect(Service::query()->count())->toBe(0);
|
||||
});
|
||||
|
||||
test('public git docker compose creates an application in local mode', function () {
|
||||
config(['app.env' => 'local']);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PublicGitRepository::class)
|
||||
->set('parameters', [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
])
|
||||
->set('query', ['destination' => $this->destination->uuid])
|
||||
->set('repository_url', 'https://github.com/coollabsio/coolify')
|
||||
->set('git_repository', 'https://github.com/coollabsio/coolify')
|
||||
->set('git_branch', 'main')
|
||||
->set('build_pack', 'dockercompose')
|
||||
->set('new_compose_services', true)
|
||||
->call('submit');
|
||||
|
||||
expect(Application::query()->count())->toBe(1)
|
||||
->and(Service::query()->count())->toBe(0);
|
||||
});
|
||||
@@ -0,0 +1,248 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Heading as ApplicationHeading;
|
||||
use App\Livewire\Project\Service\Heading as ServiceHeading;
|
||||
use App\Models\Application;
|
||||
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\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test App',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test DB',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'testdb',
|
||||
'image' => 'postgres:15',
|
||||
'status' => 'running',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->service = Service::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Service',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => 'version: "3"',
|
||||
]);
|
||||
|
||||
$this->serviceParams = [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'service_uuid' => $this->service->uuid,
|
||||
];
|
||||
|
||||
$this->databaseUrl = "/project/{$this->project->uuid}/environment/{$this->environment->uuid}/database/{$this->database->uuid}";
|
||||
});
|
||||
|
||||
// --- Application Heading ---
|
||||
|
||||
test('admin sees deploy controls for application', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->assertSee('Redeploy')
|
||||
->assertSee('Restart')
|
||||
->assertSee('Stop');
|
||||
});
|
||||
|
||||
test('member cannot call deploy on application', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->call('deploy')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot call restart on application', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->call('restart')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot call stop on application', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->call('stop')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member does not see terminal link for application', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->assertDontSee('Terminal');
|
||||
});
|
||||
|
||||
test('admin sees terminal link for application', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ApplicationHeading::class, ['application' => $this->application])
|
||||
->assertSee('Terminal');
|
||||
});
|
||||
|
||||
// --- Database Heading (via page route for rendering, policy checks for actions) ---
|
||||
|
||||
test('admin can access database page', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get($this->databaseUrl)->assertSuccessful();
|
||||
});
|
||||
|
||||
test('member can access database page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get($this->databaseUrl)->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin can manage database', function () {
|
||||
expect($this->admin->can('manage', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage database', function () {
|
||||
expect($this->member->can('manage', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can access terminal for database', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot access terminal for database', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Heading ---
|
||||
|
||||
test('admin sees service deploy button', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertSee('Deploy');
|
||||
});
|
||||
|
||||
test('member cannot call stop on service', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->call('stop')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member does not see terminal link for service', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertDontSee('Terminal');
|
||||
});
|
||||
|
||||
test('admin sees terminal link for service', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertSee('Terminal');
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\ResourceOperations;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Livewire\Livewire;
|
||||
|
||||
beforeEach(function () {
|
||||
// Team A (attacker's team)
|
||||
$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->destinationA = StandaloneDocker::factory()->create(['server_id' => $this->serverA->id]);
|
||||
$this->projectA = Project::factory()->create(['team_id' => $this->teamA->id]);
|
||||
$this->environmentA = Environment::factory()->create(['project_id' => $this->projectA->id]);
|
||||
|
||||
$this->applicationA = Application::factory()->create([
|
||||
'environment_id' => $this->environmentA->id,
|
||||
'destination_id' => $this->destinationA->id,
|
||||
'destination_type' => $this->destinationA->getMorphClass(),
|
||||
]);
|
||||
|
||||
// Team B (victim's team)
|
||||
$this->teamB = Team::factory()->create();
|
||||
$this->serverB = Server::factory()->create(['team_id' => $this->teamB->id]);
|
||||
$this->destinationB = StandaloneDocker::factory()->create(['server_id' => $this->serverB->id]);
|
||||
$this->projectB = Project::factory()->create(['team_id' => $this->teamB->id]);
|
||||
$this->environmentB = Environment::factory()->create(['project_id' => $this->projectB->id]);
|
||||
|
||||
$this->actingAs($this->userA);
|
||||
session(['currentTeam' => $this->teamA]);
|
||||
});
|
||||
|
||||
test('cloneTo rejects destination belonging to another team', function () {
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
|
||||
->call('cloneTo', $this->destinationB->id)
|
||||
->assertHasErrors('destination_id');
|
||||
|
||||
// Ensure no cross-tenant application was created
|
||||
expect(Application::where('destination_id', $this->destinationB->id)->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
test('cloneTo allows destination belonging to own team', function () {
|
||||
$secondDestination = StandaloneDocker::factory()->create(['server_id' => $this->serverA->id]);
|
||||
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
|
||||
->call('cloneTo', $secondDestination->id)
|
||||
->assertHasNoErrors('destination_id')
|
||||
->assertRedirect();
|
||||
});
|
||||
|
||||
test('moveTo rejects environment belonging to another team', function () {
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
|
||||
->call('moveTo', $this->environmentB->id);
|
||||
|
||||
// Resource should still be in original environment
|
||||
$this->applicationA->refresh();
|
||||
expect($this->applicationA->environment_id)->toBe($this->environmentA->id);
|
||||
});
|
||||
|
||||
test('moveTo allows environment belonging to own team', function () {
|
||||
$secondEnvironment = Environment::factory()->create(['project_id' => $this->projectA->id]);
|
||||
|
||||
Livewire::test(ResourceOperations::class, ['resource' => $this->applicationA])
|
||||
->call('moveTo', $secondEnvironment->id)
|
||||
->assertRedirect();
|
||||
|
||||
$this->applicationA->refresh();
|
||||
expect($this->applicationA->environment_id)->toBe($secondEnvironment->id);
|
||||
});
|
||||
|
||||
test('StandaloneDockerPolicy denies update for cross-team user', function () {
|
||||
expect($this->userA->can('update', $this->destinationB))->toBeFalse();
|
||||
});
|
||||
|
||||
test('StandaloneDockerPolicy allows update for same-team user', function () {
|
||||
expect($this->userA->can('update', $this->destinationA))->toBeTrue();
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Notifications\Discord as DiscordNotification;
|
||||
use App\Livewire\Security\PrivateKey\Index as PrivateKeyIndex;
|
||||
use App\Livewire\Team\Member\Index as TeamMemberIndex;
|
||||
use App\Models\DiscordNotificationSettings;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
// Create a private key for the team (bypass model validation via DB insert)
|
||||
DB::table('private_keys')->insert([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Team SSH Key',
|
||||
'description' => 'Key for testing',
|
||||
'private_key' => 'test-key-content',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
});
|
||||
|
||||
// --- Private Key Index ---
|
||||
|
||||
test('admin sees add and cleanup buttons on private key page', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PrivateKeyIndex::class)
|
||||
->assertSee('+ Add')
|
||||
->assertSee('Delete unused SSH Keys');
|
||||
});
|
||||
|
||||
test('member does not see add or cleanup buttons on private key page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PrivateKeyIndex::class)
|
||||
->assertDontSee('+ Add')
|
||||
->assertDontSee('Delete unused SSH Keys');
|
||||
});
|
||||
|
||||
test('member can view private key names on index page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
// Member can see key names (view is allowed for same-team members)
|
||||
// but cannot see create/delete buttons (tested separately above)
|
||||
Livewire::test(PrivateKeyIndex::class)
|
||||
->assertSee('Team SSH Key');
|
||||
});
|
||||
|
||||
test('member cannot call cleanup unused keys', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(PrivateKeyIndex::class)
|
||||
->call('cleanupUnusedKeys')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Team Member Index (Invitations) ---
|
||||
|
||||
test('admin sees invite section on team members page', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamMemberIndex::class)
|
||||
->assertSee('Invite New Member');
|
||||
});
|
||||
|
||||
test('member does not see invite section on team members page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamMemberIndex::class)
|
||||
->assertDontSee('Invite New Member');
|
||||
});
|
||||
|
||||
// --- Notification Settings (Discord as representative) ---
|
||||
|
||||
test('member cannot send test notification on discord', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$settings = DiscordNotificationSettings::where('team_id', $this->team->id)->first();
|
||||
$settings->update([
|
||||
'discord_enabled' => true,
|
||||
'discord_webhook_url' => 'https://discord.com/api/webhooks/test',
|
||||
]);
|
||||
|
||||
Livewire::test(DiscordNotification::class)
|
||||
->call('sendTestNotification')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
@@ -0,0 +1,263 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\PreventRequestsDuringMaintenance;
|
||||
use App\Livewire\Server\Navbar as ServerNavbar;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutMiddleware(PreventRequestsDuringMaintenance::class);
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
$this->server->settings()->update([
|
||||
'is_reachable' => true,
|
||||
'is_usable' => true,
|
||||
]);
|
||||
});
|
||||
|
||||
// --- Server Policy: update ---
|
||||
|
||||
test('admin can update server', function () {
|
||||
expect($this->admin->can('update', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update server', function () {
|
||||
expect($this->member->can('update', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: delete ---
|
||||
|
||||
test('admin can delete server', function () {
|
||||
expect($this->admin->can('delete', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete server', function () {
|
||||
expect($this->member->can('delete', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: view ---
|
||||
|
||||
test('admin can view server', function () {
|
||||
expect($this->admin->can('view', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member can view server', function () {
|
||||
expect($this->member->can('view', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Server Policy: manageProxy ---
|
||||
|
||||
test('admin can manage proxy', function () {
|
||||
expect($this->admin->can('manageProxy', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage proxy', function () {
|
||||
expect($this->member->can('manageProxy', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: manageSentinel ---
|
||||
|
||||
test('admin can manage sentinel', function () {
|
||||
expect($this->admin->can('manageSentinel', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage sentinel', function () {
|
||||
expect($this->member->can('manageSentinel', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: manageCaCertificate ---
|
||||
|
||||
test('admin can manage CA certificate', function () {
|
||||
expect($this->admin->can('manageCaCertificate', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage CA certificate', function () {
|
||||
expect($this->member->can('manageCaCertificate', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: viewSecurity ---
|
||||
|
||||
test('admin can view security', function () {
|
||||
expect($this->admin->can('viewSecurity', $this->server))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot view security', function () {
|
||||
expect($this->member->can('viewSecurity', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server Policy: create ---
|
||||
|
||||
test('admin can create server', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', Server::class))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot create server', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('create', Server::class))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot view server', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('view', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot update server', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('update', $this->server))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Navbar Livewire component actions ---
|
||||
|
||||
test('member cannot restart proxy via navbar', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServerNavbar::class, ['server' => $this->server])
|
||||
->call('restart')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot check proxy via navbar', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServerNavbar::class, ['server' => $this->server])
|
||||
->call('checkProxy')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot start proxy via navbar', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServerNavbar::class, ['server' => $this->server])
|
||||
->call('startProxy')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot stop proxy via navbar', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServerNavbar::class, ['server' => $this->server])
|
||||
->call('stop')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Terminal access gate ---
|
||||
|
||||
test('admin can access terminal', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot access terminal', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('canAccessTerminal'))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Server page access ---
|
||||
|
||||
test('admin can access server page', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get("/server/{$this->server->uuid}")->assertSuccessful();
|
||||
});
|
||||
|
||||
test('member can access server page', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get("/server/{$this->server->uuid}")->assertSuccessful();
|
||||
});
|
||||
|
||||
test('admin can access sentinel configuration and logs pages', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid]))
|
||||
->assertSuccessful();
|
||||
|
||||
$this->get(route('server.sentinel.logs', ['server_uuid' => $this->server->uuid]))
|
||||
->assertSuccessful();
|
||||
});
|
||||
|
||||
test('member cannot access sentinel configuration and logs pages', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get(route('server.sentinel', ['server_uuid' => $this->server->uuid]))
|
||||
->assertForbidden();
|
||||
|
||||
$this->get(route('server.sentinel.logs', ['server_uuid' => $this->server->uuid]))
|
||||
->assertForbidden();
|
||||
});
|
||||
|
||||
test('sentinel navigation is only visible to team admins', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get("/server/{$this->server->uuid}")
|
||||
->assertSuccessful()
|
||||
->assertDontSee(route('server.sentinel', ['server_uuid' => $this->server->uuid]));
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$this->get("/server/{$this->server->uuid}")
|
||||
->assertSuccessful()
|
||||
->assertSee(route('server.sentinel', ['server_uuid' => $this->server->uuid]));
|
||||
});
|
||||
|
||||
test('unauthenticated user cannot access server page', function () {
|
||||
$this->get("/server/{$this->server->uuid}")->assertRedirect('/login');
|
||||
});
|
||||
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Service\Heading as ServiceHeading;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->service = Service::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Service',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => 'version: "3"',
|
||||
]);
|
||||
|
||||
$this->serviceParams = [
|
||||
'project_uuid' => $this->project->uuid,
|
||||
'environment_uuid' => $this->environment->uuid,
|
||||
'service_uuid' => $this->service->uuid,
|
||||
];
|
||||
});
|
||||
|
||||
// --- Service Policy: view ---
|
||||
|
||||
test('admin can view service', function () {
|
||||
expect($this->admin->can('view', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member can view service', function () {
|
||||
expect($this->member->can('view', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Service Policy: update ---
|
||||
|
||||
test('admin can update service', function () {
|
||||
expect($this->admin->can('update', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update service', function () {
|
||||
expect($this->member->can('update', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Policy: delete ---
|
||||
|
||||
test('admin can delete service', function () {
|
||||
expect($this->admin->can('delete', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete service', function () {
|
||||
expect($this->member->can('delete', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Policy: deploy ---
|
||||
|
||||
test('admin can deploy service', function () {
|
||||
expect($this->admin->can('deploy', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot deploy service', function () {
|
||||
expect($this->member->can('deploy', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Policy: stop ---
|
||||
|
||||
test('admin can stop service', function () {
|
||||
expect($this->admin->can('stop', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot stop service', function () {
|
||||
expect($this->member->can('stop', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Policy: manageEnvironment ---
|
||||
|
||||
test('admin can manage service environment variables', function () {
|
||||
expect($this->admin->can('manageEnvironment', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage service environment variables', function () {
|
||||
expect($this->member->can('manageEnvironment', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Service Heading Livewire actions ---
|
||||
|
||||
test('member cannot call start on service heading', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->call('start')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot call stop on service heading', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->call('stop')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot call restart on service heading', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->call('restart')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot call force deploy on service heading', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->call('forceDeploy')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Service Heading visibility ---
|
||||
|
||||
test('member does not see terminal link for service', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertDontSee('Terminal');
|
||||
});
|
||||
|
||||
test('admin sees terminal link for service', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertSee('Terminal');
|
||||
});
|
||||
|
||||
test('admin sees deploy button for service', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(ServiceHeading::class, [
|
||||
'service' => $this->service,
|
||||
'parameters' => $this->serviceParams,
|
||||
'query' => [],
|
||||
])
|
||||
->assertSee('Deploy');
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot view service', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('view', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot update service', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('update', $this->service))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Shared\Danger;
|
||||
use App\Livewire\Project\Shared\Tags;
|
||||
use App\Livewire\Project\Shared\Webhooks;
|
||||
use App\Models\Application;
|
||||
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\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::updateOrCreate(['id' => 0], ['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test App',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test DB',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'testdb',
|
||||
'image' => 'postgres:15',
|
||||
'status' => 'running',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->service = Service::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Service',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => 'version: "3"',
|
||||
]);
|
||||
});
|
||||
|
||||
// --- Danger Zone: Application ---
|
||||
|
||||
test('admin can delete application', function () {
|
||||
expect($this->admin->can('delete', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete application via danger zone', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Danger::class, ['resource' => $this->application])
|
||||
->call('delete', 'password')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Danger Zone: Database ---
|
||||
|
||||
test('admin can delete database', function () {
|
||||
expect($this->admin->can('delete', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete database via danger zone', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Danger::class, ['resource' => $this->database])
|
||||
->call('delete', 'password')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Danger Zone: Service ---
|
||||
|
||||
test('admin can delete service', function () {
|
||||
expect($this->admin->can('delete', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete service via danger zone', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Danger::class, ['resource' => $this->service])
|
||||
->call('delete', 'password')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Tags: Application ---
|
||||
|
||||
test('member cannot add tag to application', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Tags::class, ['resource' => $this->application])
|
||||
->set('newTags', 'test-tag')
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('admin can add tag to application', function () {
|
||||
expect($this->admin->can('update', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Webhooks: Application ---
|
||||
|
||||
test('member cannot update application webhooks', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Webhooks::class, ['resource' => $this->application])
|
||||
->call('submit')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('admin can update application webhooks', function () {
|
||||
expect($this->admin->can('update', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot view application webhook secrets', function () {
|
||||
$this->application->update([
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
'manual_webhook_secret_github' => 'github-secret-value',
|
||||
'manual_webhook_secret_gitlab' => 'gitlab-secret-value',
|
||||
'manual_webhook_secret_bitbucket' => 'bitbucket-secret-value',
|
||||
'manual_webhook_secret_gitea' => 'gitea-secret-value',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()])
|
||||
->assertSet('githubManualWebhookSecret', null)
|
||||
->assertSet('gitlabManualWebhookSecret', null)
|
||||
->assertSet('bitbucketManualWebhookSecret', null)
|
||||
->assertSet('giteaManualWebhookSecret', null)
|
||||
->assertSee('Hidden (only admins can view)')
|
||||
->assertDontSee('github-secret-value')
|
||||
->assertDontSee('gitlab-secret-value')
|
||||
->assertDontSee('bitbucket-secret-value')
|
||||
->assertDontSee('gitea-secret-value');
|
||||
});
|
||||
|
||||
test('admin can view application webhook secrets', function () {
|
||||
$this->application->update([
|
||||
'git_repository' => 'coollabsio/coolify',
|
||||
'git_branch' => 'main',
|
||||
'manual_webhook_secret_github' => 'github-secret-value',
|
||||
'manual_webhook_secret_gitlab' => 'gitlab-secret-value',
|
||||
'manual_webhook_secret_bitbucket' => 'bitbucket-secret-value',
|
||||
'manual_webhook_secret_gitea' => 'gitea-secret-value',
|
||||
]);
|
||||
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(Webhooks::class, ['resource' => $this->application->fresh()])
|
||||
->assertSet('githubManualWebhookSecret', 'github-secret-value')
|
||||
->assertSet('gitlabManualWebhookSecret', 'gitlab-secret-value')
|
||||
->assertSet('bitbucketManualWebhookSecret', 'bitbucket-secret-value')
|
||||
->assertSet('giteaManualWebhookSecret', 'gitea-secret-value');
|
||||
});
|
||||
|
||||
// --- Resource Limits (policy checks, mount requires full resource data) ---
|
||||
|
||||
test('member cannot update application resource limits', function () {
|
||||
expect($this->member->can('update', $this->application))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can update application resource limits', function () {
|
||||
expect($this->admin->can('update', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update database resource limits', function () {
|
||||
expect($this->member->can('update', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('member cannot update service resource limits', function () {
|
||||
expect($this->member->can('update', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Environment Variables (Policy checks) ---
|
||||
|
||||
test('admin can manage application environment variables', function () {
|
||||
expect($this->admin->can('manageEnvironment', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage application environment variables', function () {
|
||||
expect($this->member->can('manageEnvironment', $this->application))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can manage database environment variables', function () {
|
||||
expect($this->admin->can('manageEnvironment', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage database environment variables', function () {
|
||||
expect($this->member->can('manageEnvironment', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can manage service environment variables', function () {
|
||||
expect($this->admin->can('manageEnvironment', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage service environment variables', function () {
|
||||
expect($this->member->can('manageEnvironment', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Project Policy ---
|
||||
|
||||
test('admin can update project', function () {
|
||||
expect($this->admin->can('update', $this->project))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update project', function () {
|
||||
expect($this->member->can('update', $this->project))->toBeFalse();
|
||||
});
|
||||
|
||||
test('admin can delete project', function () {
|
||||
expect($this->admin->can('delete', $this->project))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete project', function () {
|
||||
expect($this->member->can('delete', $this->project))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Environment Policy ---
|
||||
|
||||
test('admin can delete environment', function () {
|
||||
expect($this->admin->can('delete', $this->environment))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete environment', function () {
|
||||
expect($this->member->can('delete', $this->environment))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy ---
|
||||
|
||||
test('admin can update team', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update team', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeFalse();
|
||||
});
|
||||
@@ -0,0 +1,327 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Team\Index as TeamIndex;
|
||||
use App\Livewire\Team\Member as TeamMember;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
InstanceSettings::updateOrCreate(['id' => 0]);
|
||||
|
||||
$this->team = Team::factory()->create(['personal_team' => false]);
|
||||
|
||||
$this->owner = User::factory()->create();
|
||||
$this->owner->teams()->attach($this->team, ['role' => 'owner']);
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
});
|
||||
|
||||
// --- Team Policy: update ---
|
||||
|
||||
test('owner can update team', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can update team', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot update team', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy: delete ---
|
||||
|
||||
test('owner can delete team', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('delete', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can delete team', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('delete', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot delete team', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('delete', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy: manageMembers ---
|
||||
|
||||
test('owner can manage team members', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageMembers', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can manage team members', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageMembers', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage team members', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageMembers', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy: manageInvitations ---
|
||||
|
||||
test('owner can manage invitations', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageInvitations', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can manage invitations', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageInvitations', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot manage invitations', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageInvitations', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Index Livewire: update ---
|
||||
|
||||
test('member cannot submit team settings via policy', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Index Livewire: delete ---
|
||||
|
||||
test('member cannot delete team via index', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamIndex::class)
|
||||
->call('delete', 'password')
|
||||
->assertDispatched('error');
|
||||
|
||||
expect(Team::find($this->team->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('admin can delete team via policy', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('delete', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Team Member Livewire: role changes ---
|
||||
|
||||
test('member cannot change roles via team member component', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamMember::class, ['member' => $this->admin])
|
||||
->call('makeAdmin')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
test('member cannot remove team members', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
Livewire::test(TeamMember::class, ['member' => $this->admin])
|
||||
->call('remove')
|
||||
->assertDispatched('error');
|
||||
});
|
||||
|
||||
// --- Invitations & Invite Link (policy checks — views wrapped in @can render empty for members) ---
|
||||
|
||||
test('member cannot manage invitations via policy', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageInvitations', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('owner can manage invitations via policy', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('manageInvitations', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot update team', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($otherUser);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
|
||||
expect(auth()->user()->can('update', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team cannot manage members', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'owner']);
|
||||
|
||||
$this->actingAs($otherUser);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
|
||||
expect(auth()->user()->can('manageMembers', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy: view ---
|
||||
|
||||
test('owner can view team', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('view', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can view team', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('view', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member can view team', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('view', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('non-team member cannot view team', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('view', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team Policy: viewAdmin ---
|
||||
|
||||
test('owner can view admin panel', function () {
|
||||
$this->actingAs($this->owner);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('viewAdmin', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('admin can view admin panel', function () {
|
||||
$this->actingAs($this->admin);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('viewAdmin', $this->team))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot view admin panel', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(auth()->user()->can('viewAdmin', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('non-team member cannot view admin panel', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('viewAdmin', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Non-team member isolation ---
|
||||
|
||||
test('non-team member cannot update team', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('update', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('non-team member cannot delete team', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('delete', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('non-team member cannot manage members', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('manageMembers', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
test('non-team member cannot manage invitations', function () {
|
||||
$outsider = User::factory()->create();
|
||||
$this->actingAs($outsider);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect($outsider->can('manageInvitations', $this->team))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Team creation and model-level protection ---
|
||||
|
||||
test('member can create a new independent team', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$newTeam = Team::create([
|
||||
'name' => 'New Team',
|
||||
'description' => 'Created by member',
|
||||
'personal_team' => false,
|
||||
]);
|
||||
|
||||
expect($newTeam)->toBeInstanceOf(Team::class)
|
||||
->and($newTeam->name)->toBe('New Team');
|
||||
});
|
||||
|
||||
test('member cannot update an existing team at model level', function () {
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
expect(fn () => $this->team->update(['name' => 'Hacked']))
|
||||
->toThrow(Exception::class, 'You are not allowed to update this team.');
|
||||
});
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Service;
|
||||
use App\Models\ServiceDatabase;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\StandalonePostgresql;
|
||||
use App\Models\Team;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Once;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
$this->withoutVite();
|
||||
|
||||
Once::flush();
|
||||
|
||||
config(['app.maintenance.driver' => 'file']);
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
|
||||
$this->team = Team::factory()->create();
|
||||
|
||||
$this->admin = User::factory()->create();
|
||||
$this->admin->teams()->attach($this->team, ['role' => 'admin']);
|
||||
|
||||
$this->member = User::factory()->create();
|
||||
$this->member->teams()->attach($this->team, ['role' => 'member']);
|
||||
|
||||
$keyId = DB::table('private_keys')->insertGetId([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Key',
|
||||
'private_key' => 'test-key',
|
||||
'team_id' => $this->team->id,
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$this->server = Server::factory()->create([
|
||||
'team_id' => $this->team->id,
|
||||
'private_key_id' => $keyId,
|
||||
]);
|
||||
|
||||
StandaloneDocker::withoutEvents(function () {
|
||||
$this->destination = StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
);
|
||||
});
|
||||
|
||||
$this->project = Project::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test Project',
|
||||
'team_id' => $this->team->id,
|
||||
]);
|
||||
|
||||
$this->environment = $this->project->environments()->first();
|
||||
|
||||
$this->database = StandalonePostgresql::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test DB',
|
||||
'postgres_user' => 'postgres',
|
||||
'postgres_password' => 'password',
|
||||
'postgres_db' => 'testdb',
|
||||
'image' => 'postgres:15',
|
||||
'status' => 'running',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
]);
|
||||
|
||||
$this->application = Application::factory()->create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'Test App',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'status' => 'running',
|
||||
]);
|
||||
|
||||
$this->service = Service::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'test-service',
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => $this->destination->getMorphClass(),
|
||||
'docker_compose_raw' => 'version: "3"',
|
||||
]);
|
||||
|
||||
$this->serviceDatabase = ServiceDatabase::create([
|
||||
'uuid' => (string) Str::uuid(),
|
||||
'name' => 'test-service-db',
|
||||
'service_id' => $this->service->id,
|
||||
]);
|
||||
});
|
||||
|
||||
// --- DatabasePolicy::uploadBackup (covers Standalone* databases) ---
|
||||
|
||||
test('admin can upload backup for standalone database', function () {
|
||||
expect($this->admin->can('uploadBackup', $this->database))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot upload backup for standalone database', function () {
|
||||
expect($this->member->can('uploadBackup', $this->database))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- ApplicationPolicy::uploadBackup ---
|
||||
|
||||
test('admin can upload backup for application', function () {
|
||||
expect($this->admin->can('uploadBackup', $this->application))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot upload backup for application', function () {
|
||||
expect($this->member->can('uploadBackup', $this->application))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- ServicePolicy::uploadBackup ---
|
||||
|
||||
test('admin can upload backup for service', function () {
|
||||
expect($this->admin->can('uploadBackup', $this->service))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot upload backup for service', function () {
|
||||
expect($this->member->can('uploadBackup', $this->service))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- ServiceDatabasePolicy::uploadBackup (delegates to ServicePolicy) ---
|
||||
|
||||
test('admin can upload backup for service database', function () {
|
||||
expect($this->admin->can('uploadBackup', $this->serviceDatabase))->toBeTrue();
|
||||
});
|
||||
|
||||
test('member cannot upload backup for service database', function () {
|
||||
expect($this->member->can('uploadBackup', $this->serviceDatabase))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- Cross-team isolation ---
|
||||
|
||||
test('user from different team cannot upload backup', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
expect($otherUser->can('uploadBackup', $this->database))->toBeFalse();
|
||||
expect($otherUser->can('uploadBackup', $this->application))->toBeFalse();
|
||||
expect($otherUser->can('uploadBackup', $this->service))->toBeFalse();
|
||||
expect($otherUser->can('uploadBackup', $this->serviceDatabase))->toBeFalse();
|
||||
});
|
||||
|
||||
// --- HTTP endpoint: POST /upload/backup/{uuid} ---
|
||||
|
||||
test('member gets 403 from POST /upload/backup and no file lands on disk', function () {
|
||||
$uploadDir = storage_path('app/upload/'.$this->database->uuid);
|
||||
if (File::exists($uploadDir)) {
|
||||
File::deleteDirectory($uploadDir);
|
||||
}
|
||||
|
||||
$this->actingAs($this->member);
|
||||
session(['currentTeam' => $this->team]);
|
||||
|
||||
$response = $this->withHeader('Accept', 'application/json')
|
||||
->post(route('upload.backup', ['databaseUuid' => $this->database->uuid]));
|
||||
|
||||
$response->assertForbidden();
|
||||
|
||||
expect(File::exists($uploadDir.'/restore'))->toBeFalse();
|
||||
});
|
||||
|
||||
test('user from different team hits null-resource branch with 500', function () {
|
||||
$otherTeam = Team::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$otherUser->teams()->attach($otherTeam, ['role' => 'admin']);
|
||||
|
||||
$this->actingAs($otherUser);
|
||||
session(['currentTeam' => $otherTeam]);
|
||||
|
||||
$response = $this->post(route('upload.backup', ['databaseUuid' => $this->database->uuid]));
|
||||
|
||||
$response->assertStatus(500);
|
||||
});
|
||||
|
||||
test('unauthenticated request is redirected to login', function () {
|
||||
$response = $this->post(route('upload.backup', ['databaseUuid' => $this->database->uuid]));
|
||||
|
||||
$response->assertRedirect('/login');
|
||||
});
|
||||
Reference in new issue
Block a user