From eb66da14cfd4a1c328d17e810971fe1a2a943dc0 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Thu, 10 Jul 2025 00:33:38 -0500 Subject: [PATCH] Tests for Audition controller --- .../Controllers/Admin/AuditionController.php | 70 ++--- .../Requests/AuditionStoreOrUpdateRequest.php | 53 ++++ app/Observers/AuditionObserver.php | 10 +- routes/admin.php | 2 +- .../Admin/AuditionControllerTest.php | 283 ++++++++++++++++++ 5 files changed, 363 insertions(+), 55 deletions(-) create mode 100644 app/Http/Requests/AuditionStoreOrUpdateRequest.php create mode 100644 tests/Feature/app/Http/Controllers/Admin/AuditionControllerTest.php diff --git a/app/Http/Controllers/Admin/AuditionController.php b/app/Http/Controllers/Admin/AuditionController.php index 57fb2c9..67d8107 100644 --- a/app/Http/Controllers/Admin/AuditionController.php +++ b/app/Http/Controllers/Admin/AuditionController.php @@ -3,13 +3,13 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\AuditionStoreOrUpdateRequest; use App\Models\Audition; use App\Models\Event; +use App\Models\Room; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; -use function abort; use function redirect; use function request; use function response; @@ -28,38 +28,20 @@ class AuditionController extends Controller public function create() { - if (! Auth::user()->is_admin) { - abort(403); - } $events = Event::orderBy('name')->get(); return view('admin.auditions.create', ['events' => $events]); } - public function store(Request $request) + public function store(AuditionStoreOrUpdateRequest $request) { - if (! Auth::user()->is_admin) { - abort(403); - } - $validData = request()->validate([ - 'event_id' => ['required', 'exists:events,id'], - 'name' => ['required'], - 'entry_deadline' => ['required', 'date'], - 'entry_fee' => ['required', 'numeric'], - 'minimum_grade' => ['required', 'integer'], - 'maximum_grade' => 'required|numeric|gte:minimum_grade', - 'scoring_guide_id' => 'nullable|exists:scoring_guides,id', - ], [ - 'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.', - ]); + $validData = $request->validated(); - $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; - $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; - if (empty($alidData['scoring_guide_id'])) { + if (empty($validData['scoring_guide_id'])) { $validData['scoring_guide_id'] = 0; } - $new_score_order = Audition::max('score_order') + 1; - // TODO Check if room 0 exists, create if not + $validData['score_order'] = Audition::max('score_order') + 1; + Audition::create([ 'event_id' => $validData['event_id'], 'name' => $validData['name'], @@ -71,7 +53,7 @@ class AuditionController extends Controller 'for_advancement' => $validData['for_advancement'], 'scoring_guide_id' => $validData['scoring_guide_id'], 'room_id' => 0, - 'score_order' => $new_score_order, + 'score_order' => $validData['score_order'], ]); return to_route('admin.auditions.index')->with('success', 'Audition created successfully'); @@ -79,33 +61,14 @@ class AuditionController extends Controller public function edit(Audition $audition) { - if (! Auth::user()->is_admin) { - abort(403); - } $events = Event::orderBy('name')->get(); return view('admin.auditions.edit', ['audition' => $audition, 'events' => $events]); } - public function update(Request $request, Audition $audition) + public function update(AuditionStoreOrUpdateRequest $request, Audition $audition) { - if (! Auth::user()->is_admin) { - abort(403); - } - - $validData = request()->validate([ - 'event_id' => ['required', 'exists:events,id'], - 'name' => ['required'], - 'entry_deadline' => ['required', 'date'], - 'entry_fee' => ['required', 'numeric'], - 'minimum_grade' => ['required', 'integer'], - 'maximum_grade' => 'required | numeric | gte:minimum_grade', - ], [ - 'maximum_grade.gte' => 'The maximum grade must be greater than the minimum grade.', - ]); - - $validData['for_seating'] = $request->get('for_seating') ? 1 : 0; - $validData['for_advancement'] = $request->get('for_advancement') ? 1 : 0; + $validData = $request->validated(); $audition->update([ 'event_id' => $validData['event_id'], @@ -123,9 +86,6 @@ class AuditionController extends Controller public function reorder(Request $request) { - if (! Auth::user()->is_admin) { - abort(403); - } $order = $request->order; foreach ($order as $index => $id) { $audition = Audition::find($id); @@ -138,9 +98,15 @@ class AuditionController extends Controller public function roomUpdate(Request $request) { $auditions = $request->all(); - + /** + * $auditions will be an array of arrays with the following structure: + * [ + * ['id' => 1, 'room_id' => 1, 'room_order' => 1], + * ] + * is is an audition id + */ foreach ($auditions as $audition) { - Audition::where('id', $audition['id']) + $a = Audition::where('id', $audition['id']) ->update([ 'room_id' => $audition['room_id'], 'order_in_room' => $audition['room_order'], diff --git a/app/Http/Requests/AuditionStoreOrUpdateRequest.php b/app/Http/Requests/AuditionStoreOrUpdateRequest.php new file mode 100644 index 0000000..1345696 --- /dev/null +++ b/app/Http/Requests/AuditionStoreOrUpdateRequest.php @@ -0,0 +1,53 @@ +user()->is_admin; + } + + public function rules() + { + $auditionId = $this->route('audition') ? $this->route('audition')->id : null; + + return [ + 'event_id' => ['required', 'exists:events,id'], + 'name' => [ + 'required', + Rule::unique('auditions', 'name')->ignore($auditionId), + ], + 'entry_deadline' => ['required', 'date'], + 'entry_fee' => ['required', 'numeric'], + 'minimum_grade' => ['required', 'integer', 'min:1'], + 'maximum_grade' => ['required', 'integer', 'min:1', 'gte:minimum_grade'], + 'scoring_guide_id' => ['sometimes', 'nullable', 'exists:scoring_guides,id'], + 'for_seating' => ['sometimes', 'boolean'], + 'for_advancement' => ['sometimes', 'boolean'], + ]; + } + + public function messages() + { + return [ + 'maximum_grade.gte' => 'The maximum grade must be greater than or equal to the minimum grade.', + 'minimum_grade.min' => 'The minimum grade must be a positive integer.', + 'maximum_grade.min' => 'The maximum grade must be a positive integer.', + ]; + } + + protected function prepareForValidation() + { + // Convert checkbox inputs to boolean (1 or 0) + $this->merge([ + 'for_seating' => $this->has('for_seating') ? 1 : 0, + 'for_advancement' => $this->has('for_advancement') ? 1 : 0, + ]); + } +} diff --git a/app/Observers/AuditionObserver.php b/app/Observers/AuditionObserver.php index 237d554..1bc6ca2 100644 --- a/app/Observers/AuditionObserver.php +++ b/app/Observers/AuditionObserver.php @@ -10,9 +10,15 @@ class AuditionObserver public function created(Audition $audition): void { $message = 'Added audition #'.$audition->id.' '.$audition->name.' to event '.$audition->event->name; - $message .= '
Deadline: '.$audition->entry_deadline->format('m/d/Y'); + $message .= '
Deadline: '.$audition->entry_deadline; $message .= '
Entry Fee: '.$audition->display_fee(); $message .= '
Grade Range: '.$audition->minimum_grade.' - '.$audition->maximum_grade; + if ($audition->for_seating) { + $message .= '
Entered for '.auditionSetting('auditionAbbreviation'); + } + if ($audition->for_advancement) { + $message .= '
Entered for '.auditionSetting('advanceTo'); + } $affected = ['auditions' => [$audition->id], 'events' => [$audition->event_id]]; auditionLog($message, $affected); } @@ -27,7 +33,7 @@ class AuditionObserver $affected['auditions'] = [$audition->id]; } if ($audition->entry_deadline !== $audition->getOriginal('entry_deadline')) { - $message .= '
Deadline: '.$audition->entry_deadline->format('m/d/Y'); + $message .= '
Deadline: '.$audition->entry_deadline; } if ($audition->entryFee !== $audition->getOriginal('entryFee')) { $message .= '
Entry Fee: '.$audition->display_fee(); diff --git a/routes/admin.php b/routes/admin.php index 5eee798..ee9c6bb 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -40,7 +40,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::post('/auditions/roomUpdate', [ AuditionController::class, 'roomUpdate', - ]); // Endpoint for JS assigning auditions to rooms + ])->name('admin.roomUpdate'); // Endpoint for JS assigning auditions to rooms Route::post('/scoring/assign_guide_to_audition', [ AuditionController::class, 'scoringGuideUpdate', ])->name('ajax.assignScoringGuideToAudition'); // Endpoint for JS assigning scoring guides to auditions diff --git a/tests/Feature/app/Http/Controllers/Admin/AuditionControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/AuditionControllerTest.php new file mode 100644 index 0000000..112c44e --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Admin/AuditionControllerTest.php @@ -0,0 +1,283 @@ +get(route('admin.auditions.index'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.auditions.index'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.auditions.index'))->assertRedirect(route('dashboard')); + }); + it('shows the audition index page', function () { + $auditions = Audition::factory()->count(3)->create(); + actAsAdmin(); + $response = $this->get(route('admin.auditions.index'))->assertOk() + ->assertViewIs('admin.auditions.index'); + foreach ($auditions as $audition) { + $response->assertSee($audition->name); + } + }); +}); + +describe('AuditionController::create', function () { + it('denies access to a non-admin user', function () { + $this->get(route('admin.auditions.create'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.auditions.create'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.auditions.create'))->assertRedirect(route('dashboard')); + }); + it('shows the audition create page', function () { + actAsAdmin(); + $events = Event::factory()->count(3)->create(); + $response = $this->get(route('admin.auditions.create'))->assertOk() + ->assertViewIs('admin.auditions.create'); + foreach ($events as $event) { + $response->assertSee($event->name); + } + }); +}); + +describe('AuditionController::store', function () { + it('denies access to a non-admin user', function () { + $this->post(route('admin.auditions.store'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.auditions.store'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.auditions.store'))->assertRedirect(route('dashboard')); + }); + it('creates an audition', function () { + actAsAdmin(); + $response = $this->post(route('admin.auditions.store'), [ + 'name' => 'Test Audition', + 'event_id' => Event::factory()->create()->id, + 'entry_deadline' => '08/22/2025', + 'entry_fee' => '20.00', + 'minimum_grade' => '7', + 'maximum_grade' => '12', + 'for_advancement' => 'on', + 'for_seating' => 'on', + ]); + $response->assertRedirect(route('admin.auditions.index'))->assertSessionHas('success'); + $this->assertDatabaseHas('auditions', [ + 'name' => 'Test Audition', + ]); + }); +}); + +describe('AuditionController::edit', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->get(route('admin.auditions.edit', $audition))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.auditions.edit', $audition))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.auditions.edit', $audition))->assertRedirect(route('dashboard')); + }); + it('shows the audition edit page', function () { + $audition = Audition::factory()->create(); + actAsAdmin(); + $response = $this->get(route('admin.auditions.edit', $audition))->assertOk() + ->assertViewIs('admin.auditions.edit'); + $response->assertSee($audition->name) + ->assertSee(route('admin.auditions.update', $audition)); + }); +}); + +describe('AuditionController::update', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->patch(route('admin.auditions.update', $audition))->assertRedirect(route('home')); + actAsNormal(); + $this->patch(route('admin.auditions.update', $audition))->assertRedirect(route('dashboard')); + actAsTab(); + $this->patch(route('admin.auditions.update', $audition))->assertRedirect(route('dashboard')); + }); + it('updates an audition', function () { + $audition = Audition::factory()->create(); + actAsAdmin(); + $response = $this->patch(route('admin.auditions.update', $audition), [ + 'name' => 'Test Auditionnnnnn', + 'event_id' => Event::factory()->create()->id, + 'entry_deadline' => '08/22/2025', + 'entry_fee' => '20.00', + 'minimum_grade' => '7', + 'maximum_grade' => '12', + ]); + $response->assertRedirect(route('admin.auditions.index'))->assertSessionHas('success'); + $this->assertDatabaseHas('auditions', [ + 'name' => 'Test Auditionnnnnn', + ]); + }); +}); + +describe('AuditionController::reorder', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->post(route('admin.auditions.reorder'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.auditions.reorder'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.auditions.reorder'))->assertRedirect(route('dashboard')); + }); + it('reorders auditions', function () { + $audition1 = Audition::factory()->create(); + $audition2 = Audition::factory()->create(); + $audition3 = Audition::factory()->create(); + $audition4 = Audition::factory()->create(); + $audition5 = Audition::factory()->create(); + $input = [ + 'order' => [ + 1 => $audition3->id, + 2 => $audition1->id, + 3 => $audition4->id, + 4 => $audition5->id, + 5 => $audition2->id, + ], + ]; + + actAsAdmin(); + $response = $this->post(route('admin.auditions.reorder'), $input); + $response->assertJson(['status' => 'success']); + $audition1->refresh(); + $audition2->refresh(); + $audition3->refresh(); + $audition4->refresh(); + $audition5->refresh(); + expect($audition1->score_order)->toBe(2); + expect($audition2->score_order)->toBe(5); + expect($audition3->score_order)->toBe(1); + expect($audition4->score_order)->toBe(3); + expect($audition5->score_order)->toBe(4); + }); +}); + +describe('AuditionController::roomUpdate', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->post(route('admin.roomUpdate'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.roomUpdate'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.roomUpdate'))->assertRedirect(route('dashboard')); + }); + it('updates the room for an audition', function () { + $audition1 = Audition::factory()->create(); + $audition2 = Audition::factory()->create(); + $audition3 = Audition::factory()->create(); + $audition4 = Audition::factory()->create(); + $audition5 = Audition::factory()->create(); + $oddRoom = Room::factory()->create(['name' => 'odd']); + $evenRoom = Room::factory()->create(['name' => 'even']); + actAsAdmin(); + $response = $this->post(route('admin.roomUpdate'), [ + [ + 'id' => $audition1->id, + 'room_id' => $oddRoom->id, + 'room_order' => 3, + ], + [ + 'id' => $audition2->id, + 'room_id' => $evenRoom->id, + 'room_order' => 2, + ], + [ + 'id' => $audition3->id, + 'room_id' => $oddRoom->id, + 'room_order' => 2, + ], + [ + 'id' => $audition4->id, + 'room_id' => $evenRoom->id, + 'room_order' => 1, + ], + [ + 'id' => $audition5->id, + 'room_id' => $oddRoom->id, + 'room_order' => 1, + ], + + ]); + $response->assertJson(['status' => 'success']); + $audition1->refresh(); + $audition2->refresh(); + $audition3->refresh(); + $audition4->refresh(); + $audition5->refresh(); + + expect($audition1->room_id)->toEqual($oddRoom->id); + expect($audition1->order_in_room)->toEqual(3); + expect($audition2->room_id)->toEqual($evenRoom->id); + expect($audition2->order_in_room)->toEqual(2); + }); +}); + +describe('AuditionController::scoringGuideUpdate', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->post(route('ajax.assignScoringGuideToAudition'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('ajax.assignScoringGuideToAudition'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('ajax.assignScoringGuideToAudition'))->assertRedirect(route('dashboard')); + }); + it('updates the scoring guide for an audition', function () { + $audition = Audition::factory()->create(); + $guide = ScoringGuide::factory()->create(); + actAsAdmin(); + $response = $this->post(route('ajax.assignScoringGuideToAudition'), [ + 'audition_id' => $audition->id, + 'new_guide_id' => $guide->id, + ]); + $response->assertJson(['success' => true]); + $audition->refresh(); + expect($audition->scoring_guide_id)->toEqual($guide->id); + }); + it('fails if an invalid audition is called for', function () { + $audition = Audition::factory()->create(); + $guide = ScoringGuide::factory()->create(); + actAsAdmin(); + $response = $this->post(route('ajax.assignScoringGuideToAudition'), [ + 'audition_id' => $audition->id + 1, + 'new_guide_id' => $guide->id, + ]); + $response->assertJson(['success' => false]); + }); +}); + +describe('AuditionController::destroy', function () { + it('denies access to a non-admin user', function () { + $audition = Audition::factory()->create(); + $this->delete(route('admin.auditions.destroy', $audition))->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.auditions.destroy', $audition))->assertRedirect(route('dashboard')); + actAsTab(); + $this->delete(route('admin.auditions.destroy', $audition))->assertRedirect(route('dashboard')); + }); + it('deletes an audition', function () { + $audition = Audition::factory()->create(); + actAsAdmin(); + $this->delete(route('admin.auditions.destroy', $audition))->assertRedirect(route('admin.auditions.index'))->assertSessionHas('success'); + $this->assertDatabaseMissing('auditions', [ + 'id' => $audition->id, + ]); + }); + it('will not delete an audition with entries', function () { + $audition = Audition::factory()->create(); + $entry = Entry::factory()->forAudition($audition)->create(); + actAsAdmin(); + $this->delete(route('admin.auditions.destroy', $audition)) + ->assertRedirect(route('admin.auditions.index')) + ->assertSessionHas('error', 'Cannot delete an audition with entries.'); + expect(Audition::find($audition->id)->exists())->toBeTrue(); + }); +});