From e1d72ee040ed5f5eb9a03f23c2d2fdde93bf540b Mon Sep 17 00:00:00 2001 From: Matt Young Date: Wed, 9 Jul 2025 02:33:41 -0500 Subject: [PATCH] add test for admin EnsembleController --- ...s - paralell with HTML to reports.run.xml} | 6 +- .../Controllers/Admin/EnsembleController.php | 52 +++-- .../Requests/EnsembleStoreOrUpdateRequest.php | 33 +++ routes/admin.php | 2 +- .../Admin/EnsembleControllerTest.php | 209 ++++++++++++++++++ 5 files changed, 271 insertions(+), 31 deletions(-) rename .runParalellTestsAll/{tests - paralell.run.xml => tests - paralell with HTML to reports.run.xml} (54%) create mode 100644 app/Http/Requests/EnsembleStoreOrUpdateRequest.php create mode 100644 tests/Feature/app/Http/Controllers/Admin/EnsembleControllerTest.php diff --git a/.runParalellTestsAll/tests - paralell.run.xml b/.runParalellTestsAll/tests - paralell with HTML to reports.run.xml similarity index 54% rename from .runParalellTestsAll/tests - paralell.run.xml rename to .runParalellTestsAll/tests - paralell with HTML to reports.run.xml index 06e48dc..688c8da 100644 --- a/.runParalellTestsAll/tests - paralell.run.xml +++ b/.runParalellTestsAll/tests - paralell with HTML to reports.run.xml @@ -1,10 +1,10 @@ - + diff --git a/app/Http/Controllers/Admin/EnsembleController.php b/app/Http/Controllers/Admin/EnsembleController.php index 4cc0c45..f1463d9 100644 --- a/app/Http/Controllers/Admin/EnsembleController.php +++ b/app/Http/Controllers/Admin/EnsembleController.php @@ -3,12 +3,13 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; +use App\Http\Requests\EnsembleStoreOrUpdateRequest; use App\Models\Ensemble; use App\Models\Event; use App\Models\SeatingLimit; use Illuminate\Http\Request; -use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; +use Illuminate\Support\Facades\Log; use function redirect; @@ -21,30 +22,24 @@ class EnsembleController extends Controller return view('admin.ensembles.index', compact('events')); } - public function store(Request $request) + public function store(EnsembleStoreOrUpdateRequest $request) { - if (! Auth::user()->is_admin) { - abort(403); - } - request()->validate([ - 'name' => 'required', - 'code' => ['required', 'max:6'], - 'event_id' => ['required', 'exists:events,id'], - ]); - // get the maximum value of rank from the ensembles table where event_id is equal to the request event_id + Log::channel('file')->warning('hello'); + $validated = $request->validated(); + // get the maximum value of rank from the ensemble table where event_id is equal to the request event_id $maxCode = Ensemble::where('event_id', request('event_id'))->max('rank'); Ensemble::create([ - 'name' => request('name'), - 'code' => request('code'), - 'event_id' => request('event_id'), + 'name' => $validated['name'], + 'code' => $validated['code'], + 'event_id' => $validated['event_id'], 'rank' => $maxCode + 1, ]); return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully'); } - public function destroy(Request $request, Ensemble $ensemble) + public function destroy(Ensemble $ensemble) { if ($ensemble->seats->count() > 0) { return redirect()->route('admin.ensembles.index')->with('error', @@ -55,25 +50,32 @@ class EnsembleController extends Controller return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble deleted successfully'); } - public function updateEnsemble(Request $request, Ensemble $ensemble) + public function update(EnsembleStoreOrUpdateRequest $request, Ensemble $ensemble) { - request()->validate([ - 'name' => 'required', - 'code' => 'required|max:6', - ]); + $valid = $request->validated(); $ensemble->update([ - 'name' => request('name'), - 'code' => request('code'), + 'name' => $valid['name'], + 'code' => $valid['code'], ]); return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully'); } + //TODO Consider moving seating limit related functions to their own controller with index, edit, and update methods public function seatingLimits(Ensemble $ensemble) { $limits = []; - $ensembles = Ensemble::with(['event'])->orderBy('event_id')->get(); + /** + * If we weren't called with an ensemble, we're going to use an array of ensembles to fill a drop-down and + * choose one. The user will be sent back here, this time with the chosen audition. + */ + $ensembles = Ensemble::with(['event'])->orderBy('event_id')->orderBy('rank')->get(); + + /** + * If we were called with an ensemble, we need to load existing seating limits. We will put them in an array + * indexed by audition_id for easy use in the form to set seating limits. + */ if ($ensemble->exists()) { $ensemble->load('seatingLimits'); foreach ($ensemble->seatingLimits as $lim) { @@ -112,10 +114,6 @@ class EnsembleController extends Controller public function updateEnsembleRank(Request $request) { - if (! Auth::user()->is_admin) { - abort(403); - } - $order = $request->input('order'); $eventId = $request->input('event_id'); diff --git a/app/Http/Requests/EnsembleStoreOrUpdateRequest.php b/app/Http/Requests/EnsembleStoreOrUpdateRequest.php new file mode 100644 index 0000000..5651d52 --- /dev/null +++ b/app/Http/Requests/EnsembleStoreOrUpdateRequest.php @@ -0,0 +1,33 @@ +route('ensemble')?->id; + + return [ + 'name' => [ + 'required', + // Composite unique rule on (event_id, name) + Rule::unique('ensembles')->where(function ($query) { + return $query->where('event_id', $this->input('event_id')); + })->ignore($ensembleId), + ], + 'code' => ['required', 'max:6'], + 'event_id' => ['required', 'exists:events,id'], + ]; + } +} diff --git a/routes/admin.php b/routes/admin.php index 7f74433..5eee798 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -74,7 +74,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')-> Route::post('/', 'store')->name('admin.ensembles.store'); Route::delete('/{ensemble}', 'destroy')->name('admin.ensembles.destroy'); Route::post('/updateEnsembleRank', 'updateEnsembleRank')->name('admin.ensembles.updateEnsembleRank'); - Route::patch('/{ensemble}', 'updateEnsemble')->name('admin.ensembles.update'); + Route::patch('/{ensemble}', 'update')->name('admin.ensembles.update'); Route::get('/seating-limits', 'seatingLimits')->name('admin.ensembles.seatingLimits'); Route::get('/seating-limits/{ensemble}', 'seatingLimits')->name('admin.ensembles.seatingLimits.ensemble'); Route::post('/seating-limits/{ensemble}', diff --git a/tests/Feature/app/Http/Controllers/Admin/EnsembleControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/EnsembleControllerTest.php new file mode 100644 index 0000000..748b54b --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Admin/EnsembleControllerTest.php @@ -0,0 +1,209 @@ +ensemble = Ensemble::create([ + 'name' => 'Wind Ensemble', + 'rank' => 1, + 'code' => 'we', + 'event_id' => Event::factory()->create()->id, + ]); +}); + +describe('EnsembleController::index', function () { + it('denies access to non-admin users', function () { + $this->get(route('admin.ensembles.index'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.ensembles.index'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.ensembles.index'))->assertRedirect(route('dashboard')); + }); + it('shows an index of events', function () { + actAsAdmin(); + $this->get(route('admin.ensembles.index'))->assertOk() + ->assertSee($this->ensemble->name); + }); +}); + +describe('EnsembleController::store', function () { + it('denies access to non-admin users', function () { + $this->post(route('admin.ensembles.store'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.ensembles.store'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.ensembles.store'))->assertRedirect(route('dashboard')); + }); + it('creates an ensemble', function () { + actAsAdmin(); + $this->post(route('admin.ensembles.store'), [ + 'name' => 'New Ensemble', + 'code' => 'ne', + 'event_id' => Event::factory()->create()->id, + ])->assertRedirect(route('admin.ensembles.index'))->assertSessionHas('success'); + }); +}); + +describe('EnsembleController::destroy', function () { + it('denies access to non-admin users', function () { + $this->delete(route('admin.ensembles.destroy', $this->ensemble))->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.ensembles.destroy', $this->ensemble))->assertRedirect(route('dashboard')); + actAsTab(); + $this->delete(route('admin.ensembles.destroy', $this->ensemble))->assertRedirect(route('dashboard')); + }); + it('will not destroy an ensemble with seated students', function () { + actAsAdmin(); + $audition = Audition::factory()->create(); + $entry = Entry::factory()->create(); + Seat::create([ + 'ensemble_id' => $this->ensemble->id, + 'audition_id' => $audition->id, + 'seat' => 3, + 'entry_id' => $entry->id, + ]); + $this->delete(route('admin.ensembles.destroy', $this->ensemble))->assertRedirect(route('admin.ensembles.index')) + ->assertSessionHas('error', 'Ensemble has students seated and cannot be deleted'); + }); + it('can delete an ensemble', function () { + $startCount = Ensemble::count(); + actAsAdmin(); + $this->delete(route('admin.ensembles.destroy', $this->ensemble))->assertRedirect(route('admin.ensembles.index')) + ->assertSessionHas('success', 'Ensemble deleted successfully'); + expect(Ensemble::count())->toEqual($startCount - 1); + }); +}); + +describe('EnsembleController::update', function () { + it('denies access to non-admin users', function () { + $this->patch(route('admin.ensembles.update', $this->ensemble))->assertRedirect(route('home')); + actAsNormal(); + $this->patch(route('admin.ensembles.update', $this->ensemble))->assertRedirect(route('dashboard')); + actAsTab(); + $this->patch(route('admin.ensembles.update', $this->ensemble))->assertRedirect(route('dashboard')); + }); + it('can update an event', function () { + $event = Event::factory()->create(); + actAsAdmin(); + $response = $this->patch(route('admin.ensembles.update', $this->ensemble), [ + 'name' => 'Wind Ensemble Restart', + 'code' => 'we2', + 'event_id' => $event->id, + ]); + $response->assertRedirect(route('admin.ensembles.index')) + ->assertSessionHas('success', 'Ensemble updated successfully'); + }); +}); + +describe('EnsembleController::seatingLimits with no ensemble', function () { + + it('denies access to non-admin users', function () { + $this->get(route('admin.ensembles.seatingLimits'))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.ensembles.seatingLimits'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.ensembles.seatingLimits'))->assertRedirect(route('dashboard')); + }); + it('returns a page to choose and ensemble for which to set limits', function () { + actAsAdmin(); + $response = $this->get(route('admin.ensembles.seatingLimits'))->assertOk() + ->assertViewIs('admin.ensembles.seatingLimits') + ->assertViewHas('ensembles'); + expect($response->viewData('ensemble')->exists)->toBeFalse() + ->and($response->viewData('ensembles')->first()->id)->toEqual($this->ensemble->id) + ->and($response->viewData('limits'))->toBe([]); + }); +}); + +describe('EnsembleController::seatingLimits get with ensemble', function () { + it('denies access to non-admin users', function () { + $this->get(route('admin.ensembles.seatingLimits', $this->ensemble))->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.ensembles.seatingLimits', $this->ensemble))->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.ensembles.seatingLimits', $this->ensemble))->assertRedirect(route('dashboard')); + }); + it('displays a form with fields for every audition to set the max for this ensemble', function () { + actAsAdmin(); + $auditions = Audition::factory()->count(5)->create(['event_id' => $this->ensemble->event_id]); + DB::table('seating_limits')->insert([ + 'ensemble_id' => $this->ensemble->id, + 'audition_id' => $auditions[0]->id, + 'maximum_accepted' => 6, + ]); + $response = $this->get(route('admin.ensembles.seatingLimits.ensemble.set', $this->ensemble))->assertOk(); + foreach (Audition::all() as $audition) { + $response->assertSee('audition['.$audition->id.']'); + } + }); +}); + +describe('EnsembleController::seatingLimitsSet', function () { + it('denies access to non-admin users', function () { + $this->post(route('admin.ensembles.seatingLimits.ensemble.set', + $this->ensemble))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.ensembles.seatingLimits.ensemble.set', + $this->ensemble))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.ensembles.seatingLimits.ensemble.set', + $this->ensemble))->assertRedirect(route('dashboard')); + }); + it('sets seating limits', function () { + actAsAdmin(); + $auditions = Audition::factory()->count(3)->create(['event_id' => $this->ensemble->event_id]); + $response = $this->post(route('admin.ensembles.seatingLimits.ensemble.set', $this->ensemble), [ + 'audition' => [ + $auditions[0]->id => 5, + $auditions[1]->id => 10, + $auditions[2]->id => 20, + ], + ]); + $response->assertRedirect(route('admin.ensembles.seatingLimits.ensemble', $this->ensemble)) + ->assertSessionHas('success', 'Seating limits set for '.$this->ensemble->name); + }); +}); + +describe('EnsembleController::updateEnsembleRank', function () { + it('denies access to non-admin users', function () { + $this->post(route('admin.ensembles.updateEnsembleRank'))->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.ensembles.updateEnsembleRank'))->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.ensembles.updateEnsembleRank'))->assertRedirect(route('dashboard')); + }); + it('reorders ensembles', function () { + actAsAdmin(); + $newEnsemble = Ensemble::create([ + 'name' => 'Alternates', + 'code' => 'Alt', + 'rank' => 2, + 'event_id' => $this->ensemble->event_id, + ]); + expect($this->ensemble->rank)->toBe(1); + expect($newEnsemble->rank)->toBe(2); + $response = $this->post(route('admin.ensembles.updateEnsembleRank'), [ + 'event_id' => $this->ensemble->event_id, + 'order' => [ + [ + 'id' => $this->ensemble->id, + 'rank' => 2, + ], + [ + 'id' => $newEnsemble->id, + 'rank' => 1, + ], + ], + ])->assertJson(['status' => 'success']); + $this->ensemble->refresh(); + $newEnsemble->refresh(); + expect($this->ensemble->rank)->toBe(2); + expect($newEnsemble->rank)->toBe(1); + }); +});