177 lines
7.1 KiB
PHP
177 lines
7.1 KiB
PHP
<?php
|
|
|
|
use App\Models\PasswordHistory;
|
|
use App\Models\User;
|
|
use App\Services\Auth\PasswordPolicyService;
|
|
use App\Services\SystemConfig\SystemConfigService;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\Cache;
|
|
use Illuminate\Support\Facades\Hash;
|
|
|
|
beforeEach(function () {
|
|
$ref = new ReflectionClass(SystemConfigService::class);
|
|
$prop = $ref->getProperty('resolvedSettings');
|
|
$prop->setAccessible(true);
|
|
$prop->setValue(null, null);
|
|
Cache::flush();
|
|
});
|
|
|
|
// ── Happy path ────────────────────────────────────────────────────────────────
|
|
|
|
test('password update succeeds with valid current password', function () {
|
|
$user = User::factory()->create(['password' => Hash::make('current-pass')]);
|
|
|
|
$this->actingAs($user)
|
|
->from('/profile')
|
|
->put('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'New-Pass-123',
|
|
'password_confirmation' => 'New-Pass-123',
|
|
])
|
|
->assertRedirect('/profile')
|
|
->assertSessionHasNoErrors();
|
|
|
|
expect(Hash::check('New-Pass-123', $user->fresh()->password))->toBeTrue();
|
|
});
|
|
|
|
test('password update stamps password_changed_at', function () {
|
|
app(SystemConfigService::class)->update(['password_history_count' => 0]);
|
|
|
|
$user = User::factory()->create([
|
|
'password' => Hash::make('current-pass'),
|
|
'password_changed_at' => null,
|
|
]);
|
|
|
|
$this->actingAs($user)->put('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'New-Pass-456',
|
|
'password_confirmation' => 'New-Pass-456',
|
|
]);
|
|
|
|
expect($user->fresh()->password_changed_at)->not->toBeNull();
|
|
});
|
|
|
|
// ── Validation ────────────────────────────────────────────────────────────────
|
|
|
|
test('wrong current password is rejected', function () {
|
|
$user = User::factory()->create(['password' => Hash::make('real-pass')]);
|
|
|
|
$this->actingAs($user)
|
|
->from('/profile')
|
|
->put('/password', [
|
|
'current_password' => 'wrong-pass',
|
|
'password' => 'New-Pass-789',
|
|
'password_confirmation' => 'New-Pass-789',
|
|
])
|
|
->assertSessionHasErrorsIn('updatePassword', 'current_password');
|
|
|
|
expect(Hash::check('real-pass', $user->fresh()->password))->toBeTrue();
|
|
});
|
|
|
|
test('mismatched confirmation is rejected', function () {
|
|
$user = User::factory()->create(['password' => Hash::make('correct')]);
|
|
|
|
$this->actingAs($user)
|
|
->from('/profile')
|
|
->put('/password', [
|
|
'current_password' => 'correct',
|
|
'password' => 'New-Pass-Abc',
|
|
'password_confirmation' => 'Different-Xyz',
|
|
])
|
|
->assertSessionHasErrors();
|
|
});
|
|
|
|
// ── History enforcement ───────────────────────────────────────────────────────
|
|
|
|
test('reusing a recent password is rejected with history enabled', function () {
|
|
app(SystemConfigService::class)->update(['password_history_count' => 3]);
|
|
|
|
$oldHash = Hash::make('OldPassword1!');
|
|
$user = User::factory()->create(['password' => Hash::make('current-pass')]);
|
|
PasswordHistory::create(['user_id' => $user->id, 'password' => $oldHash]);
|
|
|
|
$this->actingAs($user)
|
|
->from('/profile')
|
|
->put('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'OldPassword1!',
|
|
'password_confirmation' => 'OldPassword1!',
|
|
])
|
|
->assertSessionHasErrors();
|
|
});
|
|
|
|
test('history check is skipped when history_count is zero', function () {
|
|
app(SystemConfigService::class)->update(['password_history_count' => 0]);
|
|
|
|
$oldHash = Hash::make('Recycled-Pass!1');
|
|
$user = User::factory()->create(['password' => Hash::make('current-pass')]);
|
|
PasswordHistory::create(['user_id' => $user->id, 'password' => $oldHash]);
|
|
|
|
$this->actingAs($user)
|
|
->from('/profile')
|
|
->put('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'Recycled-Pass!1',
|
|
'password_confirmation' => 'Recycled-Pass!1',
|
|
])
|
|
->assertSessionHasNoErrors();
|
|
});
|
|
|
|
// ── History recording ─────────────────────────────────────────────────────────
|
|
|
|
test('successful password change records new entry in password history', function () {
|
|
app(SystemConfigService::class)->update(['password_history_count' => 5]);
|
|
|
|
$user = User::factory()->create(['password' => Hash::make('current-pass')]);
|
|
|
|
$this->actingAs($user)->put('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'Brand-New-777!',
|
|
'password_confirmation' => 'Brand-New-777!',
|
|
]);
|
|
|
|
expect(PasswordHistory::where('user_id', $user->id)->count())->toBe(1);
|
|
});
|
|
|
|
// ── old password no longer works after update ─────────────────────────────────
|
|
|
|
test('old password cannot be used to log in after successful update', function () {
|
|
$user = User::factory()->create(['password' => Hash::make('old-pass-456')]);
|
|
|
|
$this->actingAs($user)->put('/password', [
|
|
'current_password' => 'old-pass-456',
|
|
'password' => 'Brand-New-888!',
|
|
'password_confirmation' => 'Brand-New-888!',
|
|
]);
|
|
|
|
// Old password must not match the new stored hash
|
|
expect(Hash::check('old-pass-456', $user->fresh()->password))->toBeFalse();
|
|
// New password must match
|
|
expect(Hash::check('Brand-New-888!', $user->fresh()->password))->toBeTrue();
|
|
});
|
|
|
|
// ── Guest ─────────────────────────────────────────────────────────────────────
|
|
|
|
test('guest cannot update password', function () {
|
|
$this->put('/password', [
|
|
'current_password' => 'x',
|
|
'password' => 'y',
|
|
'password_confirmation' => 'y',
|
|
])->assertRedirect('/login');
|
|
});
|
|
|
|
// ── JSON response ─────────────────────────────────────────────────────────────
|
|
|
|
test('json request receives json success response', function () {
|
|
$user = User::factory()->create(['password' => Hash::make('current-pass')]);
|
|
|
|
$this->actingAs($user)
|
|
->putJson('/password', [
|
|
'current_password' => 'current-pass',
|
|
'password' => 'Json-Pass-1!',
|
|
'password_confirmation' => 'Json-Pass-1!',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('success', true);
|
|
});
|