Build out the trainer role
is_trainer was captured at signup and stored, but nothing in the app used it - no client roster, no way to assign anything, a dead flag that misled users who checked it during registration. Adds a trainer/client link (a trainer adds a client by the email they registered with) and lets a trainer create workouts directly on a client's account (assigned_by_trainer_id on workouts). Deliberately scoped for privacy: a trainer only ever sees/manages the workouts they authored and assigned - not a client's other private data (their own workouts, weight log, health notes, session/completion history). That boundary needs a consent or notification system to relax safely, which doesn't exist yet, so it's left for a follow-up rather than building it half-safe. WorkoutController's authorization now distinguishes "manage" (owner or assigning trainer - view, add/edit exercises) from "own" (owner only - delete), so a trainer can build out an assigned workout's exercises after creating it, but can't delete a client's workout out from under them. Found a real bug via live testing before committing: the client roster page eager-loaded `assignedWorkouts` (keyed by assigned_by_trainer_id) on each client record, which is the trainer's own perspective, not the client's - so it silently showed "no workouts assigned" even after assigning one. Fixed to eager-load `workouts` filtered by assigned_by_trainer_id instead, with a regression test. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\Workout;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TrainerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function trainer(): User
|
||||
{
|
||||
$trainer = User::factory()->create();
|
||||
$trainer->is_trainer = true;
|
||||
$trainer->save();
|
||||
|
||||
return $trainer;
|
||||
}
|
||||
|
||||
public function test_guest_is_redirected_to_login(): void
|
||||
{
|
||||
$response = $this->get(route('trainer.clients'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_non_trainer_is_forbidden(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->get(route('trainer.clients'));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_trainer_can_add_a_client_by_email(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create(['email' => 'client@example.com']);
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.clients.add'), [
|
||||
'email' => 'client@example.com',
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertTrue($trainer->clients()->where('client_id', $client->id)->exists());
|
||||
}
|
||||
|
||||
public function test_trainer_cannot_add_themselves_as_a_client(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.clients.add'), [
|
||||
'email' => $trainer->email,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
$this->assertFalse($trainer->clients()->where('client_id', $trainer->id)->exists());
|
||||
}
|
||||
|
||||
public function test_trainer_cannot_add_the_same_client_twice(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.clients.add'), [
|
||||
'email' => $client->email,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
$this->assertEquals(1, $trainer->clients()->where('client_id', $client->id)->count());
|
||||
}
|
||||
|
||||
public function test_trainer_cannot_add_a_client_with_an_unknown_email(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.clients.add'), [
|
||||
'email' => 'nobody@example.com',
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('email');
|
||||
}
|
||||
|
||||
public function test_trainer_can_remove_a_client(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
|
||||
$response = $this->actingAs($trainer)->delete(route('trainer.clients.remove', $client));
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertFalse($trainer->clients()->where('client_id', $client->id)->exists());
|
||||
}
|
||||
|
||||
public function test_trainer_can_assign_a_workout_to_their_client(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.workouts.store', $client), [
|
||||
'title' => 'Week 1 Push Day',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('trainer.clients'));
|
||||
$this->assertDatabaseHas('workouts', [
|
||||
'user_id' => $client->id,
|
||||
'assigned_by_trainer_id' => $trainer->id,
|
||||
'title' => 'Week 1 Push Day',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_client_roster_lists_workouts_assigned_to_each_client(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create(['name' => 'Jamie Client']);
|
||||
$trainer->clients()->attach($client->id);
|
||||
Workout::factory()->create([
|
||||
'user_id' => $client->id,
|
||||
'assigned_by_trainer_id' => $trainer->id,
|
||||
'title' => 'Week 1 Push Day',
|
||||
]);
|
||||
// A workout the client made for themselves should not show up as "assigned".
|
||||
Workout::factory()->create(['user_id' => $client->id, 'title' => 'My Own Workout']);
|
||||
|
||||
$response = $this->actingAs($trainer)->get(route('trainer.clients'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('Week 1 Push Day');
|
||||
$response->assertDontSee('My Own Workout');
|
||||
}
|
||||
|
||||
public function test_trainer_cannot_assign_a_workout_to_a_non_client(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$stranger = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($trainer)->post(route('trainer.workouts.store', $stranger), [
|
||||
'title' => 'Week 1 Push Day',
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_client_can_manage_their_assigned_workout(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
$workout = Workout::factory()->create(['user_id' => $client->id, 'assigned_by_trainer_id' => $trainer->id]);
|
||||
|
||||
$response = $this->actingAs($client)->get(route('workouts.show', $workout));
|
||||
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_assigning_trainer_can_view_and_edit_the_assigned_workout(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
$workout = Workout::factory()->create(['user_id' => $client->id, 'assigned_by_trainer_id' => $trainer->id]);
|
||||
|
||||
$response = $this->actingAs($trainer)->get(route('workouts.show', $workout));
|
||||
$response->assertOk();
|
||||
}
|
||||
|
||||
public function test_assigning_trainer_cannot_delete_the_assigned_workout(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
$workout = Workout::factory()->create(['user_id' => $client->id, 'assigned_by_trainer_id' => $trainer->id]);
|
||||
|
||||
$response = $this->actingAs($trainer)->delete(route('workouts.destroy', $workout));
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseHas('workouts', ['id' => $workout->id]);
|
||||
}
|
||||
|
||||
public function test_unrelated_trainer_cannot_view_someone_elses_assigned_workout(): void
|
||||
{
|
||||
$trainer = $this->trainer();
|
||||
$otherTrainer = $this->trainer();
|
||||
$client = User::factory()->create();
|
||||
$trainer->clients()->attach($client->id);
|
||||
$workout = Workout::factory()->create(['user_id' => $client->id, 'assigned_by_trainer_id' => $trainer->id]);
|
||||
|
||||
$response = $this->actingAs($otherTrainer)->get(route('workouts.show', $workout));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user