diffInMinutes($past) became negative. Unchanged, * that silently disabled the inactivity logout, presence updates and the processing of inactive profiles, * and put negative periods in the warning mails. The code now passes absolute = true and casts to int, * which is what Carbon 2 returned. */ class CarbonDurationsTest extends TestCase { use DatabaseTransactions; protected function setUp(): void { parent::setUp(); // Tests share Redis with the dev site: keep presence keys out of it config(['cache.default' => 'array']); } private function runInactivityMiddleware(Carbon $lastActivity): void { $user = User::factory()->create(); Auth::guard('web')->login($user); Session::put(['activeProfileType' => User::class, 'activeProfileId' => $user->id, 'last_activity' => $lastActivity]); $request = Request::create('/'); $request->setLaravelSession(app('session.store')); app(CheckProfileInactivity::class)->handle($request, fn () => response('next')); } public function test_an_idle_session_beyond_the_session_lifetime_is_logged_out(): void { $this->runInactivityMiddleware(now()->subMinutes(config('session.lifetime', 120) + 5)); $this->assertTrue(Auth::guard('web')->guest()); } public function test_a_recently_active_session_stays_logged_in(): void { $this->runInactivityMiddleware(now()->subMinutes(2)); $this->assertTrue(Auth::guard('web')->check()); } public function test_presence_is_refreshed_once_the_update_interval_has_passed(): void { $user = User::factory()->create(); Auth::guard('web')->login($user); Cache::put("presence_last_update_web_{$user->id}", now()->subMinutes(10), 300); $presence = $this->mock(PresenceService::class); $presence->shouldReceive('updatePresence')->once(); $request = Request::create('/'); $request->setLaravelSession(app('session.store')); app(TrackUserPresence::class)->handle($request, fn () => response('next')); } public function test_a_long_inactive_profile_is_deleted_with_a_positive_age(): void { $command = new class extends ProcessInactiveProfiles { public array $calls = []; protected function deleteProfile($profile, $profileType, $secondsSinceInactive) { $this->calls[] = ['delete', $secondsSinceInactive]; return 'deleted'; } protected function sendWarning($profile, $profileType, $warningLevel, $secondsRemaining, $secondsSinceInactive) { $this->calls[] = [$warningLevel, $secondsSinceInactive]; return 'warning'; } }; $days = (int) timebank_config('delete_profile.days_after_inactive.run_delete'); $profile = (object) ['inactive_at' => now()->subDays($days + 1), 'last_inactive_warning' => 'final']; $process = new \ReflectionMethod($command, 'processProfile'); $process->invoke($command, $profile, 'User'); $this->assertSame('delete', $command->calls[0][0] ?? null); $this->assertIsInt($command->calls[0][1]); $this->assertGreaterThan(0, $command->calls[0][1]); } public function test_the_warning_mail_shows_a_positive_period_since_the_last_login(): void { $mail = (new \ReflectionClass(InactiveProfileWarning1Mail::class))->newInstanceWithoutConstructor(); $format = new \ReflectionMethod($mail, 'formatPeriod'); $this->assertSame(trans_choice('days_not_logged_in', 3, ['count' => 3]), $format->invoke($mail, now()->subDays(3))); $this->assertSame(trans_choice('weeks_not_logged_in', 2, ['count' => 2]), $format->invoke($mail, now()->subDays(14))); } public function test_timestamps_are_shown_in_the_app_timezone(): void { $timestamp = strtotime('2026-10-15 23:30:00'); $this->assertSame('2026-10-15 23:30', Carbon::createFromTimestamp($timestamp, config('app.timezone'))->format('Y-m-d H:i')); } }