The workout builder's "add exercise" dropdown was Exercise::orderBy
('name')->limit(50) with no search. Harmless with a handful of seeded
exercises, but once the library is populated with real data (1000+),
users could only ever add exercises starting around A-D - the core
"build a workout" flow would be effectively broken.
Replaces it with a debounced Alpine.js combobox backed by a new
GET /workouts/{workout}/exercises/search endpoint (owner-only, JSON,
capped at 20 results, respects each user's custom exercise aliases).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
187 lines
6.2 KiB
PHP
187 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Exercise;
|
|
use App\Models\User;
|
|
use App\Models\Workout;
|
|
use App\Models\WorkoutExercise;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class WorkoutTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_guest_is_redirected_to_login(): void
|
|
{
|
|
$response = $this->get(route('workouts.index'));
|
|
|
|
$response->assertRedirect(route('login'));
|
|
}
|
|
|
|
public function test_user_can_create_a_workout(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('workouts.store'), [
|
|
'title' => 'Leg Day',
|
|
'notes' => 'Focus on form',
|
|
'estimated_minutes' => 45,
|
|
]);
|
|
|
|
$this->assertDatabaseHas('workouts', [
|
|
'user_id' => $user->id,
|
|
'title' => 'Leg Day',
|
|
]);
|
|
$workout = Workout::where('title', 'Leg Day')->firstOrFail();
|
|
$response->assertRedirect(route('workouts.show', $workout));
|
|
}
|
|
|
|
public function test_owner_can_view_their_workout(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
|
|
$response = $this->actingAs($user)->get(route('workouts.show', $workout));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee($workout->title);
|
|
}
|
|
|
|
public function test_other_user_cannot_view_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id]);
|
|
|
|
$response = $this->actingAs($other)->get(route('workouts.show', $workout));
|
|
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_owner_can_add_exercise_to_workout(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$exercise = Exercise::factory()->create();
|
|
|
|
$response = $this->actingAs($user)->post(route('workouts.exercises.add', $workout), [
|
|
'exercise_id' => $exercise->id,
|
|
'sets' => 4,
|
|
'reps' => 8,
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas('workout_exercises', [
|
|
'workout_id' => $workout->id,
|
|
'exercise_id' => $exercise->id,
|
|
'sets' => 4,
|
|
'reps' => 8,
|
|
]);
|
|
}
|
|
|
|
public function test_other_user_cannot_add_exercise_to_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id]);
|
|
$exercise = Exercise::factory()->create();
|
|
|
|
$response = $this->actingAs($other)->post(route('workouts.exercises.add', $workout), [
|
|
'exercise_id' => $exercise->id,
|
|
]);
|
|
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_owner_can_update_workout_exercise(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$item = WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'sets' => 3]);
|
|
|
|
$response = $this->actingAs($user)->patch(route('workouts.exercises.update', [$workout, $item]), [
|
|
'sets' => 5,
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas('workout_exercises', [
|
|
'id' => $item->id,
|
|
'sets' => 5,
|
|
]);
|
|
}
|
|
|
|
public function test_owner_can_remove_workout_exercise(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
$item = WorkoutExercise::factory()->create(['workout_id' => $workout->id]);
|
|
|
|
$response = $this->actingAs($user)->delete(route('workouts.exercises.remove', [$workout, $item]));
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseMissing('workout_exercises', ['id' => $item->id]);
|
|
}
|
|
|
|
public function test_owner_can_delete_workout(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
|
|
$response = $this->actingAs($user)->delete(route('workouts.destroy', $workout));
|
|
|
|
$response->assertRedirect(route('workouts.index'));
|
|
$this->assertDatabaseMissing('workouts', ['id' => $workout->id]);
|
|
}
|
|
|
|
public function test_other_user_cannot_delete_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id]);
|
|
|
|
$response = $this->actingAs($other)->delete(route('workouts.destroy', $workout));
|
|
|
|
$response->assertForbidden();
|
|
$this->assertDatabaseHas('workouts', ['id' => $workout->id]);
|
|
}
|
|
|
|
public function test_owner_can_search_exercises_for_the_picker(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
Exercise::factory()->create(['name' => 'Barbell Bench Press']);
|
|
Exercise::factory()->create(['name' => 'Dumbbell Curl']);
|
|
|
|
$response = $this->actingAs($user)->getJson(route('workouts.exercises.search', $workout) . '?q=Bench');
|
|
|
|
$response->assertOk();
|
|
$response->assertJsonCount(1);
|
|
$response->assertJsonFragment(['name' => 'Barbell Bench Press']);
|
|
}
|
|
|
|
public function test_search_caps_results_at_twenty(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id]);
|
|
Exercise::factory()->count(25)->create();
|
|
|
|
$response = $this->actingAs($user)->getJson(route('workouts.exercises.search', $workout));
|
|
|
|
$response->assertOk();
|
|
$this->assertCount(20, $response->json());
|
|
}
|
|
|
|
public function test_other_user_cannot_search_exercises_for_someone_elses_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id]);
|
|
|
|
$response = $this->actingAs($other)->getJson(route('workouts.exercises.search', $workout));
|
|
|
|
$response->assertForbidden();
|
|
}
|
|
}
|