From e2c758471eac041452e03fd7aef08dd6e0ab4289 Mon Sep 17 00:00:00 2001 From: Emily Rowan Date: Sun, 5 Jul 2026 21:22:01 +0100 Subject: [PATCH 1/5] feat: add server-side sorting to tickets index controller Adds whitelisted order/order_direction query param handling to TicketController@index, mirroring the existing admin tickets sort convention (default: newest event first). Covers reference, type, seat, and event columns with join-based sorting and a stable tickets.id tiebreaker; invalid inputs fall back to the default. --- app/Http/Controllers/TicketController.php | 41 +++++- .../Http/Controllers/TicketControllerTest.php | 132 ++++++++++++++++++ 2 files changed, 172 insertions(+), 1 deletion(-) diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 9f4ae075..4d7f403f 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -21,13 +21,52 @@ public function index(Request $request) }); } + $sortable = ['reference', 'type', 'seat', 'event']; + $order = in_array($request->input('order'), $sortable, true) + ? $request->input('order') + : 'event'; + + $direction = strtolower((string) $request->input('order_direction')); + if (!in_array($direction, ['asc', 'desc'], true)) { + $direction = $order === 'event' ? 'desc' : 'asc'; + } + + switch ($order) { + case 'reference': + $query->orderBy('tickets.reference', $direction); + break; + case 'type': + $query->join('ticket_types', 'tickets.ticket_type_id', '=', 'ticket_types.id') + ->orderBy('ticket_types.name', $direction); + break; + case 'seat': + $query->leftJoin('seats', 'seats.ticket_id', '=', 'tickets.id') + ->orderBy('seats.row', $direction) + ->orderBy('seats.number', $direction); + break; + case 'event': + default: + $query->join('events', 'tickets.event_id', '=', 'events.id') + ->orderBy('events.starts_at', $direction); + break; + } + + $params = [ + 'order' => $order, + 'order_direction' => $direction, + ]; + $tickets = $query->with(['event' => function ($query) { $query->orderBy('starts_at', 'DESC'); }, 'type', 'seat']) - ->paginate(); + ->select('tickets.*') + ->orderBy('tickets.id') + ->paginate() + ->appends($params); return view('tickets.index', [ 'tickets' => $tickets, + 'params' => $params, ]); } diff --git a/tests/Unit/app/Http/Controllers/TicketControllerTest.php b/tests/Unit/app/Http/Controllers/TicketControllerTest.php index 0cafb240..3ae81886 100644 --- a/tests/Unit/app/Http/Controllers/TicketControllerTest.php +++ b/tests/Unit/app/Http/Controllers/TicketControllerTest.php @@ -5,8 +5,10 @@ use App\Http\Controllers\TicketController; use App\Http\Requests\TicketTransferRequest; use App\Models\Event; +use App\Models\Seat; use App\Models\Setting; use App\Models\Ticket; +use App\Models\TicketType; use App\Models\User; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Http\Request; @@ -136,4 +138,134 @@ public function testTransferWhenTransfersDisabledReturnsError() $this->assertStringContainsString('/tickets', $response->getTargetUrl()); $this->assertNotEmpty($response->getSession()->get('errorMessage')); } + + public function testIndexDefaultsToNewestEventFirst() + { + $user = User::factory()->create(); + $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]); + $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]); + $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]); + $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]); + + $tickets = $this->indexResponse($user)->getData()['tickets']; + + $this->assertSame([$newTicket->id, $oldTicket->id], $tickets->pluck('id')->all()); + } + + public function testIndexPassesResolvedSortParamsToView() + { + $user = User::factory()->create(); + + $data = $this->indexResponse($user)->getData(); + + $this->assertSame(['order' => 'event', 'order_direction' => 'desc'], $data['params']); + } + + public function testIndexSortsByReference() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + $a = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); + $z = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']); + + $asc = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'asc'])->getData()['tickets']; + $this->assertSame([$a->id, $z->id], $asc->pluck('id')->all()); + + $desc = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'desc'])->getData()['tickets']; + $this->assertSame([$z->id, $a->id], $desc->pluck('id')->all()); + } + + public function testIndexSortsByType() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + $typeA = TicketType::factory()->create(['name' => 'AAA Type']); + $typeZ = TicketType::factory()->create(['name' => 'ZZZ Type']); + $ticketA = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'ticket_type_id' => $typeA->id]); + $ticketZ = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'ticket_type_id' => $typeZ->id]); + + $asc = $this->indexResponse($user, ['order' => 'type', 'order_direction' => 'asc'])->getData()['tickets']; + $this->assertSame([$ticketA->id, $ticketZ->id], $asc->pluck('id')->all()); + + $desc = $this->indexResponse($user, ['order' => 'type', 'order_direction' => 'desc'])->getData()['tickets']; + $this->assertSame([$ticketZ->id, $ticketA->id], $desc->pluck('id')->all()); + } + + public function testIndexSortsBySeat() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + $front = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]); + $back = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]); + Seat::factory()->create(['ticket_id' => $front->id, 'row' => 1, 'number' => 1]); + Seat::factory()->create(['ticket_id' => $back->id, 'row' => 9, 'number' => 9]); + + $asc = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'asc'])->getData()['tickets']; + $this->assertSame([$front->id, $back->id], $asc->pluck('id')->all()); + + $desc = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'desc'])->getData()['tickets']; + $this->assertSame([$back->id, $front->id], $desc->pluck('id')->all()); + } + + public function testIndexSortsByEventStartsAt() + { + $user = User::factory()->create(); + $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]); + $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]); + $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]); + $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]); + + $asc = $this->indexResponse($user, ['order' => 'event', 'order_direction' => 'asc'])->getData()['tickets']; + $this->assertSame([$oldTicket->id, $newTicket->id], $asc->pluck('id')->all()); + } + + public function testIndexInvalidOrderFallsBackToNewestEventFirst() + { + $user = User::factory()->create(); + $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]); + $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]); + $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]); + $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]); + + $tickets = $this->indexResponse($user, ['order' => 'bogus', 'order_direction' => 'desc'])->getData()['tickets']; + + $this->assertSame([$newTicket->id, $oldTicket->id], $tickets->pluck('id')->all()); + } + + public function testIndexInvalidDirectionFallsBackToAscending() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + $a = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); + $z = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']); + + $tickets = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'sideways'])->getData()['tickets']; + + $this->assertSame([$a->id, $z->id], $tickets->pluck('id')->all()); + } + + public function testIndexPaginationPreservesSort() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); + + $tickets = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'asc'])->getData()['tickets']; + $url = $tickets->url(1); + + $this->assertStringContainsString('order=reference', $url); + $this->assertStringContainsString('order_direction=asc', $url); + } + + private function indexResponse(User $user, array $query = []): View + { + $this->actingAs($user); + $controller = new TicketController(); + $request = Request::create('/', 'GET', $query); + $request->setUserResolver(function () use ($user) { + return $user; + }); + + return $controller->index($request); + } } From 89d4d8d59374b9ace3af8861349bc425befb7f16 Mon Sep 17 00:00:00 2001 From: Emily Rowan Date: Sun, 5 Jul 2026 21:40:02 +0100 Subject: [PATCH 2/5] feat: make tickets index column headers sortable Reuse the existing partials/_sortheader partial for the Reference, Type, Seat, and Event column headers so they become clickable sort links (Event defaults to desc on first click for newest-first). Adds an HTTP feature test proving the headers render sort links and that rows are ordered accordingly. --- resources/views/tickets/index.blade.php | 8 ++--- tests/Feature/TicketSortingTest.php | 46 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 tests/Feature/TicketSortingTest.php diff --git a/resources/views/tickets/index.blade.php b/resources/views/tickets/index.blade.php index 313866b0..15867162 100644 --- a/resources/views/tickets/index.blade.php +++ b/resources/views/tickets/index.blade.php @@ -27,10 +27,10 @@ - - - - + + + + diff --git a/tests/Feature/TicketSortingTest.php b/tests/Feature/TicketSortingTest.php new file mode 100644 index 00000000..5dd75d61 --- /dev/null +++ b/tests/Feature/TicketSortingTest.php @@ -0,0 +1,46 @@ +create(['first_login' => false]); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); + + $response = $this->actingAs($user)->get(route('tickets.index')); + + // assertOk should hold even BEFORE the view change (plain-text headers still render 200). + // If this fails before you touch the view, it is an environment/layout problem to resolve first, + // not part of this feature. + $response->assertOk(); + + // This is the real red -> green assertion for this task: the Reference header must link with order=reference. + // On the default request the pagination links use order=event, so this string only appears once the + // Reference sort header exists. + $response->assertSee('order=reference', false); + } + + public function testTicketsPageOrdersRowsByReferenceAscending() + { + $user = User::factory()->create(['first_login' => false]); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); + Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']); + + $response = $this->actingAs($user)->get(route('tickets.index', ['order' => 'reference', 'order_direction' => 'asc'])); + + $response->assertOk(); + $response->assertSeeInOrder(['AAA-001', 'ZZZ-999']); + } +} From 7b8dfd8c85d1ab9fb07b56b624a3d92f4c7f7488 Mon Sep 17 00:00:00 2001 From: Emily Rowan Date: Sun, 5 Jul 2026 21:48:32 +0100 Subject: [PATCH 3/5] test: guard seat sort keeps unseated tickets --- .../Http/Controllers/TicketControllerTest.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/Unit/app/Http/Controllers/TicketControllerTest.php b/tests/Unit/app/Http/Controllers/TicketControllerTest.php index 3ae81886..441cca67 100644 --- a/tests/Unit/app/Http/Controllers/TicketControllerTest.php +++ b/tests/Unit/app/Http/Controllers/TicketControllerTest.php @@ -207,6 +207,29 @@ public function testIndexSortsBySeat() $this->assertSame([$back->id, $front->id], $desc->pluck('id')->all()); } + public function testIndexSeatSortKeepsUnseatedTickets() + { + $user = User::factory()->create(); + $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); + + $frontRow = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]); + $backRow = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]); + $unseated = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id]); + + Seat::factory()->create(['ticket_id' => $frontRow->id, 'row' => 1, 'number' => 1, 'label' => '1-1']); + Seat::factory()->create(['ticket_id' => $backRow->id, 'row' => 2, 'number' => 5, 'label' => '2-5']); + + $tickets = $this->indexResponse($user, ['order' => 'seat', 'order_direction' => 'asc'])->getData()['tickets']; + $ids = $tickets->pluck('id')->all(); + + $this->assertCount(3, $tickets); + $this->assertContains($unseated->id, $ids); + + $frontPosition = array_search($frontRow->id, $ids); + $backPosition = array_search($backRow->id, $ids); + $this->assertLessThan($backPosition, $frontPosition); + } + public function testIndexSortsByEventStartsAt() { $user = User::factory()->create(); From 42b450ac003d6dfe6dcc3f52732abb55211f78f9 Mon Sep 17 00:00:00 2001 From: Emily Rowan Date: Sat, 11 Jul 2026 03:04:46 +0100 Subject: [PATCH 4/5] refactor: use FormRequest and Eloquent aggregates for ticket sorting Address PR review feedback: - Move sort param validation into TicketIndexRequest FormRequest, so the controller just reads the validated value. Invalid/missing params are coerced to defaults in prepareForValidation so a bad query string still renders the page rather than 422ing. - Replace explicit joins and select('tickets.*') with withAggregate() correlated subqueries; the ORM selects tickets.* and orders by the related column without manual joins. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Http/Controllers/TicketController.php | 40 +++++-------- app/Http/Requests/TicketIndexRequest.php | 58 +++++++++++++++++++ .../Http/Controllers/TicketControllerTest.php | 17 ++---- 3 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 app/Http/Requests/TicketIndexRequest.php diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 4d7f403f..90a1b78c 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers; +use App\Http\Requests\TicketIndexRequest; use App\Http\Requests\TicketTransferRequest; use App\Models\Setting; use App\Models\Ticket; @@ -9,8 +10,11 @@ class TicketController extends Controller { - public function index(Request $request) + public function index(TicketIndexRequest $request) { + $order = $request->validated('order'); + $direction = $request->validated('order_direction'); + $query = $request ->user() ->tickets(); @@ -21,33 +25,24 @@ public function index(Request $request) }); } - $sortable = ['reference', 'type', 'seat', 'event']; - $order = in_array($request->input('order'), $sortable, true) - ? $request->input('order') - : 'event'; - - $direction = strtolower((string) $request->input('order_direction')); - if (!in_array($direction, ['asc', 'desc'], true)) { - $direction = $order === 'event' ? 'desc' : 'asc'; - } - switch ($order) { case 'reference': - $query->orderBy('tickets.reference', $direction); + $query->orderBy('reference', $direction); break; case 'type': - $query->join('ticket_types', 'tickets.ticket_type_id', '=', 'ticket_types.id') - ->orderBy('ticket_types.name', $direction); + $query->withAggregate('type', 'name') + ->orderBy('type_name', $direction); break; case 'seat': - $query->leftJoin('seats', 'seats.ticket_id', '=', 'tickets.id') - ->orderBy('seats.row', $direction) - ->orderBy('seats.number', $direction); + $query->withAggregate('seat', 'row') + ->withAggregate('seat', 'number') + ->orderBy('seat_row', $direction) + ->orderBy('seat_number', $direction); break; case 'event': default: - $query->join('events', 'tickets.event_id', '=', 'events.id') - ->orderBy('events.starts_at', $direction); + $query->withAggregate('event', 'starts_at') + ->orderBy('event_starts_at', $direction); break; } @@ -56,11 +51,8 @@ public function index(Request $request) 'order_direction' => $direction, ]; - $tickets = $query->with(['event' => function ($query) { - $query->orderBy('starts_at', 'DESC'); - }, 'type', 'seat']) - ->select('tickets.*') - ->orderBy('tickets.id') + $tickets = $query->with(['event', 'type', 'seat']) + ->orderBy('id') ->paginate() ->appends($params); diff --git a/app/Http/Requests/TicketIndexRequest.php b/app/Http/Requests/TicketIndexRequest.php new file mode 100644 index 00000000..8f75b51e --- /dev/null +++ b/app/Http/Requests/TicketIndexRequest.php @@ -0,0 +1,58 @@ + + */ + public const SORTABLE = ['reference', 'type', 'seat', 'event']; + + /** + * Determine if the user is authorized to make this request. + */ + public function authorize(): bool + { + return true; + } + + /** + * Get the validation rules that apply to the request. + * + * @return array|string> + */ + public function rules(): array + { + return [ + 'order' => ['required', Rule::in(self::SORTABLE)], + 'order_direction' => ['required', Rule::in(['asc', 'desc'])], + ]; + } + + /** + * Coerce missing or unrecognised sort parameters to sensible defaults, so a + * bad query string still renders the page rather than failing validation. + */ + protected function prepareForValidation(): void + { + $order = $this->input('order'); + $order = in_array($order, self::SORTABLE, true) ? $order : 'event'; + + $direction = strtolower((string) $this->input('order_direction')); + if (!in_array($direction, ['asc', 'desc'], true)) { + $direction = $order === 'event' ? 'desc' : 'asc'; + } + + $this->merge([ + 'order' => $order, + 'order_direction' => $direction, + ]); + } +} diff --git a/tests/Unit/app/Http/Controllers/TicketControllerTest.php b/tests/Unit/app/Http/Controllers/TicketControllerTest.php index 441cca67..36d9cd8e 100644 --- a/tests/Unit/app/Http/Controllers/TicketControllerTest.php +++ b/tests/Unit/app/Http/Controllers/TicketControllerTest.php @@ -3,6 +3,7 @@ namespace Tests\Unit\app\Http\Controllers; use App\Http\Controllers\TicketController; +use App\Http\Requests\TicketIndexRequest; use App\Http\Requests\TicketTransferRequest; use App\Models\Event; use App\Models\Seat; @@ -34,14 +35,7 @@ public function testIndexFiltersDraftEventsForNonAdmin() $ticketForDraft = Ticket::factory()->create(['event_id' => Event::factory()->create(['draft' => true, 'starts_at' => now(), 'ends_at' => now()->addHour()])->id, 'user_id' => $user->id]); $ticketForLive = Ticket::factory()->create(['event_id' => Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()])->id, 'user_id' => $user->id]); - $this->actingAs($user); - $controller = new TicketController(); - $request = Request::create('/', 'GET'); - $request->setUserResolver(function () use ($user) { - return $user; - }); - - $response = $controller->index($request); + $response = $this->indexResponse($user); $this->assertInstanceOf(View::class, $response); $data = $response->getData(); $tickets = $data['tickets']; @@ -283,12 +277,13 @@ public function testIndexPaginationPreservesSort() private function indexResponse(User $user, array $query = []): View { $this->actingAs($user); - $controller = new TicketController(); - $request = Request::create('/', 'GET', $query); + + $request = TicketIndexRequest::create('/', 'GET', $query); $request->setUserResolver(function () use ($user) { return $user; }); + $request->setContainer($this->app)->validateResolved(); - return $controller->index($request); + return (new TicketController())->index($request); } } From 3de025489474586b7837a61f8e51564dc7d55c90 Mon Sep 17 00:00:00 2001 From: Emily Rowan Date: Sat, 11 Jul 2026 03:29:38 +0100 Subject: [PATCH 5/5] refactor: simplify ticket sort validation per review - Make order/order_direction optional (drop required) and remove the prepareForValidation() hook. - Read sort params in the controller with $request->input(key, default), matching the pattern used by the other index pages. Invalid values are now rejected by the FormRequest rather than silently coerced; the fallback unit tests are replaced by feature tests asserting the validation errors. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Http/Controllers/TicketController.php | 4 +-- app/Http/Requests/TicketIndexRequest.php | 24 ++--------------- tests/Feature/TicketSortingTest.php | 18 +++++++++++++ .../Http/Controllers/TicketControllerTest.php | 26 ------------------- 4 files changed, 22 insertions(+), 50 deletions(-) diff --git a/app/Http/Controllers/TicketController.php b/app/Http/Controllers/TicketController.php index 90a1b78c..96fef026 100644 --- a/app/Http/Controllers/TicketController.php +++ b/app/Http/Controllers/TicketController.php @@ -12,8 +12,8 @@ class TicketController extends Controller { public function index(TicketIndexRequest $request) { - $order = $request->validated('order'); - $direction = $request->validated('order_direction'); + $order = $request->input('order', 'event'); + $direction = $request->input('order_direction', $order === 'event' ? 'desc' : 'asc'); $query = $request ->user() diff --git a/app/Http/Requests/TicketIndexRequest.php b/app/Http/Requests/TicketIndexRequest.php index 8f75b51e..0afd21ff 100644 --- a/app/Http/Requests/TicketIndexRequest.php +++ b/app/Http/Requests/TicketIndexRequest.php @@ -31,28 +31,8 @@ public function authorize(): bool public function rules(): array { return [ - 'order' => ['required', Rule::in(self::SORTABLE)], - 'order_direction' => ['required', Rule::in(['asc', 'desc'])], + 'order' => ['nullable', Rule::in(self::SORTABLE)], + 'order_direction' => ['nullable', Rule::in(['asc', 'desc'])], ]; } - - /** - * Coerce missing or unrecognised sort parameters to sensible defaults, so a - * bad query string still renders the page rather than failing validation. - */ - protected function prepareForValidation(): void - { - $order = $this->input('order'); - $order = in_array($order, self::SORTABLE, true) ? $order : 'event'; - - $direction = strtolower((string) $this->input('order_direction')); - if (!in_array($direction, ['asc', 'desc'], true)) { - $direction = $order === 'event' ? 'desc' : 'asc'; - } - - $this->merge([ - 'order' => $order, - 'order_direction' => $direction, - ]); - } } diff --git a/tests/Feature/TicketSortingTest.php b/tests/Feature/TicketSortingTest.php index 5dd75d61..45960fa4 100644 --- a/tests/Feature/TicketSortingTest.php +++ b/tests/Feature/TicketSortingTest.php @@ -43,4 +43,22 @@ public function testTicketsPageOrdersRowsByReferenceAscending() $response->assertOk(); $response->assertSeeInOrder(['AAA-001', 'ZZZ-999']); } + + public function testTicketsPageRejectsUnknownOrderColumn() + { + $user = User::factory()->create(['first_login' => false]); + + $response = $this->actingAs($user)->get(route('tickets.index', ['order' => 'bogus'])); + + $response->assertSessionHasErrors('order'); + } + + public function testTicketsPageRejectsUnknownOrderDirection() + { + $user = User::factory()->create(['first_login' => false]); + + $response = $this->actingAs($user)->get(route('tickets.index', ['order_direction' => 'sideways'])); + + $response->assertSessionHasErrors('order_direction'); + } } diff --git a/tests/Unit/app/Http/Controllers/TicketControllerTest.php b/tests/Unit/app/Http/Controllers/TicketControllerTest.php index 36d9cd8e..780f39d7 100644 --- a/tests/Unit/app/Http/Controllers/TicketControllerTest.php +++ b/tests/Unit/app/Http/Controllers/TicketControllerTest.php @@ -236,31 +236,6 @@ public function testIndexSortsByEventStartsAt() $this->assertSame([$oldTicket->id, $newTicket->id], $asc->pluck('id')->all()); } - public function testIndexInvalidOrderFallsBackToNewestEventFirst() - { - $user = User::factory()->create(); - $oldEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->subDays(5), 'ends_at' => now()->subDays(5)->addHour()]); - $newEvent = Event::factory()->create(['draft' => false, 'starts_at' => now()->addDays(5), 'ends_at' => now()->addDays(5)->addHour()]); - $oldTicket = Ticket::factory()->create(['event_id' => $oldEvent->id, 'user_id' => $user->id]); - $newTicket = Ticket::factory()->create(['event_id' => $newEvent->id, 'user_id' => $user->id]); - - $tickets = $this->indexResponse($user, ['order' => 'bogus', 'order_direction' => 'desc'])->getData()['tickets']; - - $this->assertSame([$newTicket->id, $oldTicket->id], $tickets->pluck('id')->all()); - } - - public function testIndexInvalidDirectionFallsBackToAscending() - { - $user = User::factory()->create(); - $event = Event::factory()->create(['draft' => false, 'starts_at' => now(), 'ends_at' => now()->addHour()]); - $a = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'AAA-001']); - $z = Ticket::factory()->create(['event_id' => $event->id, 'user_id' => $user->id, 'reference' => 'ZZZ-999']); - - $tickets = $this->indexResponse($user, ['order' => 'reference', 'order_direction' => 'sideways'])->getData()['tickets']; - - $this->assertSame([$a->id, $z->id], $tickets->pluck('id')->all()); - } - public function testIndexPaginationPreservesSort() { $user = User::factory()->create(); @@ -282,7 +257,6 @@ private function indexResponse(User $user, array $query = []): View $request->setUserResolver(function () use ($user) { return $user; }); - $request->setContainer($this->app)->validateResolved(); return (new TicketController())->index($request); }
ReferenceTypeSeatEvent@include('partials._sortheader', ['title' => 'Reference', 'field' => 'reference'])@include('partials._sortheader', ['title' => 'Type', 'field' => 'type'])@include('partials._sortheader', ['title' => 'Seat', 'field' => 'seat'])@include('partials._sortheader', ['title' => 'Event', 'field' => 'event', 'direction' => 'desc'])