Workouts were private to their owner with no way to browse or copy
another user's - FavoriteController::toggleWorkout even had a comment
anticipating sharing ("users can still favorite others' workouts if
shared later") that was never followed up on.
Adds an is_public flag workouts can be toggled to from their own page,
a /workouts/community browse+search page, and a copy action that
duplicates a public workout (including all its exercises/sets/reps)
into the copier's own library.
Making this work required splitting workout authorization into two
tiers: authorizeView() (owner, the assigning trainer, or anyone if the
workout is public - read-only) and the existing authorizeWorkout()
(owner or assigning trainer - can add/edit/remove exercises). Deletion
stays strictly owner-only as before. The show page now hides the
"Add Exercise" panel and per-exercise edit/remove controls for viewers
who can see but not manage a workout.
Found and fixed a real Blade compiler quirk via live testing: `@if`
directly after a word character with no space (`exercises@if(...)`)
doesn't compile - Blade's directive regex treats it as email-like text
and leaves it literal - while the matching `@endif` still compiles,
producing invalid mismatched PHP. Fixed by adding a space.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
158 lines
5.8 KiB
PHP
158 lines
5.8 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 WorkoutCommunityTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
public function test_community_index_only_lists_public_workouts(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$public = Workout::factory()->create(['user_id' => $user->id, 'is_public' => true, 'title' => 'Public One']);
|
|
Workout::factory()->create(['user_id' => $user->id, 'is_public' => false, 'title' => 'Private One']);
|
|
|
|
$response = $this->actingAs($user)->get(route('workouts.community'));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Public One');
|
|
$response->assertDontSee('Private One');
|
|
}
|
|
|
|
public function test_community_index_can_search_by_title(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
Workout::factory()->create(['user_id' => $user->id, 'is_public' => true, 'title' => 'Leg Day Blast']);
|
|
Workout::factory()->create(['user_id' => $user->id, 'is_public' => true, 'title' => 'Upper Body Push']);
|
|
|
|
$response = $this->actingAs($user)->get(route('workouts.community', ['q' => 'Leg']));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Leg Day Blast');
|
|
$response->assertDontSee('Upper Body Push');
|
|
}
|
|
|
|
public function test_owner_can_toggle_a_workout_public(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $user->id, 'is_public' => false]);
|
|
|
|
$this->actingAs($user)->post(route('workouts.toggle-public', $workout));
|
|
|
|
$this->assertTrue($workout->fresh()->is_public);
|
|
|
|
$this->actingAs($user)->post(route('workouts.toggle-public', $workout));
|
|
|
|
$this->assertFalse($workout->fresh()->is_public);
|
|
}
|
|
|
|
public function test_other_user_cannot_toggle_someone_elses_workout_public(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => false]);
|
|
|
|
$response = $this->actingAs($other)->post(route('workouts.toggle-public', $workout));
|
|
|
|
$response->assertForbidden();
|
|
$this->assertFalse($workout->fresh()->is_public);
|
|
}
|
|
|
|
public function test_any_user_can_view_a_public_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$viewer = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => true, 'title' => 'Full Body Blast']);
|
|
|
|
$response = $this->actingAs($viewer)->get(route('workouts.show', $workout));
|
|
|
|
$response->assertOk();
|
|
$response->assertSee('Full Body Blast');
|
|
}
|
|
|
|
public function test_viewer_of_a_public_workout_cannot_manage_its_exercises(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$viewer = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => true]);
|
|
$exercise = Exercise::factory()->create();
|
|
|
|
$response = $this->actingAs($viewer)->get(route('workouts.show', $workout));
|
|
$response->assertOk();
|
|
$response->assertDontSee('Add Exercise');
|
|
|
|
$addResponse = $this->actingAs($viewer)->post(route('workouts.exercises.add', $workout), [
|
|
'exercise_id' => $exercise->id,
|
|
]);
|
|
$addResponse->assertForbidden();
|
|
}
|
|
|
|
public function test_non_owner_cannot_view_a_private_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$viewer = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => false]);
|
|
|
|
$response = $this->actingAs($viewer)->get(route('workouts.show', $workout));
|
|
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_user_can_copy_a_public_workout_with_its_exercises(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$copier = User::factory()->create();
|
|
$workout = Workout::factory()->create([
|
|
'user_id' => $owner->id,
|
|
'is_public' => true,
|
|
'title' => 'Original Workout',
|
|
'estimated_minutes' => 45,
|
|
]);
|
|
WorkoutExercise::factory()->create(['workout_id' => $workout->id, 'sets' => 4, 'reps' => 6]);
|
|
|
|
$response = $this->actingAs($copier)->post(route('workouts.copy', $workout));
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas('workouts', [
|
|
'user_id' => $copier->id,
|
|
'title' => 'Original Workout (copy)',
|
|
'estimated_minutes' => 45,
|
|
]);
|
|
$copiedWorkout = Workout::where('user_id', $copier->id)->firstOrFail();
|
|
$this->assertEquals(1, $copiedWorkout->items()->count());
|
|
$this->assertEquals(4, $copiedWorkout->items()->first()->sets);
|
|
|
|
// The original is untouched
|
|
$this->assertEquals(1, $workout->fresh()->items()->count());
|
|
}
|
|
|
|
public function test_user_cannot_copy_a_private_workout_they_do_not_own(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$other = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => false]);
|
|
|
|
$response = $this->actingAs($other)->post(route('workouts.copy', $workout));
|
|
|
|
$response->assertForbidden();
|
|
}
|
|
|
|
public function test_owner_can_copy_their_own_private_workout(): void
|
|
{
|
|
$owner = User::factory()->create();
|
|
$workout = Workout::factory()->create(['user_id' => $owner->id, 'is_public' => false]);
|
|
|
|
$response = $this->actingAs($owner)->post(route('workouts.copy', $workout));
|
|
|
|
$response->assertRedirect();
|
|
$this->assertEquals(2, Workout::where('user_id', $owner->id)->count());
|
|
}
|
|
}
|