diff --git a/app/Actions/Fortify/UpdateUserPrivileges.php b/app/Actions/Fortify/UpdateUserPrivileges.php
new file mode 100644
index 0000000..dff3740
--- /dev/null
+++ b/app/Actions/Fortify/UpdateUserPrivileges.php
@@ -0,0 +1,55 @@
+setPrivilege($user, $action, $privilege);
+ }
+
+ /**
+ * @throws AuditionAdminException
+ */
+ public function setPrivilege(User|int $user, string $action, string $privilege): void
+ {
+ if (is_int($user)) {
+ $user = User::findOrFail($user);
+ }
+
+ if (! User::where('id', $user->id)->exists()) {
+ throw new AuditionAdminException('User does not exist');
+ }
+
+ if (! in_array($action, ['grant', 'revoke'])) {
+ throw new AuditionAdminException('Invalid Action');
+ }
+
+ $field = match ($privilege) {
+ 'admin' => 'is_admin',
+ 'tab' => 'is_tab',
+ default => throw new AuditionAdminException('Invalid Privilege'),
+ };
+
+ if ($user->$field == 1 && $action == 'revoke') {
+ $user->$field = 0;
+ $user->save();
+ }
+
+ if ($user->$field == 0 && $action == 'grant') {
+ $user->$field = 1;
+ $user->save();
+ }
+ }
+}
diff --git a/app/Actions/Schools/AssignUserToSchool.php b/app/Actions/Schools/AssignUserToSchool.php
index 7d27d54..d746d92 100644
--- a/app/Actions/Schools/AssignUserToSchool.php
+++ b/app/Actions/Schools/AssignUserToSchool.php
@@ -8,18 +8,22 @@ use App\Models\User;
class AssignUserToSchool
{
- public function __invoke(User $user, School $school): void
+ public function __invoke(User $user, School|int $school): void
{
$this->assign($user, $school);
}
- public function assign(User $user, School $school, bool $addDomainToSchool = true): void
+ public function assign(User $user, School|int $school, bool $addDomainToSchool = true): void
{
+ if (is_int($school)) {
+ $school = School::find($school);
+ }
+
if (! User::where('id', $user->id)->exists()) {
throw new AuditionAdminException('User does not exist');
}
- if (! School::where('id', $school->id)->exists()) {
+ if (is_null($school) || ! School::where('id', $school->id)->exists()) {
throw new AuditionAdminException('School does not exist');
}
diff --git a/app/Actions/Schools/SetHeadDirector.php b/app/Actions/Schools/SetHeadDirector.php
index e699061..596ec83 100644
--- a/app/Actions/Schools/SetHeadDirector.php
+++ b/app/Actions/Schools/SetHeadDirector.php
@@ -28,6 +28,10 @@ class SetHeadDirector
throw new AuditionAdminException('User does not exist');
}
+ if ($user->hasFlag('head_director')) {
+ return;
+ }
+
if (is_null($user->school_id)) {
throw new AuditionAdminException('User is not associated with a school');
}
diff --git a/app/Http/Controllers/Admin/UserController.php b/app/Http/Controllers/Admin/UserController.php
index 77c7a0a..2c79a2d 100644
--- a/app/Http/Controllers/Admin/UserController.php
+++ b/app/Http/Controllers/Admin/UserController.php
@@ -1,7 +1,13 @@
is_admin) {
- abort(403);
- }
$users = User::with('school')->with('flags')->orderBy('last_name')->orderBy('first_name')->get();
return view('admin.users.index', ['users' => $users]);
@@ -30,9 +29,7 @@ class UserController extends Controller
public function edit(User $user)
{
- if (! Auth::user()->is_admin) {
- abort(403);
- }
+
$schools = School::orderBy('name')->get();
return view('admin.users.edit', ['user' => $user, 'schools' => $schools]);
@@ -40,85 +37,53 @@ class UserController extends Controller
public function create()
{
- if (! Auth::user()->is_admin) {
- abort(403);
- }
+
$schools = School::orderBy('name')->get();
return view('admin.users.create', ['schools' => $schools]);
}
- public function update(Request $request, User $user, SetHeadDirector $headSetter)
- {
- if (! Auth::user()->is_admin) {
- abort(403);
- }
- $oldEmail = $user->email;
- $wasAdmin = $user->is_admin;
- $wasTab = $user->is_tab;
- $validData = $request->validate([
- 'first_name' => ['required'],
- 'last_name' => ['required'],
- 'email' => ['required', 'email'],
- 'cell_phone' => ['required'],
- 'judging_preference' => ['required'],
- 'school_id' => ['nullable', 'exists:schools,id'],
- ]);
- $validData['is_admin'] = $request->get('is_admin') == 'on' ? 1 : 0;
- $validData['is_tab'] = $request->get('is_tab') == 'on' ? 1 : 0;
- $validData['is_head'] = $request->get('is_head') == 'on' ? 1 : 0;
- $user->update([
- 'first_name' => $validData['first_name'],
- 'last_name' => $validData['last_name'],
- 'email' => $validData['email'],
- 'cell_phone' => $validData['cell_phone'],
- 'judging_preference' => $validData['judging_preference'],
- 'school_id' => $validData['school_id'],
- 'is_admin' => $validData['is_admin'],
- 'is_tab' => $validData['is_tab'],
- ]);
- $user->refresh();
- $logged_school = $user->school_id ? $user->school->name : 'No School';
- $message = 'Updated user #'.$user->id.' - '.$oldEmail
- .'
Name: '.$user->full_name()
- .'
Email: '.$user->email
- .'
Cell Phone: '.$user->cell_phone
- .'
Judging Pref: '.$user->judging_preference
- .'
School: '.$logged_school;
+ public function update(
+ Request $request,
+ User $user,
+ SetHeadDirector $headSetter,
+ UpdateUserProfileInformation $profileUpdater,
+ AssignUserToSchool $schoolAssigner,
+ UpdateUserPrivileges $privilegesUpdater
+ ) {
+ // Update basic profile data
+ $profileData = [
+ 'first_name' => $request->get('first_name'),
+ 'last_name' => $request->get('last_name'),
+ 'email' => $request->get('email'),
+ 'cell_phone' => $request->get('cell_phone'),
+ 'judging_preference' => $request->get('judging_preference'),
+ ];
+ $profileUpdater->update($user, $profileData);
- AuditLogEntry::create([
- 'user' => auth()->user()->email,
- 'ip_address' => request()->ip(),
- 'message' => $message,
- 'affected' => ['users' => [$user->id]],
- ]);
- if ($user->is_admin != $wasAdmin) {
- $messageStart = $user->is_admin ? 'Granted admin privileges to ' : 'Revoked admin privileges from ';
- AuditLogEntry::create([
- 'user' => auth()->user()->email,
- 'ip_address' => request()->ip(),
- 'message' => $messageStart.$user->full_name().' - '.$user->email,
- 'affected' => ['users' => [$user->id]],
- ]);
+ // Deal with school assignment
+ if ($user->school_id != $request->get('school_id')) {
+ $schoolAssigner($user, $request->get('school_id'));
}
- if ($user->is_tab != $wasTab) {
- $messageStart = $user->is_tab ? 'Granted tabulation privileges to ' : 'Revoked tabulation privileges from ';
- AuditLogEntry::create([
- 'user' => auth()->user()->email,
- 'ip_address' => request()->ip(),
- 'message' => $messageStart.$user->full_name().' - '.$user->email,
- 'affected' => ['users' => [$user->id]],
- ]);
+
+ // Deal with the head director flag
+ if ($request->has('head_director')) {
+ $headSetter($user);
+ } else {
+ $user->removeFlag('head_director');
}
- if ($user->hasFlag('head_director') != $validData['is_head'] && ! is_null($user->school_id)) {
- if ($validData['is_head']) {
- $headSetter->setHeadDirector($user);
- } else {
- $user->removeFlag('head_director');
- $logMessage = 'Removed '.$user->full_name().' as head director at '.$user->school->name;
- $logAffected = ['users' => [$user->id], 'schools' => [$user->school_id]];
- auditionLog($logMessage, $logAffected);
- }
+
+ // Deal with privileges
+ if ($request->has('is_admin')) {
+ $privilegesUpdater($user, 'grant', 'admin');
+ } else {
+ $privilegesUpdater($user, 'revoke', 'admin');
+ }
+
+ if ($request->has('is_tab')) {
+ $privilegesUpdater($user, 'grant', 'tab');
+ } else {
+ $privilegesUpdater($user, 'revoke', 'tab');
}
return redirect('/admin/users');
@@ -126,60 +91,23 @@ class UserController extends Controller
public function store(Request $request)
{
- $request->validate([
- 'first_name' => ['required'],
- 'last_name' => ['required'],
- 'email' => ['required', 'email', 'unique:users'],
- ]);
-
- // Generate a random password
+ $userCreator = app(CreateNewUser::class);
$randomPassword = Str::random(12);
-
- $user = User::make([
- 'first_name' => request('first_name'),
- 'last_name' => request('last_name'),
- 'email' => request('email'),
- 'cell_phone' => request('cell_phone'),
- 'judging_preference' => request('judging_preference'),
- 'password' => Hash::make($randomPassword),
+ $data = request()->all();
+ $data['password'] = $randomPassword;
+ $data['password_confirmation'] = $randomPassword;
+ $newDirector = $userCreator->create($data);
+ $newDirector->update([
+ 'school_id' => $request->get('school_id') ?? null,
]);
- if (! is_null(request('school_id'))) {
- $request->validate([
- 'school_id' => ['exists:schools,id'],
- ]);
- }
- $user->school_id = request('school_id');
- $user->save();
- $message = 'Created user '.$user->email.' - '.$user->full_name().'
Cell Phone: '.$user->cell_phone.'
Judging Pref: '.$user->judging_preference;
- AuditLogEntry::create([
- 'user' => auth()->user()->email,
- 'ip_address' => request()->ip(),
- 'message' => $message,
- 'affected' => ['users' => [$user->id]],
- ]);
- if ($user->school_id) {
- $message = 'Set user '.$user->full_name().' ('.$user->email.') as a director at '.$user->school->name.'(#'.$user->school->id.')';
- AuditLogEntry::create([
- 'user' => auth()->user()->email,
- 'ip_address' => request()->ip(),
- 'message' => $message,
- 'affected' => [
- 'users' => [$user->id],
- 'schools' => [$user->id],
- ],
- ]);
- }
- Mail::to($user->email)->send(new NewUserPassword($user, $randomPassword));
+ Mail::to($newDirector->email)->send(new NewUserPassword($newDirector, $randomPassword));
- return redirect('/admin/users');
+ return redirect(route('admin.users.index'))->with('success', 'Director added');
}
public function destroy(User $user)
{
- if (! Auth::user()->is_admin) {
- abort(403);
- }
$message = 'Deleted user '.$user->email;
AuditLogEntry::create([
'user' => auth()->user()->email,
diff --git a/app/Http/Controllers/Admin/YearEndResetController.php b/app/Http/Controllers/Admin/YearEndResetController.php
index 52d81d2..b3d6d87 100644
--- a/app/Http/Controllers/Admin/YearEndResetController.php
+++ b/app/Http/Controllers/Admin/YearEndResetController.php
@@ -16,7 +16,7 @@ class YearEndResetController extends Controller
public function execute()
{
- $cleanUpProcedure = new YearEndCleanup;
+ $cleanUpProcedure = app(YearEndCleanup::class);
$options = request()->options;
$cleanUpProcedure($options);
auditionLog('Executed year end reset.', []);
diff --git a/app/Observers/UserObserver.php b/app/Observers/UserObserver.php
index 9c3ac0a..0477207 100644
--- a/app/Observers/UserObserver.php
+++ b/app/Observers/UserObserver.php
@@ -50,7 +50,7 @@ class UserObserver
//Log if we removed a school
if ($oldSchool && ! $newSchool) {
$message = 'Removed '.$user->full_name().' from '.$oldSchool;
- $affected = ['users' => [$user->id], 'schools' => [$user->getOrigianl('school_id')]];
+ $affected = ['users' => [$user->id], 'schools' => [$user->getOriginal('school_id')]];
auditionLog($message, $affected);
}
diff --git a/tests/Feature/app/Http/Controllers/Admin/UserControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/UserControllerTest.php
new file mode 100644
index 0000000..1f92440
--- /dev/null
+++ b/tests/Feature/app/Http/Controllers/Admin/UserControllerTest.php
@@ -0,0 +1,220 @@
+user = User::factory()->create();
+});
+
+afterEach(function () {
+ Mockery::close();
+});
+
+describe('UserController::index', function () {
+ it('denies access to a non-admin user', function () {
+ $this->get(route('admin.users.index'))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->get(route('admin.users.index'))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->get(route('admin.users.index'))->assertRedirect(route('dashboard'));
+ });
+
+ it('allows access for an admin user', function () {
+ actAsAdmin();
+ $users = User::factory()->count(5)->create();
+ $response = $this->get(route('admin.users.index'));
+ $response->assertOk()->assertViewIs('admin.users.index')->assertViewHas('users');
+
+ // Check if each $users is in the array of users sent to the view
+ $userIdsSentToView = $response->viewData('users')->pluck('id')->toArray();
+ expect(in_array($this->user->id, $userIdsSentToView))->toBeTrue();
+ foreach ($users as $user) {
+ expect(in_array($user->id, $userIdsSentToView))->toBeTrue();
+ }
+
+ });
+});
+
+describe('UserController::edit', function () {
+ it('denies access to a non-admin user', function () {
+ $this->get(route('admin.users.edit', $this->user))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->get(route('admin.users.edit', $this->user))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->get(route('admin.users.edit', $this->user))->assertRedirect(route('dashboard'));
+ });
+
+ it('allows access for an admin user', function () {
+ $schools = School::factory()->count(5)->create();
+ actAsAdmin();
+ $response = $this->get(route('admin.users.edit', $this->user));
+ $response->assertOk()->assertViewIs('admin.users.edit')->assertViewHas(['schools', 'user']);
+ expect($response->viewData('user')->id)->toEqual($this->user->id);
+ foreach ($schools as $school) {
+ expect(in_array($school->id, $response->viewData('schools')->pluck('id')->toArray()))->toBeTrue();
+ }
+ });
+});
+
+describe('UserController::create', function () {
+ it('denies access to a non-admin user', function () {
+ $this->get(route('admin.users.create'))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->get(route('admin.users.create'))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->get(route('admin.users.create'))->assertRedirect(route('dashboard'));
+ });
+
+ it('allows access for an admin user', function () {
+ actAsAdmin();
+ $schools = School::factory()->count(5)->create();
+ $response = $this->get(route('admin.users.create'));
+ $response->assertOk()->assertViewIs('admin.users.create')->assertViewHas(['schools']);
+ foreach ($schools as $school) {
+ expect(in_array($school->id, $response->viewData('schools')->pluck('id')->toArray()))->toBeTrue();
+ }
+ });
+});
+
+describe('UserController::update', function () {
+ beforeEach(function () {
+ $this->oldSchool = School::factory()->create();
+ $this->newSchool = School::factory()->create();
+ $this->oldUser = User::create([
+ 'first_name' => 'Old',
+ 'last_name' => 'Name',
+ 'email' => 'picard@starfleet.com',
+ 'cell_phone' => '1701',
+ 'judging_preference' => 'light counting',
+ 'school_id' => $this->oldSchool->id,
+ 'password' => \Illuminate\Support\Facades\Hash::make('password'),
+ ]);
+ });
+ it('denies access to a non-admin user', function () {
+ $this->patch(route('admin.users.update', $this->user))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->patch(route('admin.users.update', $this->user))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->patch(route('admin.users.update', $this->user))->assertRedirect(route('dashboard'));
+ });
+ it('updates user profile information', function () {
+ actAsAdmin();
+ $response = $this->patch(route('admin.users.update', $this->oldUser), [
+ 'first_name' => 'New',
+ 'last_name' => 'Family',
+ 'email' => 'skywalker@rebellion.org',
+ 'cell_phone' => '555-555-5555',
+ 'judging_preference' => 'light sabers',
+ 'school_id' => $this->newSchool->id,
+ ]);
+ //file_put_contents(storage_path('debug.html'), $response->getContent());
+ $response->assertRedirect(route('admin.users.index'));
+ $this->oldUser->refresh();
+ expect($this->oldUser->first_name)->toBe('New')
+ ->and($this->oldUser->last_name)->toBe('Family')
+ ->and($this->oldUser->email)->toBe('skywalker@rebellion.org')
+ ->and($this->oldUser->cell_phone)->toBe('555-555-5555')
+ ->and($this->oldUser->judging_preference)->toBe('light sabers')
+ ->and($this->oldUser->school_id)->toBe($this->newSchool->id);
+ });
+ it('assigns privileges to a user', function () {
+ actAsAdmin();
+ $this->patch(route('admin.users.update', $this->oldUser), [
+ 'first_name' => 'Jean Luc',
+ 'last_name' => 'Picard',
+ 'email' => 'skywalker@rebellion.org',
+ 'cell_phone' => '1701',
+ 'judging_preference' => 'light sabers',
+ 'school_id' => $this->newSchool->id,
+ 'is_admin' => 'on',
+ 'is_tab' => 'on',
+ 'head_director' => 'on',
+ ]);
+ //file_put_contents(storage_path('debug.html'), $response->getContent());
+ $this->oldUser->refresh();
+ expect($this->oldUser->is_admin)->toBeTruthy()
+ ->and($this->oldUser->is_tab)->toBeTruthy();
+
+ $this->patch(route('admin.users.update', $this->oldUser), [
+ 'first_name' => 'Luke',
+ 'last_name' => 'Skywalker',
+ 'email' => 'skywalker@rebellion.org',
+ 'cell_phone' => '555-555-5555',
+ 'judging_preference' => 'light sabers',
+ 'school_id' => $this->newSchool->id,
+ ]);
+ $this->oldUser->refresh();
+ expect($this->oldUser->is_admin)->toBeFalsy()
+ ->and($this->oldUser->is_tab)->toBeFalsy();
+ });
+});
+
+describe('UserController::store', function () {
+ it('denies access to a non-admin user', function () {
+ $this->post(route('admin.users.store', $this->user))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->post(route('admin.users.store', $this->user))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->post(route('admin.users.store', $this->user))->assertRedirect(route('dashboard'));
+ });
+
+ it('creates a new user', function () {
+ actAsAdmin();
+ $school = School::factory()->create();
+ $response = $this->post(route('admin.users.store', [
+ 'first_name' => 'Jean Luc',
+ 'last_name' => 'Picard',
+ 'email' => 'picard@starfleet.com',
+ 'cell_phone' => '1701',
+ 'judging_preference' => 'light counting',
+ 'school_id' => $school->id,
+ ]));
+ //file_put_contents(storage_path('debug.html'), $response->getContent());
+ $response->assertRedirect(route('admin.users.index'));
+ $user = User::orderBy('id', 'desc')->first();
+ expect($user->first_name)->toBe('Jean Luc')
+ ->and($user->last_name)->toBe('Picard')
+ ->and($user->email)->toBe('picard@starfleet.com')
+ ->and($user->cell_phone)->toBe('1701')
+ ->and($user->judging_preference)->toBe('light counting')
+ ->and($user->school->id)->toBe($school->id);
+ });
+ it('sends an email upon user creation', function () {
+ Mail::fake();
+ actAsAdmin();
+ $school = School::factory()->create();
+ $this->post(route('admin.users.store', [
+ 'first_name' => 'Jean Luc',
+ 'last_name' => 'Picard',
+ 'email' => 'picard@starfleet.com',
+ 'cell_phone' => '1701',
+ 'judging_preference' => 'light counting',
+ 'school_id' => $school->id,
+ ]));
+ Mail::assertSent(NewUserPassword::class, function ($mail) {
+ return $mail->hasTo('picard@starfleet.com');
+ });
+ });
+});
+
+describe('UserController::destroy', function () {
+ it('denies access to a non-admin user', function () {
+ $this->delete(route('admin.users.destroy', $this->user))->assertRedirect(route('home'));
+ actAsNormal();
+ $this->delete(route('admin.users.destroy', $this->user))->assertRedirect(route('dashboard'));
+ actAsTab();
+ $this->delete(route('admin.users.destroy', $this->user))->assertRedirect(route('dashboard'));
+ });
+ it('deletes a user', function () {
+ actAsAdmin();
+ $response = $this->delete(route('admin.users.destroy', $this->user));
+ $response->assertRedirect(route('admin.users.index'));
+ $response->assertSessionHas('success', 'User deleted successfully');
+ expect(User::where('id', $this->user->id)->exists())->toBeFalsy();
+ });
+});
diff --git a/tests/Feature/app/Http/Controllers/Admin/YearEndResetControllerTest.php b/tests/Feature/app/Http/Controllers/Admin/YearEndResetControllerTest.php
new file mode 100644
index 0000000..66226c5
--- /dev/null
+++ b/tests/Feature/app/Http/Controllers/Admin/YearEndResetControllerTest.php
@@ -0,0 +1,31 @@
+get(route('admin.year_end_procedures'));
+ $response->assertRedirect(route('dashboard'));
+});
+
+it('shows options for year end reset', function () {
+ actAsAdmin();
+ $response = $this->get(route('admin.year_end_procedures'));
+ $response->assertOk()
+ ->assertViewIs('admin.year_end_reset')
+ ->assertSee('removeAuditionsFromRoom')
+ ->assertSee('unassignJudges');
+});
+
+it('calls the YearEndCleanup action', function () {
+ $mock = Mockery::mock(YearEndCleanup::class);
+ $mock->shouldReceive('__invoke')->once();
+ app()->instance(YearEndCleanup::class, $mock);
+ actAsAdmin();
+ $response = $this->post(route('admin.year_end_procedures'));
+ $response->assertRedirect(route('dashboard'))
+ ->with('success', 'Year end cleanup completed. ');
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index 54d4edf..43867b7 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -65,6 +65,11 @@ function loadSampleAudition()
artisan('db:seed', ['--class' => 'AuditionWithScoringGuideAndRoom']);
}
+function saveContentLocally($content)
+{
+ file_put_contents(storage_path('app/storage/debug.html'), $content);
+}
+
uses()->beforeEach(function () {
Settings::set('auditionName', 'Somewhere Band Directors Association');
Settings::set('auditionAbbreviation', 'SBDA');