From 8e033c5bc3aa77c0751aea41f0c85cbb8dea10ab Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Tue, 26 May 2026 14:50:29 +0200 Subject: [PATCH] fix(destination): promote networks atomically Wrap destination promotion in a transaction so the main destination swap and additional network updates stay consistent. Add coverage for promoting an owned team network while preserving the previous main destination as an additional network. --- app/Livewire/Project/Shared/Destination.php | 21 +++++++++++-------- .../CrossTeamDestinationAttachTest.php | 15 +++++++++++++ 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 79892d4c6..bab5c59b0 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -8,6 +8,7 @@ use App\Models\Server; use App\Models\StandaloneDocker; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\DB; use Livewire\Component; use Visus\Cuid2\Cuid2; @@ -115,15 +116,17 @@ public function promote(int $network_id, int $server_id) $network = StandaloneDocker::ownedByCurrentTeam()->where('server_id', $server->id)->findOrFail($network_id); $this->authorize('update', $this->resource); - $main_destination = $this->resource->destination; - $this->resource->update([ - 'destination_id' => $network->id, - 'destination_type' => StandaloneDocker::class, - ]); - $this->resource->additional_networks()->detach($network->id, ['server_id' => $server->id]); - $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); - $this->refreshServers(); - $this->resource->refresh(); + DB::transaction(function () use ($network, $server) { + $main_destination = $this->resource->destination; + $this->resource->update([ + 'destination_id' => $network->id, + 'destination_type' => StandaloneDocker::class, + ]); + $this->resource->additional_networks()->detach($network->id, ['server_id' => $server->id]); + $this->resource->additional_networks()->attach($main_destination->id, ['server_id' => $main_destination->server->id]); + $this->refreshServers(); + $this->resource->refresh(); + }); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/tests/Feature/CrossTeamDestinationAttachTest.php b/tests/Feature/CrossTeamDestinationAttachTest.php index 3bd151152..e90c8334c 100644 --- a/tests/Feature/CrossTeamDestinationAttachTest.php +++ b/tests/Feature/CrossTeamDestinationAttachTest.php @@ -143,4 +143,19 @@ expect($this->applicationA->fresh()->destination_id)->toBe($originalDestinationId); }); + + test('can promote own team network and preserve previous main as additional network', function () { + $this->applicationA->additional_networks()->attach($this->destinationA2->id, ['server_id' => $this->serverA2->id]); + + Livewire::test(Destination::class, ['resource' => $this->applicationA]) + ->call('promote', $this->destinationA2->id, $this->serverA2->id); + + $application = $this->applicationA->fresh(); + $additional = $application->additional_networks; + + expect($application->destination_id)->toBe($this->destinationA2->id); + expect($additional)->toHaveCount(1); + expect($additional->first()->id)->toBe($this->destinationA->id); + expect($additional->first()->pivot->server_id)->toBe($this->serverA->id); + }); });