From e4a646a4ce5fa338ac8e1b9eae4f81a0673de509 Mon Sep 17 00:00:00 2001 From: Matt Young Date: Mon, 7 Jul 2025 23:56:52 -0500 Subject: [PATCH] Add tests for admin/SchoolController --- .../Controllers/Admin/SchoolController.php | 89 ++--- app/Http/Requests/SchoolStoreRequest.php | 10 +- .../Admin/SchoolControllerTest.php | 328 ++++++++++++++++++ 3 files changed, 358 insertions(+), 69 deletions(-) create mode 100644 tests/Feature/app/Http/Controllers/Admin/SchoolControllerTest.php diff --git a/app/Http/Controllers/Admin/SchoolController.php b/app/Http/Controllers/Admin/SchoolController.php index ccd05fd..d3fbca9 100644 --- a/app/Http/Controllers/Admin/SchoolController.php +++ b/app/Http/Controllers/Admin/SchoolController.php @@ -5,14 +5,12 @@ namespace App\Http\Controllers\Admin; use App\Actions\Schools\CreateSchool; use App\Actions\Schools\SetHeadDirector; use App\Http\Controllers\Controller; -use App\Models\AuditLogEntry; +use App\Http\Requests\SchoolStoreRequest; use App\Models\School; use App\Models\SchoolEmailDomain; use App\Models\User; use App\Services\Invoice\InvoiceDataService; -use Illuminate\Support\Facades\Auth; -use function abort; use function redirect; use function request; @@ -39,46 +37,25 @@ class SchoolController extends Controller public function show(School $school) { - if (! Auth::user()->is_admin) { - abort(403); - } return view('admin.schools.show', ['school' => $school]); } public function edit(School $school) { - if (! Auth::user()->is_admin) { - abort(403); - } $school->loadCount('students'); return view('admin.schools.edit', ['school' => $school]); } - public function update(School $school) + public function update(SchoolStoreRequest $request, School $school) { - request()->validate([ - 'name' => ['required'], - 'address' => ['required'], - 'city' => ['required'], - 'state' => ['required'], - 'zip' => ['required'], - ]); - $school->update([ - 'name' => request('name'), - 'address' => request('address'), - 'city' => request('city'), - 'state' => request('state'), - 'zip' => request('zip'), - ]); - $message = 'Modified school #'.$school->id.' - '.$school->name.' with address
'.$school->address.'
'.$school->city.', '.$school->state.' '.$school->zip; - AuditLogEntry::create([ - 'user' => auth()->user()->email, - 'ip_address' => request()->ip(), - 'message' => $message, - 'affected' => ['schools' => [$school->id]], + 'name' => $request['name'], + 'address' => $request['address'], + 'city' => $request['city'], + 'state' => $request['state'], + 'zip' => $request['zip'], ]); return redirect()->route('admin.schools.show', ['school' => $school->id])->with('success', @@ -87,48 +64,30 @@ class SchoolController extends Controller public function create() { - if (! Auth::user()->is_admin) { - abort(403); - } - return view('admin.schools.create'); } - public function store() + public function store(SchoolStoreRequest $request) { $creator = app(CreateSchool::class); - $validData = request()->validate([ - 'name' => ['required', 'unique:schools,name'], - 'address' => ['required'], - 'city' => ['required'], - 'state' => ['required'], - 'zip' => ['required'], - ]); $school = $creator( - $validData['name'], - $validData['address'], - $validData['city'], - $validData['state'], - $validData['zip'], + $request['name'], + $request['address'], + $request['city'], + $request['state'], + $request['zip'], ); - return redirect('/admin/schools')->with('success', 'School '.$school->name.' created'); + return redirect(route('admin.schools.index'))->with('success', 'School '.$school->name.' created'); } public function destroy(School $school) { if ($school->students()->count() > 0) { - return to_route('admin.schools.index')->with('error', 'You cannot delete a school with students.'); + return to_route('admin.schools.index')->with('error', 'You cannot delete a school that has students.'); } - $name = $school->name; - $message = 'Delete school #'.$school->id.' - '.$school->name; - AuditLogEntry::create([ - 'user' => auth()->user()->email, - 'ip_address' => request()->ip(), - 'message' => $message, - 'affected' => ['schools' => [$school->id]], - ]); + $school->delete(); return to_route('admin.schools.index')->with('success', 'School '.$school->name.' deleted'); @@ -136,9 +95,6 @@ class SchoolController extends Controller public function add_domain(School $school) { - if (! Auth::user()->is_admin) { - abort(403); - } request()->validate([ // validate that the combination of school and domain is unique on the school_email_domains table 'domain' => ['required'], @@ -147,12 +103,6 @@ class SchoolController extends Controller 'school_id' => $school->id, 'domain' => request('domain'), ]); - AuditLogEntry::create([ - 'user' => auth()->user()->email, - 'ip_address' => request()->ip(), - 'message' => 'Added '.request('domain').' as an email domain for school #'.$school->id.' - '.$school->name, - 'affected' => ['schools' => [$school->id]], - ]); return redirect()->route('admin.schools.show', $school)->with('success', 'Domain Added'); @@ -164,9 +114,11 @@ class SchoolController extends Controller $domain->delete(); // return a redirect to the previous URL - return redirect()->back(); + return redirect()->back()->with('success', 'Domain removed successfully.'); } + // TODO: Add testing for invoicing + /** @codeCoverageIgnore */ public function viewInvoice(School $school) { $invoiceData = $this->invoiceService->allData($school->id); @@ -179,8 +131,9 @@ class SchoolController extends Controller if ($user->school_id !== $school->id) { return redirect()->back()->with('error', 'That user is not at that school'); } + /** @noinspection PhpUnhandledExceptionInspection */ $headSetter->setHeadDirector($user); - return redirect()->back()->with('success', 'Head director set'); + return redirect()->back()->with('success', 'Head director set successfully.'); } } diff --git a/app/Http/Requests/SchoolStoreRequest.php b/app/Http/Requests/SchoolStoreRequest.php index e0dd2ea..10c936e 100644 --- a/app/Http/Requests/SchoolStoreRequest.php +++ b/app/Http/Requests/SchoolStoreRequest.php @@ -4,13 +4,21 @@ namespace App\Http\Requests; use App\Models\School; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Validation\Rule; class SchoolStoreRequest extends FormRequest { public function rules(): array { + $schoolId = $this->route('school'); + return [ - 'name' => ['required', 'min:3', 'max:30', 'unique:schools,name'], + 'name' => [ + 'required', + 'min:3', + 'max:30', + Rule::unique('schools', 'name')->ignore($schoolId), + ], 'address' => ['required'], 'city' => ['required'], 'state' => ['required', 'min:2', 'max:2'], diff --git a/tests/Feature/app/Http/Controllers/Admin/SchoolControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/SchoolControllerTest.php new file mode 100644 index 0000000..ae647c5 --- /dev/null +++ b/tests/Feature/app/Http/Controllers/Admin/SchoolControllerTest.php @@ -0,0 +1,328 @@ +createMock(InvoiceDataService::class); + $mockInvoice->method('getGrandTotal')->willReturn(40.00); + $this->app->instance(InvoiceDataService::class, $mockInvoice); +}); + +describe('SchoolController::index', function () { + it('denies access to a non-admin user', function () { + $this->get(route('admin.schools.index')) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.schools.index')) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.schools.index')) + ->assertRedirect(route('dashboard')); + }); + it('shows all schools', function () { + School::factory()->count(3)->create(); + actAsAdmin(); + $response = $this->get(route('admin.schools.index')); + $response->assertOk(); + foreach (School::all() as $school) { + $response->assertSee($school->name); + } + $response->assertSee('$40.00'); + }); +}); + +describe('SchoolController::show', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + }); + it('denies access to a non-admin user', function () { + $this->get(route('admin.schools.show', $this->school)) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.schools.show', $this->school)) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.schools.show', $this->school)) + ->assertRedirect(route('dashboard')); + }); + it('shows the school details', function () { + actAsAdmin(); + $response = $this->get(route('admin.schools.show', $this->school)); + $response->assertOk(); + $response->assertSee($this->school->name); + }); +}); + +describe('SchoolController::edit', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + }); + it('denies access to a non-admin user', function () { + $this->get(route('admin.schools.edit', $this->school)) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.schools.edit', $this->school)) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.schools.edit', $this->school)) + ->assertRedirect(route('dashboard')); + }); + it('shows the school details', function () { + actAsAdmin(); + $response = $this->get(route('admin.schools.edit', $this->school)); + $response->assertOk(); + $response->assertSee($this->school->name); + }); +}); + +describe('SchoolController::update', function () { + beforeEach(function () { + $this->school = School::create([ + 'name' => 'Test School', + 'address' => '123 4th street', + 'city' => 'New York', + 'state' => 'NY', + 'zip' => '10001', + ]); + $this->newData = [ + 'name' => 'New Name', + 'address' => '567 8th street', + 'city' => 'New Orleans', + 'state' => 'LA', + 'zip' => '70110', + ]; + }); + it('denies access to a non-admin user', function () { + $this->patch(route('admin.schools.update', $this->school), $this->newData) + ->assertRedirect(route('home')); + actAsNormal(); + $this->patch(route('admin.schools.update', $this->school), $this->newData) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->patch(route('admin.schools.update', $this->school), $this->newData) + ->assertRedirect(route('dashboard')); + }); + it('updates the school', function () { + actAsAdmin(); + $this->patch(route('admin.schools.update', $this->school), $this->newData); + $this->school->refresh(); + $this->assertEquals($this->newData['name'], $this->newData['name']); + $this->assertEquals($this->newData['address'], $this->newData['address']); + $this->assertEquals($this->newData['city'], $this->newData['city']); + $this->assertEquals($this->newData['state'], $this->newData['state']); + $this->assertEquals($this->newData['zip'], $this->newData['zip']); + }); + it('will not let us duplicate a name', function () { + $otherSchool = School::factory()->create(); + $this->newData['name'] = $otherSchool->name; + actAsAdmin(); + $response = $this->patch(route('admin.schools.update', $this->school), $this->newData); + $response->assertSessionHasErrors('name'); + $this->school->refresh(); + $this->assertNotEquals($this->school->name, $otherSchool->name); + }); + it('will let us update other data and retain the name', function () { + $this->newData['name'] = 'Test School'; + actAsAdmin(); + $response = $this->patch(route('admin.schools.update', $this->school), $this->newData); + /** @noinspection PhpUnhandledExceptionInspection */ + $response->assertSessionHasNoErrors(); + $this->school->refresh(); + $this->assertEquals($this->newData['address'], $this->newData['address']); + }); +}); + +describe('SchoolController::create', function () { + it('denies access to a non-admin user', function () { + $this->get(route('admin.schools.create')) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.schools.create')) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.schools.create')) + ->assertRedirect(route('dashboard')); + }); + it('shows the school creation form', function () { + actAsAdmin(); + $response = $this->get(route('admin.schools.create')); + $response->assertOk(); + $response->assertSee('Create School'); + }); +}); + +describe('SchoolController::store', function () { + beforeEach(function () { + $this->newData = [ + 'name' => 'Test School', + 'address' => '123 4th street', + 'city' => 'New York', + 'state' => 'NY', + 'zip' => '10001', + ]; + }); + it('denies access to a non-admin user', function () { + $this->post(route('admin.schools.store'), $this->newData) + ->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.schools.store'), $this->newData) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.schools.store'), $this->newData) + ->assertRedirect(route('dashboard')); + }); + it('creates a new school', function () { + actAsAdmin(); + /** @noinspection PhpUnhandledExceptionInspection */ + $this->post(route('admin.schools.store'), + $this->newData)->assertRedirect(route('admin.schools.index'))->assertSessionHasNoErrors(); + $newSchool = School::first(); + expect($newSchool)->toBeInstanceOf(School::class) + ->and($newSchool->name)->toEqual($this->newData['name']) + ->and($newSchool->address)->toEqual($this->newData['address']) + ->and($newSchool->city)->toEqual($this->newData['city']) + ->and($newSchool->state)->toEqual($this->newData['state']) + ->and($newSchool->zip)->toEqual($this->newData['zip']); + }); + + it('will not let us duplicate a name', function () { + $otherSchool = School::factory()->create(); + $this->newData['name'] = $otherSchool->name; + actAsAdmin(); + $this->post(route('admin.schools.store'), $this->newData)->assertSessionHasErrors('name'); + $this->assertEquals(School::count(), 1); + }); +}); + +describe('SchoolController::destroy', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + }); + it('denies access to a non-admin user', function () { + $this->delete(route('admin.schools.destroy', $this->school)) + ->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.schools.destroy', $this->school)) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->delete(route('admin.schools.destroy', $this->school)) + ->assertRedirect(route('dashboard')); + }); + + it('deletes a school', function () { + actAsAdmin(); + $this->delete(route('admin.schools.destroy', $this->school))->assertRedirect(route('admin.schools.index')); + expect(School::count())->toEqual(0); + }); + + it('will not delete a school with students', function () { + Student::factory()->forSchool($this->school)->create(); + actAsAdmin(); + $response = $this->delete(route('admin.schools.destroy', $this->school)); + $response->assertRedirect(route('admin.schools.index')) + ->assertSessionHas('error', 'You cannot delete a school that has students.'); + + expect(School::count())->toEqual(1); + }); +}); + +describe('SchoolController::add_domain', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + }); + it('denies access to a non-admin user', function () { + $this->post(route('admin.schools.add_domain', $this->school)) + ->assertRedirect(route('home')); + actAsNormal(); + $this->post(route('admin.schools.add_domain', $this->school)) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->post(route('admin.schools.add_domain', $this->school)) + ->assertRedirect(route('dashboard')); + }); + + it('can add a domain to a school', function () { + actAsAdmin(); + $this->post(route('admin.schools.add_domain', $this->school), + ['domain' => 'test.com'])->assertRedirect(route('admin.schools.show', $this->school)); + $this->school->refresh(); + expect($this->school->emailDomains)->toHaveCount(1) + ->and($this->school->emailDomains->first()->domain)->toEqual('test.com'); + }); +}); + +describe('SchoolController::remove_domain', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + $this->domain = SchoolEmailDomain::create([ + 'school_id' => $this->school->id, + 'domain' => 'test.com', + ]); + }); + it('denies access to a non-admin user', function () { + $this->delete(route('admin.schools.destroy_domain', $this->domain)) + ->assertRedirect(route('home')); + actAsNormal(); + $this->delete(route('admin.schools.destroy_domain', $this->domain)) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->delete(route('admin.schools.destroy_domain', $this->domain)) + ->assertRedirect(route('dashboard')); + }); + it('can remove a domain from a school', function () { + actAsAdmin(); + $this->delete(route('admin.schools.destroy_domain', $this->domain))->assertSessionHas('success', + 'Domain removed successfully.'); + $this->school->refresh(); + expect($this->school->emailDomains)->toHaveCount(0); + }); +}); + +describe('SchoolController::setHeadDirector', function () { + beforeEach(function () { + $this->school = School::factory()->create(); + $this->oldHeadDirector = User::factory()->forSchool($this->school)->create(); + $this->oldHeadDirector->addFlag('head_director'); + $this->newHeadDirector = User::factory()->forSchool($this->school)->create(); + $this->uneployedDirector = User::factory()->create(); + }); + it('denies access to a non-admin user', function () { + $this->get(route('admin.schools.set_head_director', [$this->school, $this->newHeadDirector])) + ->assertRedirect(route('home')); + actAsNormal(); + $this->get(route('admin.schools.set_head_director', [$this->school, $this->newHeadDirector])) + ->assertRedirect(route('dashboard')); + actAsTab(); + $this->get(route('admin.schools.set_head_director', [$this->school, $this->newHeadDirector])) + ->assertRedirect(route('dashboard')); + }); + it('can set a new head director', function () { + actAsAdmin(); + $this->get(route('admin.schools.set_head_director', [$this->school, $this->newHeadDirector])) + ->assertSessionHas('success', 'Head director set successfully.'); + $this->school->refresh(); + $this->oldHeadDirector->refresh(); + $this->newHeadDirector->refresh(); + expect($this->oldHeadDirector->hasFlag('head_director'))->toBeFalse() + ->and($this->newHeadDirector->hasFlag('head_director'))->toBeTrue(); + }); + it('will not promote a user a the wrong school', function () { + actAsAdmin(); + $this->get(route('admin.schools.set_head_director', [$this->school, $this->uneployedDirector])) + ->assertSessionHas('error', 'That user is not at that school'); + $this->school->refresh(); + $this->oldHeadDirector->refresh(); + $this->newHeadDirector->refresh(); + expect($this->oldHeadDirector->hasFlag('head_director'))->toBeTrue() + ->and($this->newHeadDirector->hasFlag('head_director'))->toBeFalse() + ->and($this->uneployedDirector->hasFlag('head_director'))->toBeFalse(); + }); +});