add test for admin EnsembleController

This commit is contained in:
Matt Young 2025-07-09 02:33:41 -05:00
parent fa25e76c5b
commit e1d72ee040
5 changed files with 271 additions and 31 deletions

View File

@ -1,10 +1,10 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="tests - paralell" type="PestRunConfigurationType"> <configuration default="false" name="tests - paralell with HTML to reports" type="PestRunConfigurationType">
<option name="pestRunnerSettings"> <option name="pestRunnerSettings">
<PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" /> <PestRunner directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage-html reports" />
</option> </option>
<option name="runnerSettings"> <option name="runnerSettings">
<PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases" /> <PhpTestRunnerSettings directory="$PROJECT_DIR$/tests" method="" options="--parallel --recreate-databases --coverage-html reports" />
</option> </option>
<method v="2" /> <method v="2" />
</configuration> </configuration>

View File

@ -3,12 +3,13 @@
namespace App\Http\Controllers\Admin; namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\EnsembleStoreOrUpdateRequest;
use App\Models\Ensemble; use App\Models\Ensemble;
use App\Models\Event; use App\Models\Event;
use App\Models\SeatingLimit; use App\Models\SeatingLimit;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function redirect; use function redirect;
@ -21,30 +22,24 @@ class EnsembleController extends Controller
return view('admin.ensembles.index', compact('events')); return view('admin.ensembles.index', compact('events'));
} }
public function store(Request $request) public function store(EnsembleStoreOrUpdateRequest $request)
{ {
if (! Auth::user()->is_admin) { Log::channel('file')->warning('hello');
abort(403); $validated = $request->validated();
} // get the maximum value of rank from the ensemble table where event_id is equal to the request event_id
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
$maxCode = Ensemble::where('event_id', request('event_id'))->max('rank'); $maxCode = Ensemble::where('event_id', request('event_id'))->max('rank');
Ensemble::create([ Ensemble::create([
'name' => request('name'), 'name' => $validated['name'],
'code' => request('code'), 'code' => $validated['code'],
'event_id' => request('event_id'), 'event_id' => $validated['event_id'],
'rank' => $maxCode + 1, 'rank' => $maxCode + 1,
]); ]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble created successfully'); 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) { if ($ensemble->seats->count() > 0) {
return redirect()->route('admin.ensembles.index')->with('error', 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'); 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([ $valid = $request->validated();
'name' => 'required',
'code' => 'required|max:6',
]);
$ensemble->update([ $ensemble->update([
'name' => request('name'), 'name' => $valid['name'],
'code' => request('code'), 'code' => $valid['code'],
]); ]);
return redirect()->route('admin.ensembles.index')->with('success', 'Ensemble updated successfully'); 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) public function seatingLimits(Ensemble $ensemble)
{ {
$limits = []; $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()) { if ($ensemble->exists()) {
$ensemble->load('seatingLimits'); $ensemble->load('seatingLimits');
foreach ($ensemble->seatingLimits as $lim) { foreach ($ensemble->seatingLimits as $lim) {
@ -112,10 +114,6 @@ class EnsembleController extends Controller
public function updateEnsembleRank(Request $request) public function updateEnsembleRank(Request $request)
{ {
if (! Auth::user()->is_admin) {
abort(403);
}
$order = $request->input('order'); $order = $request->input('order');
$eventId = $request->input('event_id'); $eventId = $request->input('event_id');

View File

@ -0,0 +1,33 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class EnsembleStoreOrUpdateRequest extends FormRequest
{
public function authorize(): true
{
// Adjust authorization logic as needed
return true;
}
public function rules(): array
{
// Get the ID of the ensemble being updated, if any
$ensembleId = $this->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'],
];
}
}

View File

@ -74,7 +74,7 @@ Route::middleware(['auth', 'verified', CheckIfAdmin::class])->prefix('admin/')->
Route::post('/', 'store')->name('admin.ensembles.store'); Route::post('/', 'store')->name('admin.ensembles.store');
Route::delete('/{ensemble}', 'destroy')->name('admin.ensembles.destroy'); Route::delete('/{ensemble}', 'destroy')->name('admin.ensembles.destroy');
Route::post('/updateEnsembleRank', 'updateEnsembleRank')->name('admin.ensembles.updateEnsembleRank'); 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', 'seatingLimits')->name('admin.ensembles.seatingLimits');
Route::get('/seating-limits/{ensemble}', 'seatingLimits')->name('admin.ensembles.seatingLimits.ensemble'); Route::get('/seating-limits/{ensemble}', 'seatingLimits')->name('admin.ensembles.seatingLimits.ensemble');
Route::post('/seating-limits/{ensemble}', Route::post('/seating-limits/{ensemble}',

View File

@ -0,0 +1,209 @@
<?php
use App\Models\Audition;
use App\Models\Ensemble;
use App\Models\Entry;
use App\Models\Event;
use App\Models\Seat;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->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);
});
});