mirror of
https://github.com/coollabsio/coolify.git
synced 2026-11-06 13:10:37 -04:00
Merge remote-tracking branch 'origin/main' into next
This commit is contained in:
@@ -5,6 +5,7 @@ namespace App\Livewire;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\TeamInvitation;
|
||||
use Illuminate\Support\Collection;
|
||||
use Livewire\Component;
|
||||
|
||||
@@ -40,6 +41,34 @@ class Dashboard extends Component
|
||||
|
||||
public function render()
|
||||
{
|
||||
return view('livewire.dashboard');
|
||||
return view('livewire.dashboard', [
|
||||
'pendingInvitations' => $this->pendingInvitations(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unexpired invitations for the user's email to teams the user has not joined yet.
|
||||
*
|
||||
* Uses hasExpired() instead of isValid() so rendering never deletes invitations or users.
|
||||
*
|
||||
* @return Collection<int, TeamInvitation>
|
||||
*/
|
||||
private function pendingInvitations(): Collection
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
if (! $user) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return TeamInvitation::query()
|
||||
->where('email', strtolower($user->email))
|
||||
->whereHas('team')
|
||||
->whereNotIn('team_id', $user->teams()->select('teams.id'))
|
||||
->with('team:id,name')
|
||||
->oldest()
|
||||
->get()
|
||||
->reject(fn (TeamInvitation $invitation) => $invitation->hasExpired())
|
||||
->values();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,9 +77,11 @@ class Domains extends Component
|
||||
public ?string $editingGeneratedHost = null;
|
||||
|
||||
/** @var array<int, array{url: string, service: ?string, dns_status: string, dns_message: string, expected_ip: ?string, checked_at?: ?string, is_suggested?: bool, suggested_for?: ?string, suggestion_label?: ?string, needs_force_add?: bool, internal_port?: ?int, has_port_override?: bool}> */
|
||||
#[Locked]
|
||||
public array $domainRows = [];
|
||||
|
||||
/** When set, the next addSuggestedDomain call for this index skips the DNS block. */
|
||||
#[Locked]
|
||||
public ?int $forceAddSuggestedIndex = null;
|
||||
|
||||
/** @var array<int, string> */
|
||||
@@ -122,6 +124,7 @@ class Domains extends Component
|
||||
|
||||
public bool $isCheckingDns = false;
|
||||
|
||||
#[Locked]
|
||||
public bool $dnsValidationEnabled = true;
|
||||
|
||||
/** Resolved or literal IP users should point DNS at. */
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Support\DomainUrlParts;
|
||||
use App\Support\ValidationPatterns;
|
||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class PreviewDomains extends Component
|
||||
@@ -19,6 +20,8 @@ class PreviewDomains extends Component
|
||||
|
||||
public ApplicationPreview $preview;
|
||||
|
||||
/** @var array<int, array<string, mixed>> */
|
||||
#[Locked]
|
||||
public array $domainRows = [];
|
||||
|
||||
public array $newDomainParts = ['scheme' => 'https', 'host' => '', 'port' => '', 'path' => ''];
|
||||
|
||||
@@ -110,6 +110,7 @@ class Domains extends Component
|
||||
|
||||
public bool $isCheckingDns = false;
|
||||
|
||||
#[Locked]
|
||||
public bool $dnsValidationEnabled = true;
|
||||
|
||||
#[Locked]
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Livewire\Team;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use Livewire\Component;
|
||||
use Livewire\WithPagination;
|
||||
@@ -79,6 +80,10 @@ class AdminView extends Component
|
||||
return $this->dispatch('error', 'User not found');
|
||||
}
|
||||
|
||||
if ($error = $this->deletionError(auth()->user(), $user)) {
|
||||
return $this->dispatch('error', $error);
|
||||
}
|
||||
|
||||
try {
|
||||
$user->delete();
|
||||
auditLog('ui.user.deleted', [
|
||||
@@ -96,6 +101,38 @@ class AdminView extends Component
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Instance admins may delete other users, but never the root user, their
|
||||
* own account (use the profile instead), or a user whose role in the root
|
||||
* team is higher than their own.
|
||||
*/
|
||||
private function deletionError(User $actor, User $target): ?string
|
||||
{
|
||||
if ($target->id === 0) {
|
||||
return 'The root user cannot be deleted.';
|
||||
}
|
||||
|
||||
if ($target->id === $actor->id) {
|
||||
return 'Delete your own account from your profile.';
|
||||
}
|
||||
|
||||
$actorRole = $this->rootTeamRole($actor);
|
||||
$targetRole = $this->rootTeamRole($target);
|
||||
|
||||
if (! $actorRole || ($targetRole && $targetRole->gt($actorRole))) {
|
||||
return 'You cannot delete a user with a higher role in the root team.';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function rootTeamRole(User $user): ?Role
|
||||
{
|
||||
$role = $user->teams()->where('teams.id', 0)->first()?->pivot?->role;
|
||||
|
||||
return $role ? Role::tryFrom($role) : null;
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
abort_unless(isInstanceAdmin(), 403);
|
||||
|
||||
@@ -6,7 +6,6 @@ use App\Actions\Fortify\CreateNewUser;
|
||||
use App\Actions\Fortify\ResetUserPassword;
|
||||
use App\Actions\Fortify\UpdateUserPassword;
|
||||
use App\Models\OauthSetting;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
@@ -82,29 +81,18 @@ class FortifyServiceProvider extends ServiceProvider
|
||||
$user->updated_at = now();
|
||||
$user->save();
|
||||
|
||||
// Check if user has a pending invitation they haven't accepted yet
|
||||
$invitation = TeamInvitation::whereEmail($email)->first();
|
||||
if ($invitation && $invitation->isValid()) {
|
||||
// User is logging in for the first time after being invited
|
||||
// Attach them to the invited team if not already attached
|
||||
if (! $user->teams()->where('team_id', $invitation->team->id)->exists()) {
|
||||
$user->teams()->attach($invitation->team->id, ['role' => $invitation->role]);
|
||||
}
|
||||
$team = $invitation->team;
|
||||
$invitation->delete();
|
||||
session(['currentTeam' => $team]);
|
||||
} else {
|
||||
// Restore the last active team; only fall back when unambiguous.
|
||||
$team = $user->resolveStoredTeam();
|
||||
if (! $team && $user->teams->isEmpty()) {
|
||||
$team = $user->recreate_personal_team();
|
||||
}
|
||||
if ($team) {
|
||||
session(['currentTeam' => $team]);
|
||||
}
|
||||
// Otherwise (multiple teams, no stored choice) leave the session
|
||||
// team unset so the user is sent to the team-selection screen.
|
||||
// Pending team invitations are not accepted here; the user accepts
|
||||
// them explicitly on the invitation page (team.invitation.show).
|
||||
// Restore the last active team; only fall back when unambiguous.
|
||||
$team = $user->resolveStoredTeam();
|
||||
if (! $team && $user->teams->isEmpty()) {
|
||||
$team = $user->recreate_personal_team();
|
||||
}
|
||||
if ($team) {
|
||||
session(['currentTeam' => $team]);
|
||||
}
|
||||
// Otherwise (multiple teams, no stored choice) leave the session
|
||||
// team unset so the user is sent to the team-selection screen.
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,23 @@
|
||||
@endphp
|
||||
|
||||
<div class="flex min-w-0 flex-col gap-8">
|
||||
@if ($pendingInvitations->isNotEmpty())
|
||||
<div class="flex min-w-0 flex-col gap-2">
|
||||
@foreach ($pendingInvitations as $invitation)
|
||||
<x-callout type="info" title="Pending team invitation"
|
||||
wire:key="dashboard-invitation-{{ $invitation->uuid }}">
|
||||
<div class="flex min-w-0 flex-wrap items-center justify-between gap-2">
|
||||
<span class="min-w-0">Team <span class="font-semibold">{{ $invitation->team->name }}</span>
|
||||
invited you as {{ ucfirst($invitation->role) }}.</span>
|
||||
<a href="{{ route('team.invitation.show', $invitation->uuid) }}" class="button shrink-0">
|
||||
Review invitation
|
||||
</a>
|
||||
</div>
|
||||
</x-callout>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<livewire:dashboard.active-deployments />
|
||||
|
||||
@if ($hasTrafficAnalytics)
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Team\AdminView;
|
||||
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::forceCreate(['id' => 0]);
|
||||
|
||||
$this->rootTeam = Team::factory()->create(['id' => 0]);
|
||||
$this->rootUser = User::factory()->create(['id' => 0]);
|
||||
|
||||
$this->rootAdmin = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($this->rootAdmin->id, ['role' => 'admin']);
|
||||
|
||||
$this->rootOwner = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($this->rootOwner->id, ['role' => 'owner']);
|
||||
});
|
||||
|
||||
function actAsRootTeamMemberForAdminViewDeletion(User $user): void
|
||||
{
|
||||
test()->actingAs($user);
|
||||
session(['currentTeam' => Team::find(0)]);
|
||||
}
|
||||
|
||||
test('a root team admin cannot delete the root user', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', 0, 'password')
|
||||
->assertDispatched('error', 'The root user cannot be deleted.');
|
||||
|
||||
expect(User::find(0))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team admin cannot delete a root team owner', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootOwner->id, 'password')
|
||||
->assertDispatched('error', 'You cannot delete a user with a higher role in the root team.');
|
||||
|
||||
expect(User::find($this->rootOwner->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team owner cannot delete the root user', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootOwner);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', 0, 'password')
|
||||
->assertDispatched('error', 'The root user cannot be deleted.');
|
||||
|
||||
expect(User::find(0))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('an instance admin cannot delete their own account from the admin view', function () {
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootAdmin->id, 'password')
|
||||
->assertDispatched('error', 'Delete your own account from your profile.');
|
||||
|
||||
expect(User::find($this->rootAdmin->id))->not->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team owner can delete a root team admin and a member', function () {
|
||||
$member = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($member->id, ['role' => 'member']);
|
||||
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootOwner);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $this->rootAdmin->id, 'password')
|
||||
->assertReturned(true)
|
||||
->call('delete', $member->id, 'password')
|
||||
->assertReturned(true);
|
||||
|
||||
expect(User::find($this->rootAdmin->id))->toBeNull()
|
||||
->and(User::find($member->id))->toBeNull();
|
||||
});
|
||||
|
||||
test('a root team admin can delete another root team admin and a user outside the root team', function () {
|
||||
$otherAdmin = User::factory()->create();
|
||||
$this->rootTeam->members()->attach($otherAdmin->id, ['role' => 'admin']);
|
||||
$outsider = User::factory()->create();
|
||||
|
||||
actAsRootTeamMemberForAdminViewDeletion($this->rootAdmin);
|
||||
|
||||
Livewire::test(AdminView::class)
|
||||
->call('delete', $otherAdmin->id, 'password')
|
||||
->assertReturned(true)
|
||||
->call('delete', $outsider->id, 'password')
|
||||
->assertReturned(true);
|
||||
|
||||
expect(User::find($otherAdmin->id))->toBeNull()
|
||||
->and(User::find($outsider->id))->toBeNull();
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
beforeEach(function () {
|
||||
config()->set('app.maintenance.store', 'array');
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::firstOrCreate(['id' => 0]));
|
||||
|
||||
$this->invitee = User::factory()->create(['email' => 'invitee@example.com']);
|
||||
$this->personalTeam = $this->invitee->teams->first();
|
||||
$this->personalTeam->update(['show_boarding' => false]);
|
||||
|
||||
$this->invitingTeam = Team::factory()->create(['show_boarding' => false]);
|
||||
$this->invitation = TeamInvitation::create([
|
||||
'team_id' => $this->invitingTeam->id,
|
||||
'uuid' => 'pending-login-invitation',
|
||||
'email' => $this->invitee->email,
|
||||
'role' => 'admin',
|
||||
'link' => route('team.invitation.show', ['uuid' => 'pending-login-invitation']),
|
||||
'via' => 'email',
|
||||
]);
|
||||
});
|
||||
|
||||
it('does not accept a pending invitation on password login', function () {
|
||||
$this->post('/login', ['email' => $this->invitee->email, 'password' => 'password'])
|
||||
->assertRedirect();
|
||||
|
||||
$this->assertAuthenticatedAs($this->invitee);
|
||||
expect($this->invitee->teams()->where('team_id', $this->invitingTeam->id)->exists())->toBeFalse()
|
||||
->and(data_get(session('currentTeam'), 'id'))->toBe($this->personalTeam->id);
|
||||
$this->assertDatabaseHas('team_invitations', ['id' => $this->invitation->id]);
|
||||
});
|
||||
|
||||
it('lets the user accept the invitation explicitly after login', function () {
|
||||
$this->post('/login', ['email' => $this->invitee->email, 'password' => 'password']);
|
||||
|
||||
$this->get(route('team.invitation.show', $this->invitation->uuid))
|
||||
->assertSuccessful()
|
||||
->assertSee($this->invitingTeam->name);
|
||||
|
||||
$this->post(route('team.invitation.accept', $this->invitation->uuid))
|
||||
->assertRedirect(route('team.index'));
|
||||
|
||||
expect($this->invitee->teams()->where('team_id', $this->invitingTeam->id)->first()?->pivot->role)->toBe('admin')
|
||||
->and(data_get(session('currentTeam'), 'id'))->toBe($this->invitingTeam->id);
|
||||
$this->assertDatabaseMissing('team_invitations', ['id' => $this->invitation->id]);
|
||||
});
|
||||
|
||||
it('returns to the invitation page after logging in from the invitation link', function () {
|
||||
$this->get(route('team.invitation.show', $this->invitation->uuid))
|
||||
->assertRedirect(route('login'));
|
||||
|
||||
$this->post('/login', ['email' => $this->invitee->email, 'password' => 'password'])
|
||||
->assertRedirect(route('team.invitation.show', $this->invitation->uuid));
|
||||
|
||||
$this->get(route('team.invitation.show', $this->invitation->uuid))
|
||||
->assertSuccessful()
|
||||
->assertSee($this->invitingTeam->name);
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Dashboard;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\Team;
|
||||
use App\Models\TeamInvitation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
function createDashboardTestInvitation(Team $team, string $email, array $attributes = []): TeamInvitation
|
||||
{
|
||||
$uuid = $attributes['uuid'] ?? (string) str()->uuid();
|
||||
|
||||
return TeamInvitation::create(array_merge([
|
||||
'team_id' => $team->id,
|
||||
'uuid' => $uuid,
|
||||
'email' => $email,
|
||||
'role' => 'admin',
|
||||
'link' => url('/invitations/'.$uuid),
|
||||
'via' => 'link',
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
beforeEach(function () {
|
||||
Queue::fake();
|
||||
|
||||
InstanceSettings::unguarded(fn () => InstanceSettings::query()->create(['id' => 0]));
|
||||
|
||||
$this->user = User::factory()->create(['email' => 'invited@example.com']);
|
||||
$this->ownTeam = Team::factory()->create();
|
||||
$this->user->teams()->attach($this->ownTeam, ['role' => 'owner']);
|
||||
$this->invitingTeam = Team::factory()->create(['name' => 'Acme Platform']);
|
||||
|
||||
Team::query()->update(['show_boarding' => false]);
|
||||
|
||||
$this->actingAs($this->user);
|
||||
session(['currentTeam' => $this->ownTeam]);
|
||||
});
|
||||
|
||||
it('shows a pending invitation for the user email with a review link', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'Invited@Example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertSee('Acme Platform')
|
||||
->assertSee('invited you as Admin.')
|
||||
->assertSee('Review invitation')
|
||||
->assertSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
|
||||
it('does not show invitations for another email', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'someone-else@example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation')
|
||||
->assertDontSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
|
||||
it('does not show expired invitations and keeps them in place', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
$invitation->forceFill([
|
||||
'created_at' => now()->subDays(config('constants.invitation.link.expiration_days') + 1),
|
||||
])->save();
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation');
|
||||
|
||||
expect(TeamInvitation::query()->whereKey($invitation->id)->exists())->toBeTrue()
|
||||
->and(User::query()->whereKey($this->user->id)->exists())->toBeTrue();
|
||||
});
|
||||
|
||||
it('does not show invitations to a team the user already belongs to', function () {
|
||||
$this->user->teams()->attach($this->invitingTeam, ['role' => 'member']);
|
||||
createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation');
|
||||
});
|
||||
|
||||
it('no longer shows the invitation after it is accepted', function () {
|
||||
$invitation = createDashboardTestInvitation($this->invitingTeam, 'invited@example.com');
|
||||
|
||||
$this->get(route('dashboard'))
|
||||
->assertOk()
|
||||
->assertSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
|
||||
$this->post(route('team.invitation.accept', $invitation->uuid))
|
||||
->assertRedirect(route('team.index'));
|
||||
|
||||
expect($this->user->teams()->whereKey($this->invitingTeam->id)->exists())->toBeTrue();
|
||||
|
||||
$this->actingAs($this->user->fresh());
|
||||
session(['currentTeam' => $this->invitingTeam->fresh()]);
|
||||
|
||||
Livewire::test(Dashboard::class)
|
||||
->assertDontSee('Acme Platform')
|
||||
->assertDontSee('Review invitation')
|
||||
->assertDontSee(route('team.invitation.show', $invitation->uuid), false);
|
||||
});
|
||||
@@ -1,7 +1,11 @@
|
||||
<?php
|
||||
|
||||
use App\Livewire\Project\Application\Domains as ApplicationDomains;
|
||||
use App\Livewire\Project\Application\PreviewDomains;
|
||||
use App\Livewire\Project\Service\Domains;
|
||||
use App\Livewire\Server\DockerImages;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\InstanceSettings;
|
||||
use App\Models\PrivateKey;
|
||||
@@ -36,7 +40,7 @@ beforeEach(function () {
|
||||
$this->server->settings()->update(['is_reachable' => true, 'is_usable' => true]);
|
||||
});
|
||||
|
||||
it('rejects a client update of the service domain rows', function () {
|
||||
it('rejects a client update of the service domain state', function (string $property, mixed $value) {
|
||||
$destination = StandaloneDocker::withoutEvents(fn () => StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $this->server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
@@ -59,8 +63,11 @@ it('rejects a client update of the service domain rows', function () {
|
||||
|
||||
Livewire::test(Domains::class, ['service' => $service->fresh(['applications', 'server'])])
|
||||
->assertSet('domainRows', fn (array $rows): bool => collect($rows)->pluck('url')->all() === ['https://web.example.com'])
|
||||
->set('domainRows', [['url' => 'https://attacker.example.com']]);
|
||||
})->throws(CannotUpdateLockedPropertyException::class);
|
||||
->set($property, $value);
|
||||
})->with([
|
||||
'domain rows' => ['domainRows', [['url' => 'https://attacker.example.com']]],
|
||||
'dns validation' => ['dnsValidationEnabled', false],
|
||||
])->throws(CannotUpdateLockedPropertyException::class);
|
||||
|
||||
it('rejects a client update of the docker image list', function () {
|
||||
$imageId = 'sha256:'.str_repeat('a', 64);
|
||||
@@ -76,3 +83,48 @@ it('rejects a client update of the docker image list', function () {
|
||||
->assertSet('images.0.containers', ['web'])
|
||||
->set('images.0.containers', []);
|
||||
})->throws(CannotUpdateLockedPropertyException::class);
|
||||
|
||||
function createLockedDomainRowsApplication(Server $server, Team $team): Application
|
||||
{
|
||||
$destination = StandaloneDocker::withoutEvents(fn () => StandaloneDocker::firstOrCreate(
|
||||
['server_id' => $server->id, 'network' => 'coolify'],
|
||||
['uuid' => (string) Str::uuid(), 'name' => 'test-docker']
|
||||
));
|
||||
$environment = Environment::factory()->create(['project_id' => Project::factory()->create(['team_id' => $team->id])->id]);
|
||||
|
||||
return Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => $destination->getMorphClass(),
|
||||
'fqdn' => 'https://app.example.com',
|
||||
'build_pack' => 'nixpacks',
|
||||
'ports_exposes' => '3000',
|
||||
]);
|
||||
}
|
||||
|
||||
it('rejects a client update of the application domain rows', function (string $property, mixed $value) {
|
||||
$application = createLockedDomainRowsApplication($this->server, $this->team);
|
||||
|
||||
Livewire::test(ApplicationDomains::class, ['application' => $application->fresh()])
|
||||
->assertSet('domainRows', fn (array $rows): bool => collect($rows)->where('is_suggested', false)->pluck('url')->all() === ['https://app.example.com'])
|
||||
->set($property, $value);
|
||||
})->with([
|
||||
'domain rows' => ['domainRows', [['url' => 'https://app.example.com', 'service' => null, 'dns_status' => 'ok', 'dns_message' => 'faked', 'expected_ip' => null, 'needs_force_add' => true]]],
|
||||
'one row flag' => ['domainRows.0.needs_force_add', true],
|
||||
'forced suggestion index' => ['forceAddSuggestedIndex', 0],
|
||||
'DNS validation flag' => ['dnsValidationEnabled', false],
|
||||
])->throws(CannotUpdateLockedPropertyException::class);
|
||||
|
||||
it('rejects a client update of the preview domain rows', function () {
|
||||
$application = createLockedDomainRowsApplication($this->server, $this->team);
|
||||
$preview = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => 7,
|
||||
'pull_request_html_url' => 'https://github.com/coollabsio/coolify/pull/7',
|
||||
'fqdn' => 'https://pr-7.example.com',
|
||||
]);
|
||||
|
||||
Livewire::test(PreviewDomains::class, ['preview' => $preview])
|
||||
->assertSet('domainRows', fn (array $rows): bool => collect($rows)->pluck('url')->all() === ['https://pr-7.example.com'])
|
||||
->set('domainRows.0.dns_status', 'ok');
|
||||
})->throws(CannotUpdateLockedPropertyException::class);
|
||||
|
||||
@@ -1003,7 +1003,6 @@ it('shows an inherited internal port badge from the coolify service env port', f
|
||||
Livewire::test(Domains::class, ['service' => $this->service->fresh(['applications', 'server'])])
|
||||
->assertSet('domainRows.0.internal_port', 3000)
|
||||
->assertSet('domainRows.0.has_port_override', false)
|
||||
->set('dnsValidationEnabled', false)
|
||||
->call('startEdit', 0)
|
||||
->assertSet('editingDomainParts.port', '3000')
|
||||
->call('updateDomain')
|
||||
|
||||
Reference in New Issue
Block a user