feat: add routes, lang, tests, stubs, docs, and docker configurations
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
<?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);
|
||||
});
|
||||
Reference in New Issue
Block a user