Write tests - Write tests for what was done to this point that will be kept #11

Merged
okorpheus merged 61 commits from write-tests into master 2024-07-05 21:21:32 +00:00
19 changed files with 418 additions and 64 deletions
Showing only changes of commit f400b2bda5 - Show all commits

View File

@ -5,10 +5,10 @@ namespace App\Http\Controllers;
use App\Models\Audition;
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use App\Rules\UniqueFullNameAtSchool;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use function abort;
use function redirect;
@ -20,9 +20,13 @@ class StudentController extends Controller
*/
public function index()
{
if (! Auth::user()->school_id) {
return redirect()->route('dashboard');
}
$students = Auth::user()->students()->with('entries')->get();
$auditions = Audition::all();
return view('students.index',['students' => $students, 'auditions' => $auditions]);
return view('students.index', ['students' => $students, 'auditions' => $auditions]);
}
/**
@ -38,21 +42,26 @@ class StudentController extends Controller
*/
public function store(Request $request)
{
if ($request->user()->cannot('create', Student::class)) abort(403);
if ($request->user()->cannot('create', Student::class)) {
abort(403);
}
$request->validate([
'first_name' => ['required'],
'last_name' => ['required', new UniqueFullNameAtSchool(request('first_name'),request('last_name'), Auth::user()->school_id)],
'grade' => ['required', 'integer'],
'last_name' => [
'required',
new UniqueFullNameAtSchool(request('first_name'), request('last_name'), Auth::user()->school_id),
],
'grade' => ['required', 'integer'],
]);
$student = Student::create([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade'),
'school_id' => Auth::user()->school_id
'school_id' => Auth::user()->school_id,
]);
$request->session()->put('auditionMessages',['success','I did it again ma']);
$request->session()->put('auditionMessages', ['success', 'I did it again ma']);
return redirect('/students');
}
@ -70,7 +79,10 @@ class StudentController extends Controller
*/
public function edit(Request $request, Student $student)
{
if ($request->user()->cannot('update', $student)) abort(403);
if ($request->user()->cannot('update', $student)) {
abort(403);
}
return view('students.edit', ['student' => $student]);
}
@ -79,22 +91,23 @@ class StudentController extends Controller
*/
public function update(Request $request, Student $student)
{
if ($request->user()->cannot('update', $student)) abort(403);
if ($request->user()->cannot('update', $student)) {
abort(403);
}
request()->validate([
'first_name' => ['required'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
'last_name' => ['required'],
'grade' => ['required', 'integer'],
]);
$student->update([
'first_name' => request('first_name'),
'last_name' => request('last_name'),
'grade' => request('grade')
'grade' => request('grade'),
]);
// TODO if a students grade is changed, we need to be sure they are still eligible for the auditions in which they are entered.
return redirect('/students');
}
@ -103,8 +116,11 @@ class StudentController extends Controller
*/
public function destroy(Request $request, Student $student)
{
if ($request->user()->cannot('delete', $student)) abort(403);
if ($request->user()->cannot('delete', $student)) {
abort(403);
}
$student->delete();
return redirect('/students');
}
}

View File

@ -20,7 +20,7 @@ class CheckIfAdmin
return $next($request);
}
return redirect('/')->with('error', 'You do not have admin access.');
return redirect(route('home'))->with('error', 'You do not have admin access.');
}
}

View File

@ -4,7 +4,10 @@ namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
use function redirect;
use function route;
class CheckIfHasSchool
{
@ -15,6 +18,11 @@ class CheckIfHasSchool
*/
public function handle(Request $request, Closure $next): Response
{
return $next($request);
if (Auth::check() && Auth::user()->school_id) {
return $next($request);
}
return redirect(route('dashboard'))->with('error', 'You do not have a school to view students for.');
}
}

View File

@ -10,8 +10,6 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use function now;
class Audition extends Model
{
use HasFactory;
@ -26,7 +24,6 @@ class Audition extends Model
protected $scored_entries_count; //Set by TabulationService
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
@ -175,12 +172,14 @@ class Audition extends Model
}
$this->flags()->create(['flag_name' => $flag]);
$this->load('flags');
}
public function removeFlag($flag): void
{
// remove related auditionFlag where flag_name = $flag
$this->flags()->where('flag_name', $flag)->delete();
$this->load('flags');
}
public function scopeOpen(Builder $query): void

View File

@ -5,7 +5,7 @@ namespace App\Policies;
use App\Models\Entry;
use App\Models\Student;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use function is_null;
class StudentPolicy
@ -31,7 +31,10 @@ class StudentPolicy
*/
public function create(User $user): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
return ! is_null($user->school_id);
}
@ -41,7 +44,10 @@ class StudentPolicy
public function update(User $user, Student $student): bool
{
if($user->is_admin) return true;
if ($user->is_admin) {
return true;
}
return $user->school_id == $student->school_id;
}
@ -50,7 +56,10 @@ class StudentPolicy
*/
public function delete(User $user, Student $student): bool
{
if (Entry::where('student_id','=',$student->id)->exists()) return false; // Don't allow deletion of a student with entries
if (Entry::where('student_id', '=', $student->id)->exists()) {
return false;
} // Don't allow deletion of a student with entries
return $user->school_id == $student->school_id;
}

View File

@ -19,7 +19,7 @@ class UniqueFullNameAtSchool implements ValidationRule
$this->school_id = $schoolID;
}
public function passes($attributies, $value)
public function studentExists()
{
return Student::where('first_name', $this->first_name)
->where('last_name', $this->last_name)
@ -38,6 +38,8 @@ class UniqueFullNameAtSchool implements ValidationRule
*/
public function validate(string $attribute, mixed $value, Closure $fail): void
{
//
if($this->studentExists()) {
$fail($this->message());
}
}
}

View File

@ -41,7 +41,7 @@ class AuditionFactory extends Factory
return [
'event_id' => $event->id,
'name' => $this->faker->randomElement($instruments).$this->faker->randomNumber(1),
'name' => $this->faker->randomElement($instruments).$this->faker->randomNumber(3),
'score_order' => 1,
'entry_deadline' => Carbon::tomorrow(),
'entry_fee' => 1000,

View File

@ -17,8 +17,8 @@ class RoomFactory extends Factory
public function definition(): array
{
return [
'name' => 'Room ' . fake()->numberBetween(7,500),
'description' => fake()->sentence()
'name' => 'Room '.fake()->numberBetween(7, 500),
'description' => fake()->sentence(),
];
}
}

View File

@ -17,7 +17,7 @@ class ScoringGuideFactory extends Factory
public function definition(): array
{
return [
//
'name' => $this->faker->sentence(3),
];
}
}

View File

@ -6,7 +6,7 @@ use App\Models\SiteSetting;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class SampleSettings extends Seeder
class SampleSettingsSeeder extends Seeder
{
/**
* Run the database seeds.
@ -55,10 +55,6 @@ class SampleSettings extends Seeder
]);
SiteSetting::create([
'setting_key' => 'school_fee',
'setting_value' => 'SBDA',
]);
SiteSetting::create([
'setting_key' => 'auditionAbbreviation',
'setting_value' => '2500',
]);
SiteSetting::create([
@ -66,12 +62,16 @@ class SampleSettings extends Seeder
'setting_value' => '143 Sousa Lane',
]);
SiteSetting::create([
'setting_key' => 'auditionAbbreviation',
'setting_value' => 'SBDA',
'setting_key' => 'payment_city',
'setting_value' => 'Maud',
]);
SiteSetting::create([
'setting_key' => 'auditionAbbreviation',
'setting_value' => 'SBDA',
'setting_key' => 'payment_state',
'setting_value' => 'OK',
]);
SiteSetting::create([
'setting_key' => 'payment_zip',
'setting_value' => '77777',
]);
}
}

View File

@ -2,7 +2,7 @@
{{-- <button type="button" class="inline-flex items-center gap-x-1 text-sm font-semibold leading-6 text-gray-900" aria-expanded="false" @on:click=" open = ! open">--}}
<button type="button" class="inline-flex items-center gap-x-1 text-white rounded-md px-3 py-2 text-sm font-medium hover:bg-indigo-500 hover:bg-opacity-75" aria-expanded="false" @click=" open = ! open" @click.outside=" open = false">
<span>Admin</span>
<span>Administration</span>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" clip-rule="evenodd" />
</svg>

View File

@ -14,16 +14,16 @@
</div>
<div class="hidden md:block">
<div class="ml-10 flex items-baseline space-x-4">
<x-layout.navbar.nav-link href="/dashboard" :active="request()->is('dashboard')">Dashboard
<x-layout.navbar.nav-link href="{{ route('dashboard') }}" :active="request()->is('dashboard')">Dashboard
</x-layout.navbar.nav-link>
@if(Auth::user()->school_id)
<x-layout.navbar.nav-link href="/students" :active="request()->is('students')">Students
<x-layout.navbar.nav-link href="{{ route('students.index') }}" :active="request()->is('students')">Students
</x-layout.navbar.nav-link>
<x-layout.navbar.nav-link href="/entries" :active="request()->is('entries')">Entries
<x-layout.navbar.nav-link href="{{ route('entries.index') }}" :active="request()->is('entries')">Entries
</x-layout.navbar.nav-link>
@endif
@if(Auth::user()->isJudge() AND Settings::get('judging_enabled'))
<x-layout.navbar.nav-link href="/judging" :active="request()->is('judging')">Judging
<x-layout.navbar.nav-link href="{{ route('judging.index') }}" :active="request()->is('judging')">Judging
</x-layout.navbar.nav-link>
@endif
@if(Auth::user()->is_admin)
@ -164,8 +164,7 @@
<!-- Current: "bg-indigo-700 text-white", Default: "text-white hover:bg-indigo-500 hover:bg-opacity-75" -->
<a href="/dashboard" class="bg-indigo-700 text-white block rounded-md px-3 py-2 text-base font-medium"
aria-current="page">Dashboard</a>
<a href="/students"
class="text-white hover:bg-indigo-500 hover:bg-opacity-75 block rounded-md px-3 py-2 text-base font-medium">Students</a>
</div>
<div class="border-t border-indigo-700 pb-3 pt-4">
<div class="flex items-center px-5">

View File

@ -2,7 +2,7 @@
<x-layout.app>
<x-slot:page_title>Dashboard</x-slot:page_title>
@if(! Auth::user()->school_id)
You aren't currently associated with a school. <a href="/my_school" class="text-blue-600">Click here to choose or create one.</a>
<p class="pb-5">You aren't currently associated with a school. <a href="/my_school" class="text-blue-600">Click here to choose or create one.</a></p>
@endif
<div class="grid sm:grid-cols-2 md:grid-cols-4">
<div>{{-- Column 1 --}}

View File

@ -19,32 +19,32 @@ Route::middleware(['auth', 'verified'])->group(function () {
// Entry Related Routes
Route::middleware(['auth', 'verified', 'can:create,App\Models\Entry'])->controller(EntryController::class)->group(function () {
Route::get('/entries', 'index');
Route::get('/entries/create', 'create');
Route::post('/entries', 'store');
Route::delete('/entries/{entry}', 'destroy');
Route::get('/entries', 'index')->name('entries.index');
Route::get('/entries/create', 'create')->name('entries.create');
Route::post('/entries', 'store')->name('entries.store');
Route::delete('/entries/{entry}', 'destroy')->name('entries.destroy');
});
// User Related Routes
Route::middleware(['auth', 'verified'])->controller(UserController::class)->group(function () {
Route::patch('/users/{user}/set_school', 'set_school');
Route::patch('/users/{$user}', 'update');
Route::patch('/users/{user}/set_school', 'set_school')->name('users.set_school');
Route::patch('/users/{$user}', 'update')->name('users.update');
});
// Student Related Routes
Route::middleware(['auth', 'verified', 'can:create,App\Models\Student'])->controller(StudentController::class)->group(function () {
Route::get('/students', 'index');
Route::post('students', 'store');
Route::get('/students/{student}/edit', 'edit');
Route::patch('/students/{student}', 'update');
Route::delete('/students/{student}', 'destroy');
Route::get('/students', 'index')->name('students.index');
Route::post('students', 'store')->name('students.store');
Route::get('/students/{student}/edit', 'edit')->name('students.edit');
Route::patch('/students/{student}', 'update')->name('students.update');
Route::delete('/students/{student}', 'destroy')->name('students.destroy');
});
// School Related Routes
Route::middleware(['auth', 'verified'])->controller(SchoolController::class)->group(function () {
Route::get('/schools/create', 'create');
Route::post('/schools', 'store');
Route::get('/schools/{school}/edit', 'edit');
Route::get('/schools/{school}', 'show')->name('schools.show');
Route::patch('/schools/{school}', 'update');
Route::get('/schools/create', 'create')->name('schools.create');
Route::post('/schools', 'store')->name('schools.store');
Route::get('/schools/{school}/edit', 'edit')->name('schools.edit');
Route::get('/schools/{school}', 'show')->name('schools.show')->name('schools.show');
Route::patch('/schools/{school}', 'update')->name('schools.update');
});

View File

@ -0,0 +1,21 @@
<?php
use App\Models\Audition;
use App\Models\AuditionFlag;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('has an audition', function () {
$audition = Audition::factory()->create();
// Arrange
$flag = AuditionFlag::create([
'audition_id' => $audition->id,
'flag_name' => 'Test Flag',
]);
// Act and Assert
expect($flag->audition->name)->toBe($audition->name);
});

View File

@ -1,6 +1,12 @@
<?php
use App\Models\Audition;
use App\Models\AuditionFlag;
use App\Models\Entry;
use App\Models\Event;
use App\Models\Room;
use App\Models\ScoringGuide;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
@ -63,3 +69,102 @@ it('only returns published advancement auditions for advancementPublished scope'
->first()->id->toEqual($published->id);
});
it('has an event', function () {
// Arrange
$event = Event::factory()->create(['name' => 'Symphonic Concert Wind Ensemble Band']);
$audition = Audition::factory()->create(['event_id' => $event->id]);
// Act & Assert
expect($audition->event->name)->toEqual('Symphonic Concert Wind Ensemble Band');
});
it('has entries', function () {
// Arrange
$audition = Audition::factory()->create();
Entry::factory()->count(10)->create(['audition_id' => $audition->id]);
// Act & Assert
expect($audition->entries->count())->toEqual(10);
});
it('has a room', function () {
// Arrange
$room = Room::factory()->create(['name' => 'Room 1']);
$audition = Audition::factory()->create(['room_id' => $room->id]);
// Act & Assert
expect($audition->room->name)->toEqual('Room 1');
});
it('has a scoring guide', function () {
// Arrange
$sg = ScoringGuide::factory()->create(['name' => 'Sight Reading']);
$audition = Audition::factory()->create(['scoring_guide_id' => $sg->id]);
// Act & Assert
expect($audition->scoringGuide->name)->toEqual('Sight Reading');
});
it('displays the entry fee', function () {
// Arrange
$audition = Audition::factory()->create(['entry_fee' => 1000]);
// Act & Assert
expect($audition->display_fee())->toEqual('$10.00');
});
it('has many judges', function () {
// Arrange
$room = Room::factory()->create();
$audition = Audition::factory()->create(['room_id' => $room->id]);
$judges = User::factory()->count(5)->create();
foreach ($judges as $judge) {
$room->addJudge($judge->id);
}
// Act & Assert
expect($audition->judges->count())->toEqual(5);
});
it('has a judges_count available', function () {
// Arrange
$room = Room::factory()->create();
$audition = Audition::factory()->create(['room_id' => $room->id]);
$judges = User::factory()->count(5)->create();
foreach ($judges as $judge) {
$room->addJudge($judge->id);
}
// Act & Assert
expect($audition->judges_count)->toEqual(5);
});
it('can have flags', function () {
// Arrange
$audition = Audition::factory()->create();
AuditionFlag::create(['audition_id' => $audition->id, 'flag_name' => 'seats_published']);
AuditionFlag::create(['audition_id' => $audition->id, 'flag_name' => 'advance_published']);
// Act
// Assert
expect($audition->hasFlag('seats_published'))->toBeTrue();
expect($audition->hasFlag('notaflag'))->toBeFalse();
expect($audition->flags->count())->toEqual(2);
});
it('can add flags', function () {
// Arrange
$audition = Audition::factory()->create();
// Act
$audition->addFlag('seats_published');
// Assert
expect($audition->hasFlag('seats_published'))->toBeTrue();
});
it('can remove flags', function () {
// Arrange
$audition = Audition::factory()->create();
AuditionFlag::create(['audition_id' => $audition->id, 'flag_name' => 'seats_published']);
// Act & Assert
$audition->addFlag('seats_published');
expect($audition->hasFlag('seats_published'))->toBeTrue();
$audition->removeFlag('seats_published');
expect($audition->hasFlag('seats_published'))->toBeFalse();
});

View File

@ -1,5 +1,56 @@
<?php
it('', function () {
use App\Models\School;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\get;
uses(RefreshDatabase::class);
it('only shows Students and Entries menu options if the user has a school', function () {
// Act & Assert
$user = User::factory()->create();
$this->actingAs($user);
get(route('dashboard'))
->assertStatus(200)
->assertSeeText('Dashboard')
->assertDontSeeText('Students')
->assertDontSeeText('Entries');
$school = School::factory()->create();
$user->school_id = $school->id;
$user->save();
get(route('dashboard'))
->assertStatus(200)
->assertSeeText('My School')
->assertSeeText('Dashboard')
->assertSeeText('Students')
->assertSeeText('Entries');
});
it('only shows Admin menu if an administrator only shows Tabulation if admin or tabulator', function () {
// Arrange
$user = User::factory()->create();
$adminUser = User::factory()->admin()->create();
$tabUser = User::factory()->tab()->create();
// Act & Assert
$this->actingAs($user);
get(route('dashboard'))
->assertStatus(200)
->assertDontSeeText('Administration');
$this->actingAs($adminUser);
get(route('dashboard'))
->assertStatus(200)
->assertSeeText('Administration')
->assertSeeText('Tabulation');
$this->actingAs($tabUser);
get(route('dashboard'))
->assertStatus(200)
->assertDontSeeText('Administration')
->assertSeeText('Tabulation');
});

View File

@ -1,5 +1,134 @@
<?php
it('', function () {
use App\Models\School;
use App\Models\Student;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use function Pest\Laravel\get;
use function Pest\Laravel\post;
uses(RefreshDatabase::class);
it('cannot be accessed by a guest', function () {
// Act & Assert
get(route('students.index'))
->assertStatus(302)
->assertRedirect(route('home'));
});
it('cannot be accessed by a user without a school', function () {
// Arrange
$user = User::factory()->create();
$userWithSchool = User::factory()->create([
'school_id' => School::factory()->create()->id,
]);
// Act & Assert
$this->actingAs($user);
get('/students')
->assertStatus(403);
$this->actingAs($userWithSchool);
get('/students')
->assertStatus(200);
});
it('Shows a student for the user', function () {
// Arrange
$school = School::factory()->create();
$user = User::factory()->create([
'school_id' => $school->id,
]);
$student = Student::factory()->create([
'school_id' => $school->id,
]);
$students = [
['first_name' => 'John', 'last_name' => 'Lennon'],
['first_name' => 'Paul', 'last_name' => 'McCartney'],
['first_name' => 'George', 'last_name' => 'Harrison'],
['first_name' => 'Ringo', 'last_name' => 'Starr'],
];
foreach ($students as $s) {
Student::factory()->create([
'school_id' => $school->id,
'first_name' => $s['first_name'],
'last_name' => $s['last_name'],
]);
}
// Act and Assert
$this->actingAs($user);
get(route('students.index'))
->assertStatus(200)
->assertSeeText($student->name)
->assertSeeText(['Lennon, John', 'McCartney, Paul', 'Harrison, George', 'Starr, Ringo']);
});
it('does not show a student from another school', function () {
// Arrange
$school = School::factory()->create();
$user = User::factory()->create([
'school_id' => $school->id,
]);
$student = Student::factory()->create([
'school_id' => $school->id,
]);
$otherSchool = School::factory()->create();
$otherStudent = Student::factory()->create([
'school_id' => $otherSchool->id,
]);
// Act & Assert
$this->actingAs($user);
get(route('students.index'))
->assertStatus(200)
->assertSeeText($student->name)
->assertDontSeeText($otherStudent->name);
// Assert
});
it('can accept a new student entry', function () {
// Act and Assert
$school = School::factory()->create();
$user = User::factory()->create([
'school_id' => $school->id,
]);
$this->actingAs($user);
post(route('students.store'), [
'first_name' => 'James',
'last_name' => 'Brown',
'grade' => 10,
])
->assertSessionHasNoErrors()
->assertRedirect(route('students.index'));
get(route('students.index'))
->assertSeeText('Brown, James');
expect(Student::count())->toBe(1);
});
it('cannot have two students with identical names at a school', function () {
$school = School::factory()->create();
$user = User::factory()->create([
'school_id' => $school->id,
]);
Student::factory()->create([
'school_id' => $school->id,
'first_name' => 'James',
'last_name' => 'Brown',
]);
$this->actingAs($user);
post(route('students.store'), [
'first_name' => 'James',
'last_name' => 'Brown',
'grade' => 10,
])
->assertSessionHasErrors('last_name');
});

View File

@ -1,5 +1,6 @@
<?php
use App\Models\School;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
@ -57,3 +58,17 @@ it('shows dashboard only if logged in', function () {
->assertSeeText('My School')
->assertSeeText('Dashboard');
});
it('shows students index only for a user with a school', function () {
// Act & Assert
$user = User::factory()->create();
$this->actingAs($user);
get(route('students.index'))
->assertStatus(403);
$school = School::factory()->create();
$user->school_id = $school->id;
get(route('students.index'))
->assertStatus(200)
->assertSeeText('Student Listing');
});