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>
200 lines
6.6 KiB
PHP
200 lines
6.6 KiB
PHP
<?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();
|
|
}
|
|
}
|