-
+
Kata Sandi
+
-
-
-
-
>
);
}
@@ -140,7 +119,7 @@ export default function Security(props: Props) {
Security.layout = {
breadcrumbs: [
{
- title: 'Security settings',
+ title: 'Kata Sandi',
href: edit(),
},
],
diff --git a/routes/settings.php b/routes/settings.php
index d74f967..f7bfb88 100644
--- a/routes/settings.php
+++ b/routes/settings.php
@@ -1,8 +1,8 @@
group(function () {
@@ -13,10 +13,7 @@
});
Route::middleware(['auth', 'verified'])->group(function () {
- Route::delete('settings/profile', [ProfileController::class, 'destroy'])->name('profile.destroy');
-
Route::get('settings/security', [SecurityController::class, 'edit'])
- ->middleware(RequirePassword::class)
->name('security.edit');
Route::put('settings/password', [SecurityController::class, 'update'])
@@ -24,6 +21,9 @@
->name('user-password.update');
Route::inertia('settings/appearance', 'settings/appearance')->name('appearance.edit');
+
+ Route::get('settings/permissions', [PermissionController::class, 'edit'])
+ ->name('permissions.edit');
});
Route::get('.well-known/passkey-endpoints', function () {
diff --git a/tests/Feature/Settings/ProfileUpdateTest.php b/tests/Feature/Settings/ProfileUpdateTest.php
index 9f49e25..f906369 100644
--- a/tests/Feature/Settings/ProfileUpdateTest.php
+++ b/tests/Feature/Settings/ProfileUpdateTest.php
@@ -1,6 +1,37 @@
get(route('profile.edit'));
+ $response->assertRedirect(route('login'));
+});
+
+test('guests are redirected to the login page for profile update', function () {
+ $response = $this->patch(route('profile.update'), [
+ 'email' => 'test@example.com',
+ 'username' => 'testuser',
+ 'full_name' => 'Test User',
+ ]);
+ $response->assertRedirect(route('login'));
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE EDIT PAGE
+|--------------------------------------------------------------------------
+*/
test('profile page is displayed', function () {
$user = User::factory()->create();
@@ -12,14 +43,85 @@
$response->assertOk();
});
-test('profile information can be updated', function () {
+test('profile page renders correct inertia component', function () {
$user = User::factory()->create();
+ $this->actingAs($user)
+ ->get(route('profile.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->component('settings/profile')
+ );
+});
+
+test('profile page passes user data to view', function () {
+ $user = User::factory()->create([
+ 'email' => 'john@example.com',
+ 'username' => 'johndoe',
+ ]);
+ $user->userProfile()->create([
+ 'full_name' => 'John Doe',
+ 'phone_number' => '08123456789',
+ 'gender' => 'male',
+ 'birth_date' => '1990-05-15',
+ 'address' => 'Jl. Sudirman No. 1',
+ ]);
+
+ $this->actingAs($user)
+ ->get(route('profile.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('user.id', $user->id)
+ ->where('user.email', 'john@example.com')
+ ->where('user.username', 'johndoe')
+ ->where('user.userProfile.full_name', 'John Doe')
+ ->where('user.userProfile.phone_number', '08123456789')
+ ->where('user.userProfile.gender', 'male')
+ ->where('user.userProfile.birth_date', '1990-05-15')
+ ->where('user.userProfile.address', 'Jl. Sudirman No. 1')
+ );
+});
+
+test('profile page works when user has no profile', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->get(route('profile.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('user.id', $user->id)
+ ->where('user.email', $user->email)
+ ->where('user.username', $user->username)
+ ->where('user.userProfile', null)
+ );
+});
+
+test('profile page passes mustVerifyEmail flag', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->get(route('profile.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->has('mustVerifyEmail')
+ );
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - SUCCESS
+|--------------------------------------------------------------------------
+*/
+
+test('profile information can be updated with all fields', function () {
+ $user = User::factory()->create(['username' => 'originaluser']);
+
$response = $this
->actingAs($user)
->patch(route('profile.update'), [
- 'name' => 'Test User',
- 'email' => 'test@example.com',
+ 'email' => $user->email,
+ 'username' => 'updateduser',
+ 'full_name' => 'Updated Name',
+ 'phone_number' => '08987654321',
+ 'gender' => 'female',
+ 'birth_date' => '1995-03-20',
+ 'address' => 'Jl. Thamrin No. 2',
]);
$response
@@ -28,58 +130,587 @@
$user->refresh();
- expect($user->name)->toBe('Test User');
- expect($user->email)->toBe('test@example.com');
- expect($user->email_verified_at)->toBeNull();
+ expect($user->username)->toBe('updateduser');
+ expect($user->userProfile->full_name)->toBe('Updated Name');
+ expect($user->userProfile->phone_number)->toBe('08987654321');
+ expect($user->userProfile->gender->value)->toBe('female');
+ expect($user->userProfile->birth_date->format('Y-m-d'))->toBe('1995-03-20');
+ expect($user->userProfile->address)->toBe('Jl. Thamrin No. 2');
});
-test('email verification status is unchanged when the email address is unchanged', function () {
- $user = User::factory()->create();
+test('profile can be updated with only required fields', function () {
+ $user = User::factory()->create(['username' => 'minimaluser']);
$response = $this
->actingAs($user)
->patch(route('profile.update'), [
- 'name' => 'Test User',
'email' => $user->email,
+ 'username' => 'minimaluser',
+ 'full_name' => 'Minimal Update',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('profile.edit'));
- expect($user->refresh()->email_verified_at)->not->toBeNull();
+ expect($user->refresh()->userProfile->full_name)->toBe('Minimal Update');
});
-test('user can delete their account', function () {
- $user = User::factory()->create();
+test('profile update creates user profile if not exists', function () {
+ $user = User::factory()->create(['username' => 'newprofile']);
+ expect($user->userProfile)->toBeNull();
+
+ $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'newprofile',
+ 'full_name' => 'New Profile',
+ 'phone_number' => '08111111111',
+ 'gender' => 'male',
+ 'birth_date' => '2000-01-01',
+ 'address' => 'New Address',
+ ]);
+
+ $user->refresh();
+ expect($user->userProfile)->not->toBeNull();
+ expect($user->userProfile->full_name)->toBe('New Profile');
+ expect($user->userProfile->phone_number)->toBe('08111111111');
+});
+
+test('profile update updates existing user profile', function () {
+ $user = User::factory()->create(['username' => 'existingprofile']);
+ $user->userProfile()->create([
+ 'full_name' => 'Old Name',
+ 'phone_number' => '08000000000',
+ 'gender' => 'male',
+ 'address' => 'Old Address',
+ ]);
+
+ $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'existingprofile',
+ 'full_name' => 'New Name',
+ 'phone_number' => '08999999999',
+ 'gender' => 'female',
+ 'address' => 'New Address',
+ ]);
+
+ $user->refresh();
+ expect($user->userProfile->full_name)->toBe('New Name');
+ expect($user->userProfile->phone_number)->toBe('08999999999');
+ expect($user->userProfile->gender->value)->toBe('female');
+ expect($user->userProfile->address)->toBe('New Address');
+ $this->assertDatabaseCount('user_profiles', 1);
+});
+
+test('profile update with null optional fields saves nulls', function () {
+ $user = User::factory()->create(['username' => 'nulltest']);
+ $user->userProfile()->create([
+ 'full_name' => 'Has Data',
+ 'phone_number' => '08123456789',
+ 'gender' => 'male',
+ 'birth_date' => '1990-01-01',
+ 'address' => 'Some Address',
+ ]);
+
+ $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'nulltest',
+ 'full_name' => 'Still Has Name',
+ 'phone_number' => null,
+ 'gender' => null,
+ 'birth_date' => null,
+ 'address' => null,
+ ]);
+
+ $user->refresh();
+ expect($user->userProfile->full_name)->toBe('Still Has Name');
+ expect($user->userProfile->phone_number)->toBeNull();
+ expect($user->userProfile->gender)->toBeNull();
+ expect($user->userProfile->birth_date)->toBeNull();
+ expect($user->userProfile->address)->toBeNull();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - VALIDATION: EMAIL
+|--------------------------------------------------------------------------
+*/
+
+test('email is required', function () {
+ $user = User::factory()->create(['username' => 'emailreq']);
$response = $this
->actingAs($user)
- ->delete(route('profile.destroy'), [
- 'password' => 'password',
+ ->patch(route('profile.update'), [
+ 'username' => 'emailreq',
+ 'full_name' => 'Test',
]);
- $response
- ->assertSessionHasNoErrors()
- ->assertRedirect(route('home'));
-
- $this->assertGuest();
- expect($user->fresh())->toBeNull();
+ $response->assertSessionHasErrors('email');
});
-test('correct password must be provided to delete account', function () {
- $user = User::factory()->create();
+test('email must be valid format', function () {
+ $user = User::factory()->create(['username' => 'emailfmt']);
$response = $this
->actingAs($user)
- ->from(route('profile.edit'))
- ->delete(route('profile.destroy'), [
- 'password' => 'wrong-password',
+ ->patch(route('profile.update'), [
+ 'email' => 'not-an-email',
+ 'username' => 'emailfmt',
+ 'full_name' => 'Test',
]);
- $response
- ->assertSessionHasErrors('password')
- ->assertRedirect(route('profile.edit'));
-
- expect($user->fresh())->not->toBeNull();
+ $response->assertSessionHasErrors('email');
+});
+
+test('email must not exceed 255 characters', function () {
+ $user = User::factory()->create(['username' => 'emailmax']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => str_repeat('a', 246) . '@example.com',
+ 'username' => 'emailmax',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('email');
+});
+
+test('email must be unique excluding self', function () {
+ $user = User::factory()->create(['email' => 'user1@example.com', 'username' => 'uniqueemail1']);
+ User::factory()->create(['email' => 'user2@example.com', 'username' => 'uniqueemail2']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => 'user2@example.com',
+ 'username' => 'uniqueemail1',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('email');
+});
+
+test('user can keep their own email on update', function () {
+ $user = User::factory()->create(['email' => 'keep@example.com', 'username' => 'keepemail']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => 'keep@example.com',
+ 'username' => 'keepemail',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - VALIDATION: USERNAME
+|--------------------------------------------------------------------------
+*/
+
+test('username is required', function () {
+ $user = User::factory()->create(['username' => 'usernamereq']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('username');
+});
+
+test('username must not exceed 20 characters', function () {
+ $user = User::factory()->create(['username' => 'usernamemax']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => str_repeat('a', 21),
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('username');
+});
+
+test('username must be alpha_dash', function () {
+ $user = User::factory()->create(['username' => 'alphauser']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'has spaces!',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('username');
+});
+
+test('username allows dashes and underscores', function () {
+ $user = User::factory()->create(['username' => 'dashuser']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'user-name_123',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('username must be unique excluding self', function () {
+ $user = User::factory()->create(['username' => 'uniqueuser1']);
+ User::factory()->create(['username' => 'uniqueuser2']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'uniqueuser2',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasErrors('username');
+});
+
+test('user can keep their own username on update', function () {
+ $user = User::factory()->create(['username' => 'keepuser']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'keepuser',
+ 'full_name' => 'Test',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - VALIDATION: FULL NAME
+|--------------------------------------------------------------------------
+*/
+
+test('full_name is required', function () {
+ $user = User::factory()->create(['username' => 'fullnamereq']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'fullnamereq',
+ ]);
+
+ $response->assertSessionHasErrors('full_name');
+});
+
+test('full_name must not exceed 200 characters', function () {
+ $user = User::factory()->create(['username' => 'fullnamemax']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'fullnamemax',
+ 'full_name' => str_repeat('a', 201),
+ ]);
+
+ $response->assertSessionHasErrors('full_name');
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - VALIDATION: OPTIONAL FIELDS
+|--------------------------------------------------------------------------
+*/
+
+test('phone_number is optional', function () {
+ $user = User::factory()->create(['username' => 'phonetest']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'phonetest',
+ 'full_name' => 'Test',
+ 'phone_number' => null,
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('phone_number must not exceed 20 characters', function () {
+ $user = User::factory()->create(['username' => 'phonemax']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'phonemax',
+ 'full_name' => 'Test',
+ 'phone_number' => str_repeat('1', 21),
+ ]);
+
+ $response->assertSessionHasErrors('phone_number');
+});
+
+test('gender must be valid enum value', function () {
+ $user = User::factory()->create(['username' => 'genderbad']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'genderbad',
+ 'full_name' => 'Test',
+ 'gender' => 'other',
+ ]);
+
+ $response->assertSessionHasErrors('gender');
+});
+
+test('gender accepts male', function () {
+ $user = User::factory()->create(['username' => 'gendermale']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'gendermale',
+ 'full_name' => 'Test',
+ 'gender' => 'male',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('gender accepts female', function () {
+ $user = User::factory()->create(['username' => 'genderfemale']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'genderfemale',
+ 'full_name' => 'Test',
+ 'gender' => 'female',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('birth_date is optional', function () {
+ $user = User::factory()->create(['username' => 'birthnull']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'birthnull',
+ 'full_name' => 'Test',
+ 'birth_date' => null,
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+test('birth_date must be valid date', function () {
+ $user = User::factory()->create(['username' => 'birthbad']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'birthbad',
+ 'full_name' => 'Test',
+ 'birth_date' => 'not-a-date',
+ ]);
+
+ $response->assertSessionHasErrors('birth_date');
+});
+
+test('address is optional', function () {
+ $user = User::factory()->create(['username' => 'addrnull']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'addrnull',
+ 'full_name' => 'Test',
+ 'address' => null,
+ ]);
+
+ $response->assertSessionHasNoErrors();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - VALIDATION: EMPTY REQUEST
+|--------------------------------------------------------------------------
+*/
+
+test('user cannot update profile without submitting any data', function () {
+ $user = User::factory()->create(['username' => 'emptyreq']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), []);
+
+ $response->assertSessionHasErrors(['email', 'username', 'full_name']);
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - TOAST / FLASH
+|--------------------------------------------------------------------------
+*/
+
+test('profile update flashes success toast via inertia', function () {
+ $user = User::factory()->create(['username' => 'toastuser']);
+
+ $response = $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'toastuser',
+ 'full_name' => 'Toast Test',
+ ]);
+
+ $response->assertRedirect();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PROFILE UPDATE - DATA INTEGRITY
+|--------------------------------------------------------------------------
+*/
+
+test('profile update preserves other user fields', function () {
+ $user = User::factory()->create(['username' => 'preservetest']);
+
+ $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'preservetest',
+ 'full_name' => 'New Name',
+ ]);
+
+ $user->refresh();
+ expect(Hash::check('password', $user->password))->toBeTrue();
+});
+
+test('profile update with realistic indonesian data', function () {
+ $user = User::factory()->create(['email' => 'budi.santoso@gmail.com', 'username' => 'budisan']);
+
+ $this
+ ->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => 'budi.santoso@gmail.com',
+ 'username' => 'budisan',
+ 'full_name' => 'Budi Santoso',
+ 'phone_number' => '08123456789',
+ 'gender' => 'male',
+ 'birth_date' => '1990-05-15',
+ 'address' => 'Jl. Sudirman No. 123, Jakarta Selatan',
+ ]);
+
+ $user->refresh();
+ expect($user->email)->toBe('budi.santoso@gmail.com');
+ expect($user->username)->toBe('budisan');
+ expect($user->userProfile->full_name)->toBe('Budi Santoso');
+ expect($user->userProfile->phone_number)->toBe('08123456789');
+ expect($user->userProfile->gender->value)->toBe('male');
+ expect($user->userProfile->birth_date->format('Y-m-d'))->toBe('1990-05-15');
+ expect($user->userProfile->address)->toBe('Jl. Sudirman No. 123, Jakarta Selatan');
+});
+
+/*
+|--------------------------------------------------------------------------
+| ROUTE TESTS
+|--------------------------------------------------------------------------
+*/
+
+test('settings redirect goes to profile edit', function () {
+ $user = User::factory()->create(['username' => 'redirtest']);
+
+ $response = $this
+ ->actingAs($user)
+ ->get('/settings');
+
+ $response->assertRedirect('/settings/profile');
+});
+
+/*
+|--------------------------------------------------------------------------
+| REALISTIC SCENARIOS
+|--------------------------------------------------------------------------
+*/
+
+test('user updates profile then views it again', function () {
+ $user = User::factory()->create(['username' => 'scenariouser']);
+
+ $this->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'scenariouser',
+ 'full_name' => 'Scenario User',
+ 'phone_number' => '08123456789',
+ 'gender' => 'female',
+ 'birth_date' => '1995-08-20',
+ 'address' => 'Jl. Gatot Subroto No. 45',
+ ]);
+
+ $this->actingAs($user->fresh())
+ ->get(route('profile.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->where('user.userProfile.full_name', 'Scenario User')
+ ->where('user.userProfile.phone_number', '08123456789')
+ ->where('user.userProfile.gender', 'female')
+ ->where('user.userProfile.birth_date', '1995-08-20')
+ ->where('user.userProfile.address', 'Jl. Gatot Subroto No. 45')
+ );
+});
+
+test('user updates profile multiple times keeps only latest data', function () {
+ $user = User::factory()->create(['username' => 'multitimes']);
+
+ $this->actingAs($user)
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'multitimes',
+ 'full_name' => 'First Update',
+ 'phone_number' => '08111111111',
+ ]);
+
+ $this->actingAs($user->fresh())
+ ->patch(route('profile.update'), [
+ 'email' => $user->email,
+ 'username' => 'multitimes',
+ 'full_name' => 'Second Update',
+ 'phone_number' => '08222222222',
+ ]);
+
+ $user->refresh();
+ expect($user->userProfile->full_name)->toBe('Second Update');
+ expect($user->userProfile->phone_number)->toBe('08222222222');
+ $this->assertDatabaseCount('user_profiles', 1);
});
diff --git a/tests/Feature/Settings/SecurityTest.php b/tests/Feature/Settings/SecurityTest.php
index c190619..102427d 100644
--- a/tests/Feature/Settings/SecurityTest.php
+++ b/tests/Feature/Settings/SecurityTest.php
@@ -1,72 +1,88 @@
get(route('security.edit'));
+ $response->assertRedirect(route('login'));
+});
+
+test('guests are redirected to the login page for password update', function () {
+ $response = $this->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+ $response->assertRedirect(route('login'));
+});
+
+/*
+|--------------------------------------------------------------------------
+| SECURITY EDIT PAGE
+|--------------------------------------------------------------------------
+*/
test('security page is displayed', function () {
- $this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
-
- Features::twoFactorAuthentication([
- 'confirm' => true,
- 'confirmPassword' => true,
- ]);
- Features::passkeys([
- 'confirmPassword' => true,
- ]);
-
$user = User::factory()->create();
- $this->actingAs($user)
- ->withSession(['auth.password_confirmed_at' => time()])
- ->get(route('security.edit'))
- ->assertInertia(fn (Assert $page) => $page
- ->component('settings/security')
- ->where('canManagePasskeys', true)
- ->where('passkeys', [])
- ->where('canManageTwoFactor', true)
- ->where('twoFactorEnabled', false),
- );
-});
-
-test('security page requires password confirmation when enabled', function () {
- $this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
-
- $user = User::factory()->create();
-
- Features::twoFactorAuthentication([
- 'confirm' => true,
- 'confirmPassword' => true,
- ]);
-
- $response = $this->actingAs($user)
+ $response = $this
+ ->actingAs($user)
->get(route('security.edit'));
- $response->assertRedirect(route('password.confirm'));
+ $response->assertOk();
});
-test('security page renders without two factor when feature is disabled', function () {
- $this->skipUnlessFortifyHas(Features::twoFactorAuthentication());
-
- config(['fortify.features' => []]);
-
+test('security page renders correct inertia component', function () {
$user = User::factory()->create();
$this->actingAs($user)
- ->withSession(['auth.password_confirmed_at' => time()])
->get(route('security.edit'))
- ->assertOk()
->assertInertia(fn (Assert $page) => $page
->component('settings/security')
- ->where('canManagePasskeys', false)
- ->where('passkeys', [])
- ->where('canManageTwoFactor', false)
- ->missing('twoFactorEnabled')
- ->missing('requiresConfirmation'),
);
});
+test('security page passes password rules', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->get(route('security.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->has('passwordRules')
+ );
+});
+
+test('security page does not pass two factor or passkey data', function () {
+ $user = User::factory()->create();
+
+ $this->actingAs($user)
+ ->get(route('security.edit'))
+ ->assertInertia(fn (Assert $page) => $page
+ ->missing('canManageTwoFactor')
+ ->missing('canManagePasskeys')
+ ->missing('passkeys')
+ ->missing('twoFactorEnabled')
+ ->missing('requiresConfirmation')
+ );
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - SUCCESS
+|--------------------------------------------------------------------------
+*/
+
test('password can be updated', function () {
$user = User::factory()->create();
@@ -86,7 +102,28 @@
expect(Hash::check('new-password', $user->refresh()->password))->toBeTrue();
});
-test('correct password must be provided to update password', function () {
+test('password update redirects back to security page', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'another-new-password',
+ 'password_confirmation' => 'another-new-password',
+ ]);
+
+ $response->assertRedirect();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - VALIDATION: CURRENT PASSWORD
+|--------------------------------------------------------------------------
+*/
+
+test('correct current password must be provided to update password', function () {
$user = User::factory()->create();
$response = $this
@@ -101,4 +138,193 @@
$response
->assertSessionHasErrors('current_password')
->assertRedirect(route('security.edit'));
+
+ expect(Hash::check('new-password', $user->refresh()->password))->toBeFalse();
+});
+
+test('current_password is required', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response->assertSessionHasErrors('current_password');
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - VALIDATION: NEW PASSWORD
+|--------------------------------------------------------------------------
+*/
+
+test('password is required', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response->assertSessionHasErrors('password');
+});
+
+test('password must be confirmed', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ ]);
+
+ $response->assertSessionHasErrors('password');
+});
+
+test('password and confirmation must match', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'different-password',
+ ]);
+
+ $response->assertSessionHasErrors('password');
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - VALIDATION: EMPTY REQUEST
+|--------------------------------------------------------------------------
+*/
+
+test('user cannot update password without submitting any data', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), []);
+
+ $response->assertSessionHasErrors(['current_password', 'password']);
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - TOAST / FLASH
+|--------------------------------------------------------------------------
+*/
+
+test('password update flashes success toast via inertia', function () {
+ $user = User::factory()->create();
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response->assertRedirect();
+});
+
+/*
+|--------------------------------------------------------------------------
+| PASSWORD UPDATE - DATA INTEGRITY
+|--------------------------------------------------------------------------
+*/
+
+test('password update does not change other user fields', function () {
+ $user = User::factory()->create([
+ 'email' => 'test@example.com',
+ 'username' => 'testuser',
+ ]);
+
+ $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $user->refresh();
+ expect($user->email)->toBe('test@example.com');
+ expect($user->username)->toBe('testuser');
+});
+
+/*
+|--------------------------------------------------------------------------
+| AUTHORIZATION - GUEST CANNOT PERFORM ACTIONS
+|--------------------------------------------------------------------------
+*/
+
+test('guest cannot access security page', function () {
+ $response = $this->get(route('security.edit'));
+ $response->assertRedirect(route('login'));
+});
+
+test('guest cannot update password', function () {
+ $response = $this->put(route('user-password.update'), [
+ 'current_password' => 'password',
+ 'password' => 'new-password',
+ 'password_confirmation' => 'new-password',
+ ]);
+
+ $response->assertRedirect(route('login'));
+});
+
+/*
+|--------------------------------------------------------------------------
+| REALISTIC USER SCENARIOS
+|--------------------------------------------------------------------------
+*/
+
+test('user changes password then can login with new password', function () {
+ $user = User::factory()->create(['password' => 'old-password']);
+
+ $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'old-password',
+ 'password' => 'brand-new-password',
+ 'password_confirmation' => 'brand-new-password',
+ ]);
+
+ $user->refresh();
+ expect(Hash::check('brand-new-password', $user->password))->toBeTrue();
+ expect(Hash::check('old-password', $user->password))->toBeFalse();
+});
+
+test('user tries to set same password as current', function () {
+ $user = User::factory()->create(['password' => 'same-password']);
+
+ $response = $this
+ ->actingAs($user)
+ ->from(route('security.edit'))
+ ->put(route('user-password.update'), [
+ 'current_password' => 'same-password',
+ 'password' => 'same-password',
+ 'password_confirmation' => 'same-password',
+ ]);
+
+ $response->assertSessionHasNoErrors();
+ expect(Hash::check('same-password', $user->refresh()->password))->toBeTrue();
});