getProperty('resolvedSettings'); $prop->setAccessible(true); $prop->setValue(null, null); Cache::flush(); }); function setSetting(string $key, mixed $value): void { app(SystemConfigService::class)->update([$key => $value]); } test('isPasswordExpired returns false when expiry is disabled', function () { setSetting('password_expiry_days', 0); $user = User::factory()->create([ 'password_changed_at' => now()->subYears(5), ]); expect(PasswordPolicyService::isPasswordExpired($user))->toBeFalse(); }); test('isPasswordExpired uses password_changed_at when present', function () { setSetting('password_expiry_days', 30); $expired = User::factory()->create(['password_changed_at' => now()->subDays(31)]); $fresh = User::factory()->create(['password_changed_at' => now()->subDays(5)]); expect(PasswordPolicyService::isPasswordExpired($expired))->toBeTrue(); expect(PasswordPolicyService::isPasswordExpired($fresh))->toBeFalse(); }); test('isPasswordExpired falls back to created_at when password_changed_at is null', function () { setSetting('password_expiry_days', 30); $user = User::factory()->create(); DB::table('users')->where('id', $user->id)->update([ 'password_changed_at' => null, 'created_at' => now()->subDays(40), ]); expect(PasswordPolicyService::isPasswordExpired($user->fresh()))->toBeTrue(); }); test('checkHistory is a no-op when history count is zero', function () { setSetting('password_history_count', 0); $user = User::factory()->create(); PasswordHistory::create(['user_id' => $user->id, 'password' => Hash::make('old-pass')]); PasswordPolicyService::checkHistory($user, 'old-pass'); })->throwsNoExceptions(); test('checkHistory throws when reusing a recent password', function () { setSetting('password_history_count', 3); $user = User::factory()->create(); PasswordHistory::create(['user_id' => $user->id, 'password' => Hash::make('reused-pass')]); PasswordPolicyService::checkHistory($user, 'reused-pass'); })->throws(ValidationException::class); test('checkHistory passes when new password is different from history', function () { setSetting('password_history_count', 3); $user = User::factory()->create(); PasswordHistory::create(['user_id' => $user->id, 'password' => Hash::make('old-1')]); PasswordHistory::create(['user_id' => $user->id, 'password' => Hash::make('old-2')]); PasswordPolicyService::checkHistory($user, 'totally-different'); })->throwsNoExceptions(); test('checkHistory only inspects the most recent N entries', function () { setSetting('password_history_count', 2); $user = User::factory()->create(); DB::table('password_histories')->insert([ ['user_id' => $user->id, 'password' => Hash::make('ancient'), 'created_at' => now()->subDays(10), 'updated_at' => now()->subDays(10)], ['user_id' => $user->id, 'password' => Hash::make('recent-1'), 'created_at' => now()->subDays(2), 'updated_at' => now()->subDays(2)], ['user_id' => $user->id, 'password' => Hash::make('recent-2'), 'created_at' => now()->subDay(), 'updated_at' => now()->subDay()], ]); PasswordPolicyService::checkHistory($user, 'ancient'); })->throwsNoExceptions(); test('recordPasswordChange creates history row and stamps password_changed_at', function () { setSetting('password_history_count', 5); $user = User::factory()->create(['password_changed_at' => null]); PasswordPolicyService::recordPasswordChange($user, Hash::make('new-pass')); expect($user->fresh()->password_changed_at)->not->toBeNull(); expect(PasswordHistory::where('user_id', $user->id)->count())->toBe(1); }); test('getRules respects min/max length from settings', function () { setSetting('password_min_length', 10); setSetting('password_max_length', 50); $rules = PasswordPolicyService::getRules(); expect($rules)->toBeInstanceOf(Password::class); });