From ddaef93768565fc96f3c4cab3e8466e2f66cf433 Mon Sep 17 00:00:00 2001 From: dualfroz Date: Sat, 10 Oct 2026 11:52:35 +0200 Subject: [PATCH] Move the primary allocation when it is removed from a server --- .../AllocationsRelationManager.php | 4 +- .../Servers/NetworkAllocationController.php | 4 ++ app/Models/Server.php | 7 +++- tests/Filament/Admin/ServerCrudTest.php | 40 +++++++++++++++++++ .../Allocation/DeleteAllocationTest.php | 19 +++++++++ tests/Unit/Models/ServerTest.php | 12 ++++++ 6 files changed, 82 insertions(+), 4 deletions(-) diff --git a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php index 3b3bf0c..a54f66f 100644 --- a/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php +++ b/app/Filament/Admin/Resources/Servers/RelationManagers/AllocationsRelationManager.php @@ -117,7 +117,7 @@ class AllocationsRelationManager extends RelationManager ->after(function (Allocation $allocation) { $allocation->update(Allocation::RELEASE_ATTRIBUTES); - if (!$this->getOwnerRecord()->allocation_id) { + if (!$this->getOwnerRecord()->allocation_id || $this->getOwnerRecord()->allocation_id === $allocation->id) { $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); } }), @@ -128,7 +128,7 @@ class AllocationsRelationManager extends RelationManager ->after(function (Collection $records) { Allocation::whereIn('id', $records->pluck('id'))->update(Allocation::RELEASE_ATTRIBUTES); - if (!$this->getOwnerRecord()->allocation_id) { + if (!$this->getOwnerRecord()->allocation_id || $records->contains('id', $this->getOwnerRecord()->allocation_id)) { $this->getOwnerRecord()->update(['allocation_id' => $this->getOwnerRecord()->allocations()->first()?->id]); } }), diff --git a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php index de38e2a..ed1bfa9 100644 --- a/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php +++ b/app/Http/Controllers/Api/Client/Servers/NetworkAllocationController.php @@ -139,6 +139,10 @@ class NetworkAllocationController extends ClientApiController Allocation::query()->where('id', $allocation->id)->update(Allocation::RELEASE_ATTRIBUTES); + if ($allocation->id === $server->allocation_id) { + $server->update(['allocation_id' => $server->allocations()->first()?->id]); + } + Activity::event('server:allocation.delete') ->subject($allocation) ->property('allocation', $allocation->address) diff --git a/app/Models/Server.php b/app/Models/Server.php index 1ed2e15..0be50c1 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -231,11 +231,14 @@ class Server extends Model implements HasAvatar, Validatable */ public function getAllocationMappings(): array { - if (!$this->allocation) { + $allocations = $this->allocations->where('node_id', $this->node_id); + + // Daemon expects a map here, and an empty array would be encoded as a JSON list. + if (!$this->allocation || $allocations->isEmpty()) { return ['' => []]; } - return $this->allocations->where('node_id', $this->node_id)->groupBy('ip')->map(function ($item) { + return $allocations->groupBy('ip')->map(function ($item) { return $item->pluck('port'); })->toArray(); } diff --git a/tests/Filament/Admin/ServerCrudTest.php b/tests/Filament/Admin/ServerCrudTest.php index b199e26..e41c327 100644 --- a/tests/Filament/Admin/ServerCrudTest.php +++ b/tests/Filament/Admin/ServerCrudTest.php @@ -3,6 +3,7 @@ use App\Enums\RolePermissionModels; use App\Filament\Admin\Resources\Servers\Pages\CreateServer; use App\Filament\Admin\Resources\Servers\Pages\EditServer; +use App\Filament\Admin\Resources\Servers\RelationManagers\AllocationsRelationManager; use App\Models\Allocation; use App\Models\Egg; use App\Models\Node; @@ -88,6 +89,45 @@ it('can delete a server', function () { $this->assertDatabaseMissing('servers', ['id' => $server->id]); }); +it('moves the primary allocation when it is removed from a server', function () { + $server = createServerModel(); + $primary = $server->allocation; + $other = Allocation::factory()->forServer($server)->create(); + + livewire(AllocationsRelationManager::class, ['ownerRecord' => $server, 'pageClass' => EditServer::class]) + ->callAction(TestAction::make('dissociate')->table($primary)); + + expect($primary->refresh()->server_id)->toBeNull() + ->and($server->refresh()->allocation_id)->toBe($other->id); +}); + +it('clears the primary allocation when the last allocation is removed from a server', function () { + $server = createServerModel(); + $primary = $server->allocation; + + livewire(AllocationsRelationManager::class, ['ownerRecord' => $server, 'pageClass' => EditServer::class]) + ->callAction(TestAction::make('dissociate')->table($primary)); + + $server->refresh(); + + expect($server->allocation_id)->toBeNull() + ->and(json_encode($server->getAllocationMappings()))->toBe('{"":[]}'); +}); + +it('moves the primary allocation when it is removed from a server in bulk', function () { + $server = createServerModel(); + $primary = $server->allocation; + [$removed, $kept] = Allocation::factory()->count(2)->forServer($server)->create(); + + livewire(AllocationsRelationManager::class, ['ownerRecord' => $server, 'pageClass' => EditServer::class]) + ->selectTableRecords([$primary->id, $removed->id]) + ->callAction(TestAction::make('dissociate')->table()->bulk()); + + expect($primary->refresh()->server_id)->toBeNull() + ->and($removed->refresh()->server_id)->toBeNull() + ->and($server->refresh()->allocation_id)->toBe($kept->id); +}); + it('non root admin without permission cannot create servers', function () { $role = Role::factory()->create(['name' => 'Egg Viewer', 'guard_name' => 'web']); // Egg permission is on purpose, we check the wrong permissions. diff --git a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php b/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php index 271d30a..d858218 100644 --- a/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php +++ b/tests/Integration/Api/Client/Server/Allocation/DeleteAllocationTest.php @@ -57,6 +57,25 @@ class DeleteAllocationTest extends ClientApiIntegrationTestCase $this->actingAs($user)->deleteJson($this->link($allocation))->assertStatus(Response::HTTP_NO_CONTENT); $this->assertDatabaseHas('allocations', ['id' => $allocation->id, 'server_id' => null, 'notes' => null]); + $this->assertNull($server->refresh()->allocation_id); + } + + /** + * Test that deleting the primary allocation makes one of the remaining allocations + * the new primary allocation for the server. + */ + public function test_primary_allocation_is_moved_when_deleted_from_server(): void + { + /** @var Server $server */ + [$user, $server] = $this->generateTestAccount(); + $server->update(['allocation_limit' => 2]); + + $allocation = $server->allocation; + $other = Allocation::factory()->forServer($server)->create(); + + $this->actingAs($user)->deleteJson($this->link($allocation))->assertStatus(Response::HTTP_NO_CONTENT); + + $this->assertSame($other->id, $server->refresh()->allocation_id); } /** diff --git a/tests/Unit/Models/ServerTest.php b/tests/Unit/Models/ServerTest.php index b7c2722..3700c9a 100644 --- a/tests/Unit/Models/ServerTest.php +++ b/tests/Unit/Models/ServerTest.php @@ -2,9 +2,11 @@ namespace App\Tests\Unit\Models; +use App\Models\Allocation; use App\Models\Node; use App\Models\Server; use App\Tests\TestCase; +use Illuminate\Database\Eloquent\Collection; use PHPUnit\Framework\Attributes\DataProvider; class ServerTest extends TestCase @@ -44,6 +46,16 @@ class ServerTest extends TestCase $this->assertSame('sftp://user%20name.abcdefgh@node.example.com:2022/data/', $server->getSftpUrl('user name', '/data')); } + public function test_allocation_mappings_stay_an_object_when_the_server_has_no_allocations(): void + { + $server = new Server(); + $server->node_id = 1; + $server->setRelation('allocation', new Allocation(['node_id' => 1, 'ip' => '127.0.0.1', 'port' => 25565])); + $server->setRelation('allocations', new Collection()); + + $this->assertSame('{"":[]}', json_encode($server->getAllocationMappings())); + } + private function serverWithNode(string $fqdn = 'node.example.com', ?string $sftpAlias = null): Server { $node = new Node();