diff --git a/app/Filament/Resources/UserResource/RelationManagers/HostsRelationManager.php b/app/Filament/Resources/UserResource/RelationManagers/HostsRelationManager.php index 580f2f7..d8e63d3 100644 --- a/app/Filament/Resources/UserResource/RelationManagers/HostsRelationManager.php +++ b/app/Filament/Resources/UserResource/RelationManagers/HostsRelationManager.php @@ -38,7 +38,7 @@ public function form(Schema $schema): Schema // Obtener hosts que NO están ya asignados a este usuario $assignedHostIds = $this->getOwnerRecord()->hosts()->pluck('hosts.id')->toArray(); - return Host::whereNotIn('id', $assignedHostIds) + return Host::whereNotIn('hosts.id', $assignedHostIds) ->get() ->mapWithKeys(fn ($host) => [ $host->id => "{$host->fqdn} ({$host->ip}) - {$host->panel->getLabel()}", @@ -102,7 +102,7 @@ public function table(Table $table): Table // Solo mostrar hosts que NO están ya asignados a este usuario $assignedHostIds = $this->getOwnerRecord()->hosts()->pluck('hosts.id')->toArray(); - return $query->whereNotIn('id', $assignedHostIds); + return $query->whereNotIn('hosts.id', $assignedHostIds); }) ->recordSelectSearchColumns(['fqdn', 'alias', 'ip']) ->recordTitle(fn ($record) => "{$record->fqdn} ({$record->ip}) - {$record->panel->getLabel()}"), @@ -137,7 +137,7 @@ public function table(Table $table): Table ->recordSelectOptionsQuery(function ($query) { $assignedHostIds = $this->getOwnerRecord()->hosts()->pluck('hosts.id')->toArray(); - return $query->whereNotIn('id', $assignedHostIds); + return $query->whereNotIn('hosts.id', $assignedHostIds); }) ->recordSelectSearchColumns(['fqdn', 'alias', 'ip']) ->recordTitle(fn ($record) => "{$record->fqdn} ({$record->ip}) - {$record->panel->getLabel()}"), diff --git a/tests/Feature/Filament/UserResourceRelationManagersTest.php b/tests/Feature/Filament/UserResourceRelationManagersTest.php index 3de7486..818b7f8 100644 --- a/tests/Feature/Filament/UserResourceRelationManagersTest.php +++ b/tests/Feature/Filament/UserResourceRelationManagersTest.php @@ -1,7 +1,11 @@ fresh()->hasAccessToHost($host->id))->toBeTrue(); }); +test('server assignment action loads for a user that already has hosts (regression: ambiguous column id)', function () { + // Arrange - admin session (OTP verified) + $admin = User::factory()->admin()->create(); + $this->actingAs($admin); + session()->put('admin_otp_verified', true); + session()->put('admin_otp_user_id', $admin->id); + session()->put('admin_otp_verified_at', now()->timestamp); + + // The owner user ALREADY has a host assigned, so $assignedHostIds is non-empty + // and the whereNotIn(...) clause is actually emitted. With an unqualified 'id' + // over the BelongsToMany base query (which joins user_host_permissions), + // this triggers "ambiguous column name: id" before the fix. + $owner = User::factory()->create(['parent_user_id' => null]); + $assignedHost = Host::factory()->create(); + $owner->hosts()->attach($assignedHost->id, ['is_active' => true]); + + // Another host available to be assigned + $availableHost = Host::factory()->create(); + + // Act - run the full "Asignar Servidor" flow: selecting a host forces Filament + // to resolve its option label against the record-select options query, which is + // where the unqualified whereNotIn('id', ...) over the joined base query blows up + // with "ambiguous column name: id" (the production symptom). + livewire(HostsRelationManager::class, [ + 'ownerRecord' => $owner, + 'pageClass' => EditUser::class, + ]) + ->callTableAction('attach', data: [ + 'recordId' => $availableHost->id, + 'is_active' => true, + ]) + ->assertHasNoTableActionErrors(); + + // Assert - the host was actually attached (flow completed end to end) + expect($owner->fresh()->hosts()->whereKey($availableHost->id)->exists())->toBeTrue(); +}); + test('unauthorized users cannot access admin panel', function () { // Arrange - Create users $regularUser = User::factory()->create(['is_admin' => false]);