From 3cb78f79c7faf6ed4c9bdc4cd8573a64794ed41d Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Fri, 26 Jun 2026 13:03:03 +0100 Subject: [PATCH] workign tests --- .../Livewire/HelpdeskDashboardWidget.php | 19 ++++++++++-- Modules/Helpdesk/Livewire/TicketIndex.php | 5 ++-- Modules/Helpdesk/Models/Ticket.php | 1 - .../views/livewire/dashboard-widget.blade.php | 1 - ...TicketIndexDirectUserNPlusOneFilesTest.php | 13 +++++---- .../TicketIndexUserNPlusOneFilesTest.php | 10 +++---- .../Feature/TicketUserNPlusOneFilesTest.php | 29 ++++++++++++++----- app/Providers/AppServiceProvider.php | 9 ++++++ 8 files changed, 62 insertions(+), 25 deletions(-) diff --git a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php index 29a661a7..f28fd4e9 100644 --- a/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php +++ b/Modules/Helpdesk/Livewire/HelpdeskDashboardWidget.php @@ -15,7 +15,18 @@ final class HelpdeskDashboardWidget extends Component public function render(): View { $user = Auth::user(); - $base = Ticket::query()->visibleTo($user); + $base = Ticket::query(); + + if ($user->helpdesk_admin ?? false) { + // no filter + } elseif ($user->helpdesk_agent ?? false) { + $base->where(function ($q) use ($user) { + $q->where('agent_id', $user->id) + ->orWhere('user_id', $user->id); + }); + } else { + $base->where('user_id', $user->id); + } $totalCount = (clone $base)->count(); $openCount = (clone $base)->whereNull('completed_at')->count(); $closedCount = (clone $base)->whereNotNull('completed_at')->count(); @@ -31,7 +42,11 @@ final class HelpdeskDashboardWidget extends Component 'closedPercent' => $closedPercent, 'openPercent' => $openPercent, 'recentCommits' => GitCommit::query()->with(['repository', 'ticket'])->latest('committed_at')->limit(6)->get(), - 'recentTickets' => (clone $base)->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority'])->latest('last_activity_at')->limit(5)->get(), + 'recentTickets' => (clone $base) + ->with(['status', 'priority']) + ->latest('last_activity_at') + ->limit(5) + ->get(), ]); } } diff --git a/Modules/Helpdesk/Livewire/TicketIndex.php b/Modules/Helpdesk/Livewire/TicketIndex.php index 751cbe11..c4d0a84d 100644 --- a/Modules/Helpdesk/Livewire/TicketIndex.php +++ b/Modules/Helpdesk/Livewire/TicketIndex.php @@ -4,12 +4,12 @@ declare(strict_types=1); namespace Modules\Helpdesk\Livewire; +use App\Models\User; use Illuminate\Contracts\View\View; use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\Auth; -use App\Models\User; use Livewire\Attributes\Url; use Livewire\Component; use Livewire\WithPagination; @@ -132,7 +132,7 @@ final class TicketIndex extends Component ->whereColumn('users.id', 'helpdesk_tickets.agent_id') ->limit(1), ]) - ->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team']) + ->with(['status', 'priority', 'category', 'project', 'team']) ->withCount('attachments') ->visibleTo($user) ->when($this->scope === 'open', static fn (Builder $query): Builder => $query->whereNull('completed_at')) @@ -148,6 +148,5 @@ final class TicketIndex extends Component }) ->latest('last_activity_at') ->paginate(15); - } } diff --git a/Modules/Helpdesk/Models/Ticket.php b/Modules/Helpdesk/Models/Ticket.php index b2c21c62..01a91c98 100644 --- a/Modules/Helpdesk/Models/Ticket.php +++ b/Modules/Helpdesk/Models/Ticket.php @@ -27,7 +27,6 @@ final class Ticket extends Model protected $table = 'helpdesk_tickets'; - /** @var list */ protected $with = ['user']; /** @var list */ diff --git a/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php b/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php index 718de3ce..35907081 100644 --- a/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php +++ b/Modules/Helpdesk/Resources/views/livewire/dashboard-widget.blade.php @@ -74,7 +74,6 @@ View all tickets -
@forelse ($recentTickets as $ticket) diff --git a/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php index 11f5db6b..b66ea794 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketIndexDirectUserNPlusOneFilesTest.php @@ -4,16 +4,19 @@ declare(strict_types=1); namespace Modules\Helpdesk\Tests\Feature; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; final class TicketIndexDirectUserNPlusOneFilesTest extends TestCase { - public function test_ticket_index_eager_loads_direct_user_relation(): void + public function test_ticket_index_eager_loads_required_relations(): void { - $source = file_get_contents(dirname(__DIR__, 2) . '/Livewire/TicketIndex.php'); + $source = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php'); self::assertIsString($source); - self::assertStringContainsString("->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team'])", $source); - self::assertStringNotContainsString("->with(['owner', 'agent', 'status', 'priority', 'category', 'project', 'team'])", $source); + + self::assertStringContainsString("->with(['status', 'priority', 'category', 'project', 'team'])", $source); + self::assertStringNotContainsString("'user:id,name,email'", $source); + self::assertStringNotContainsString("'owner:id,name,email'", $source); + self::assertStringNotContainsString("'agent:id,name,email'", $source); } } diff --git a/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php index 64894791..baf8051f 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketIndexUserNPlusOneFilesTest.php @@ -4,14 +4,14 @@ declare(strict_types=1); namespace Modules\Helpdesk\Tests\Feature; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; final class TicketIndexUserNPlusOneFilesTest extends TestCase { - public function test_ticket_index_uses_scalar_agent_name_and_eager_loads_ticket_users(): void + public function test_ticket_index_uses_scalar_agent_name(): void { - $component = file_get_contents(dirname(__DIR__, 2) . '/Livewire/TicketIndex.php'); - $view = file_get_contents(dirname(__DIR__, 2) . '/Resources/views/livewire/ticket-index.blade.php'); + $component = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php'); + $view = file_get_contents(dirname(__DIR__, 2).'/Resources/views/livewire/ticket-index.blade.php'); self::assertIsString($component); self::assertIsString($view); @@ -19,7 +19,7 @@ final class TicketIndexUserNPlusOneFilesTest extends TestCase self::assertStringContainsString("->select('helpdesk_tickets.*')", $component); self::assertStringContainsString("'agent_name' => User::query()", $component); self::assertStringContainsString("->whereColumn('users.id', 'helpdesk_tickets.agent_id')", $component); - self::assertStringContainsString("->with(['user:id,name,email', 'owner:id,name,email', 'agent:id,name,email', 'status', 'priority', 'category', 'project', 'team'])", $component); + self::assertStringContainsString('$ticket->agent_name ?? \'Unassigned\'', $view); self::assertStringNotContainsString('$ticket->agent?->name ?? \'Unassigned\'', $view); } diff --git a/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php index bf8df6e6..ada3fee8 100644 --- a/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php +++ b/Modules/Helpdesk/Tests/Feature/TicketUserNPlusOneFilesTest.php @@ -4,22 +4,35 @@ declare(strict_types=1); namespace Modules\Helpdesk\Tests\Feature; -use Tests\TestCase; +use PHPUnit\Framework\TestCase; final class TicketUserNPlusOneFilesTest extends TestCase { - public function test_ticket_collection_queries_eager_load_user_owner_and_agent(): void + public function test_ticket_queries_only_eager_load_required_relations(): void { - $index = file_get_contents(dirname(__DIR__, 2) . '/Livewire/TicketIndex.php'); - $kanban = file_get_contents(dirname(__DIR__, 2) . '/Livewire/KanbanBoard.php'); - $dashboard = file_get_contents(dirname(__DIR__, 2) . '/Livewire/HelpdeskDashboardWidget.php'); + $index = file_get_contents(dirname(__DIR__, 2).'/Livewire/TicketIndex.php'); + $kanban = file_get_contents(dirname(__DIR__, 2).'/Livewire/KanbanBoard.php'); + $dashboard = file_get_contents(dirname(__DIR__, 2).'/Livewire/HelpdeskDashboardWidget.php'); self::assertIsString($index); self::assertIsString($kanban); self::assertIsString($dashboard); - self::assertStringContainsString("'user:id,name,email', 'owner:id,name,email', 'agent:id,name,email'", $index); - self::assertStringContainsString("'user:id,name,email', 'owner:id,name,email', 'agent:id,name,email'", $kanban); - self::assertStringContainsString("'user:id,name,email', 'owner:id,name,email', 'agent:id,name,email'", $dashboard); + // Index deliberately avoids loading user relations. + self::assertStringNotContainsString("'user:id,name,email'", $index); + self::assertStringNotContainsString("'owner:id,name,email'", $index); + self::assertStringNotContainsString("'agent:id,name,email'", $index); + + self::assertStringContainsString("'owner:id,name,email'", $kanban); + + self::assertStringContainsString("'agent:id,name,email'", $kanban); + +// Dashboard intentionally only loads the relations it renders. + + self::assertStringContainsString("->with(['status', 'priority'])", $dashboard); + + self::assertStringNotContainsString("'owner:id,name,email'", $dashboard); + + self::assertStringNotContainsString("'agent:id,name,email'", $dashboard); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4ac9703a..aa062008 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -3,6 +3,8 @@ namespace App\Providers; use Carbon\CarbonImmutable; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\LazyLoadingViolationException; use Illuminate\Support\Facades\Date; use Illuminate\Support\Facades\DB; use Illuminate\Support\ServiceProvider; @@ -24,6 +26,13 @@ class AppServiceProvider extends ServiceProvider public function boot(): void { $this->configureDefaults(); + Model::preventLazyLoading(! app()->isProduction()); + Model::handleLazyLoadingViolationUsing(function ($model, $relation) { + throw new LazyLoadingViolationException( + $model, + $relation, + ); + }); } /**