Add multi-week workout programs
A Workout was a single flat session with no way to schedule several across weeks/days as a structured plan (e.g. Push/Pull/Legs over 4 weeks) - every user started from a blank builder each time, and there was no template concept at all. Adds a Program (title, description, week count) that Workouts can optionally belong to via nullable program_id/week_number/day_number columns, reusing all existing workout and session-tracking infrastructure rather than building a parallel system. The program show page lays out the schedule week by week and derives progress (completed vs total) from the user's existing WorkoutSession completions - no new enrollment/progress-tracking concept needed. Deleting a program unschedules its workouts (nullOnDelete) rather than destroying them. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\Program;
|
||||
use App\Models\User;
|
||||
use App\Models\Workout;
|
||||
use App\Models\WorkoutSession;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProgramTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_guest_is_redirected_to_login(): void
|
||||
{
|
||||
$response = $this->get(route('programs.index'));
|
||||
|
||||
$response->assertRedirect(route('login'));
|
||||
}
|
||||
|
||||
public function test_user_can_create_a_program(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->actingAs($user)->post(route('programs.store'), [
|
||||
'title' => 'Push Pull Legs',
|
||||
'weeks' => 6,
|
||||
]);
|
||||
|
||||
$this->assertDatabaseHas('programs', ['user_id' => $user->id, 'title' => 'Push Pull Legs', 'weeks' => 6]);
|
||||
$program = Program::where('title', 'Push Pull Legs')->firstOrFail();
|
||||
$response->assertRedirect(route('programs.show', $program));
|
||||
}
|
||||
|
||||
public function test_owner_can_view_their_program(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('programs.show', $program));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee($program->title);
|
||||
}
|
||||
|
||||
public function test_other_user_cannot_view_program(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $owner->id]);
|
||||
|
||||
$response = $this->actingAs($other)->get(route('programs.show', $program));
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_owner_can_add_a_workout_to_a_program_week(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $user->id, 'weeks' => 4]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('programs.workouts.add', $program), [
|
||||
'title' => 'Push Day',
|
||||
'week_number' => 2,
|
||||
'day_number' => 1,
|
||||
]);
|
||||
|
||||
$response->assertRedirect();
|
||||
$this->assertDatabaseHas('workouts', [
|
||||
'program_id' => $program->id,
|
||||
'user_id' => $user->id,
|
||||
'title' => 'Push Day',
|
||||
'week_number' => 2,
|
||||
'day_number' => 1,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_week_number_beyond_program_length_is_rejected(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $user->id, 'weeks' => 4]);
|
||||
|
||||
$response = $this->actingAs($user)->post(route('programs.workouts.add', $program), [
|
||||
'title' => 'Push Day',
|
||||
'week_number' => 5,
|
||||
'day_number' => 1,
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('week_number');
|
||||
}
|
||||
|
||||
public function test_other_user_cannot_add_a_workout_to_someone_elses_program(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $owner->id]);
|
||||
|
||||
$response = $this->actingAs($other)->post(route('programs.workouts.add', $program), [
|
||||
'title' => 'Push Day',
|
||||
'week_number' => 1,
|
||||
'day_number' => 1,
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_show_reports_progress_from_completed_sessions(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $user->id, 'weeks' => 2]);
|
||||
$workout1 = Workout::factory()->create(['user_id' => $user->id, 'program_id' => $program->id, 'week_number' => 1, 'day_number' => 1]);
|
||||
$workout2 = Workout::factory()->create(['user_id' => $user->id, 'program_id' => $program->id, 'week_number' => 1, 'day_number' => 2]);
|
||||
|
||||
WorkoutSession::factory()->create([
|
||||
'user_id' => $user->id,
|
||||
'workout_id' => $workout1->id,
|
||||
'completed_at' => now(),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($user)->get(route('programs.show', $program));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('1 / 2 workouts', false);
|
||||
}
|
||||
|
||||
public function test_owner_can_delete_a_program_without_deleting_its_workouts(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$program = Program::factory()->create(['user_id' => $user->id]);
|
||||
$workout = Workout::factory()->create(['user_id' => $user->id, 'program_id' => $program->id, 'week_number' => 1, 'day_number' => 1]);
|
||||
|
||||
$response = $this->actingAs($user)->delete(route('programs.destroy', $program));
|
||||
|
||||
$response->assertRedirect(route('programs.index'));
|
||||
$this->assertDatabaseMissing('programs', ['id' => $program->id]);
|
||||
$this->assertDatabaseHas('workouts', ['id' => $workout->id]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user