Compare commits

..

3 commits

Author SHA1 Message Date
c1d8008b5d 56 - Cover session invalidation and fix pest-browser path handling
All checks were successful
CI / ci-image (push) Successful in 2m2s
CI / ci (push) Successful in 2m53s
2026-08-20 22:05:39 +02:00
95c5a72294 56 - Add browser test for logout 2026-08-20 21:45:28 +02:00
c3a5532028 60 - Make schedule test assertion counts deterministic 2026-08-20 20:24:09 +02:00
7 changed files with 115 additions and 28 deletions

View file

@ -1662,12 +1662,6 @@ parameters:
count: 2 count: 2
path: tests/Unit/Schedule/ScheduleGeneratorTest.php path: tests/Unit/Schedule/ScheduleGeneratorTest.php
-
message: '#^Parameter \#1 \$callback of method Illuminate\\Support\\Collection\<int,Carbon\\Carbon\>\:\:reduce\(\) expects callable\(Illuminate\\Support\\Carbon\|null, Carbon\\Carbon, int\)\: Illuminate\\Support\\Carbon, Closure\(Illuminate\\Support\\Carbon\|null, Illuminate\\Support\\Carbon\)\: Illuminate\\Support\\Carbon given\.$#'
identifier: argument.type
count: 1
path: tests/Unit/Schedule/ScheduleGeneratorTest.php
- -
message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#' message: '#^Access to an undefined property Illuminate\\Database\\Eloquent\\Model\:\:\$id\.$#'
identifier: property.notFound identifier: property.notFound

View file

@ -109,7 +109,35 @@ pkgs.mkShell {
pest-browser() { pest-browser() {
# Run Pest browser tests (Playwright) on the host. Pass --headed for a # Run Pest browser tests (Playwright) on the host. Pass --headed for a
# visible browser or --debug to pause on failure. Requires `npm run build`. # visible browser or --debug to pause on failure. Requires `npm run build`.
# A path argument replaces the default suite; flags alone keep it.
# Only existing paths count, so `--filter Logout` cannot silently
# unscope the run to the whole test directory.
local has_path=0
local skip_next=0
local arg
for arg in "$@"; do
if [ "$skip_next" -eq 1 ]; then
skip_next=0
continue
fi
case "$arg" in
--filter|--group|--exclude-group|--test-suffix)
skip_next=1
;;
-*) ;;
*)
if [ -e "$arg" ]; then
has_path=1
fi
;;
esac
done
if [ "$has_path" -eq 1 ]; then
vendor/bin/pest "$@"
else
vendor/bin/pest tests/Browser "$@" vendor/bin/pest tests/Browser "$@"
fi
} }
dev-fix-permissions() { dev-fix-permissions() {

View file

@ -0,0 +1,31 @@
<?php
it('logs out and lands on the login page', function () {
$page = loginAs(createPlanner());
logout($page)
->assertPathIs('/login')
->assertSee('Sign In');
});
it('blocks protected routes after logging out', function (string $path) {
$page = loginAs(createPlanner());
logout($page)
->assertPathIs('/login')
->navigate($path)
->assertPathIs('/login')
->assertSee('Sign In');
})->with(['/dashboard', '/dishes', '/schedule', '/users']);
it('does not restore an authenticated view via the back button', function () {
$planner = createPlanner();
$page = loginAs($planner)->assertSee($planner->name);
logout($page)
->assertPathIs('/login')
->back()
->assertPathIs('/login')
->assertDontSee('Logout');
});

View file

@ -90,4 +90,21 @@ public function test_users_can_logout(): void
$response->assertRedirect('/'); $response->assertRedirect('/');
$this->assertGuest(); $this->assertGuest();
} }
public function test_logout_invalidates_the_session(): void
{
$user = Planner::factory()->create();
// Boots the session store so getId() below reflects a real value.
$this->actingAs($user)->get('/dashboard');
session()->put('scratch', 'value');
$sessionId = session()->getId();
$this->actingAs($user)->post('/logout');
$this->assertNotSame($sessionId, session()->getId());
$this->assertFalse(session()->has('scratch'));
}
} }

View file

@ -3,6 +3,7 @@
use App\Models\Planner; use App\Models\Planner;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Pest\Browser\Api\AwaitableWebpage;
use Pest\Browser\Api\Webpage; use Pest\Browser\Api\Webpage;
use Tests\BrowserTestCase; use Tests\BrowserTestCase;
use Tests\TestCase; use Tests\TestCase;
@ -62,3 +63,15 @@ function loginAndGoToSchedule()
{ {
return loginAs(createPlanner(), '/schedule'); return loginAs(createPlanner(), '/schedule');
} }
/**
* Open the account dropdown and submit the logout form.
*
* @return Webpage
*/
function logout(Webpage|AwaitableWebpage $page)
{
return $page
->click('button[\@click="open = !open"]')
->click('.sm\:flex form[action$="/logout"] button[type="submit"]');
}

View file

@ -12,7 +12,6 @@
use App\WeekdaysEnum; use App\WeekdaysEnum;
use DishPlanner\Schedule\Services\ScheduleGenerator; use DishPlanner\Schedule\Services\ScheduleGenerator;
use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Carbon;
use Tests\TestCase; use Tests\TestCase;
use Tests\Traits\HasPlanner; use Tests\Traits\HasPlanner;
@ -128,19 +127,23 @@ public function test_it_takes_minimum_recurrences_into_account(): void
$this->assertTrue(Schedule::all()->isNotEmpty()); $this->assertTrue(Schedule::all()->isNotEmpty());
Schedule::all() $recurringDates = Schedule::all()
->filter(fn (Schedule $schedule) => $schedule->scheduledUserDishes()->first()->userDish->dish->id === $dishRecurring->id) ->filter(fn (Schedule $schedule) => $schedule->scheduledUserDishes
->contains(fn ($scheduledUserDish) => $scheduledUserDish->userDish->dish->id === $dishRecurring->id)
)
->map(fn (Schedule $schedule) => $schedule->date) ->map(fn (Schedule $schedule) => $schedule->date)
->reduce(function (?Carbon $previousDate, Carbon $currentDate) use ($recurringMinimum) { ->sort()
if (! is_null($previousDate)) { ->values();
$this->assertGreaterThanOrEqual(
$recurringMinimum, $this->assertGreaterThan(1, $recurringDates->count(), 'Recurring dish was not scheduled often enough to verify spacing');
$previousDate->diffInDays($currentDate),
$gaps = $recurringDates
->sliding(2)
->map(fn ($pair) => (int) $pair->first()->diffInDays($pair->last()));
$this->assertEmpty(
$gaps->reject(fn (int $gap) => $gap >= $recurringMinimum)->all(),
'Dates are not spaced properly' 'Dates are not spaced properly'
); );
} }
return $currentDate;
});
}
} }

View file

@ -55,19 +55,20 @@ public function test_includes_correct_day_numbers(): void
public function test_marks_today_correctly(): void public function test_marks_today_correctly(): void
{ {
$this->travelTo(Carbon::createFromDate(2026, 3, 15)->startOfDay());
$planner = $this->planner; $planner = $this->planner;
$today = now();
$calendarDays = $this->service->getCalendarDays($planner, $today->month, $today->year); $calendarDays = $this->service->getCalendarDays($planner, 3, 2026);
$todayIndex = $today->day - 1; $todayIndex = 14;
$this->assertTrue($calendarDays[$todayIndex]['isToday']); $this->assertTrue($calendarDays[$todayIndex]['isToday']);
foreach ($calendarDays as $index => $day) { $otherDaysMarkedToday = collect($calendarDays)
if ($index !== $todayIndex && $day['day'] !== null) { ->filter(fn (array $day, int $index) => $index !== $todayIndex && $day['day'] !== null)
$this->assertFalse($day['isToday']); ->filter(fn (array $day) => $day['isToday']);
}
} $this->assertCount(0, $otherDaysMarkedToday);
} }
public function test_includes_scheduled_dishes(): void public function test_includes_scheduled_dishes(): void