libra/tests/Feature/Admin/AccountLifecycleTest.php

559 lines
18 KiB
PHP

<?php
use App\Enums\UserStatus;
use App\Models\AdminLog;
use App\Models\Consultation;
use App\Models\Notification;
use App\Models\Timeline;
use App\Models\TimelineUpdate;
use App\Models\User;
use App\Notifications\AccountReactivatedNotification;
use App\Notifications\PasswordResetByAdminNotification;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Notification as NotificationFacade;
use Livewire\Volt\Volt;
beforeEach(function () {
$this->admin = User::factory()->admin()->create();
});
// ===========================================
// Deactivation Tests
// ===========================================
test('admin can deactivate an active individual client', function () {
$client = User::factory()->individual()->create(['status' => UserStatus::Active]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->assertSet('showDeactivateModal', true)
->call('deactivate')
->assertHasNoErrors();
expect($client->fresh()->status)->toBe(UserStatus::Deactivated);
});
test('admin can deactivate an active company client', function () {
$client = User::factory()->company()->create(['status' => UserStatus::Active]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->call('deactivate')
->assertHasNoErrors();
expect($client->fresh()->status)->toBe(UserStatus::Deactivated);
});
test('deactivated user cannot login', function () {
$user = User::factory()->individual()->create([
'status' => UserStatus::Deactivated,
'email' => 'deactivated@example.com',
]);
$this->post('/login', [
'email' => 'deactivated@example.com',
'password' => 'password',
])->assertSessionHasErrors();
$this->assertGuest();
});
test('user sessions are invalidated on deactivation', function () {
$client = User::factory()->individual()->create(['status' => UserStatus::Active]);
// Create a fake session for the user
DB::table('sessions')->insert([
'id' => 'test-session-id',
'user_id' => $client->id,
'ip_address' => '127.0.0.1',
'user_agent' => 'Test',
'payload' => 'test',
'last_activity' => time(),
]);
expect(DB::table('sessions')->where('user_id', $client->id)->exists())->toBeTrue();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->call('deactivate');
expect(DB::table('sessions')->where('user_id', $client->id)->exists())->toBeFalse();
});
test('admin log entry created on deactivation', function () {
$client = User::factory()->individual()->create(['status' => UserStatus::Active]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->call('deactivate');
expect(AdminLog::where('action', 'deactivate')
->where('target_type', 'user')
->where('target_id', $client->id)
->where('admin_id', $this->admin->id)
->exists())->toBeTrue();
$log = AdminLog::where('action', 'deactivate')->where('target_id', $client->id)->first();
expect($log->old_values)->toHaveKey('status', 'active');
expect($log->new_values)->toHaveKey('status', 'deactivated');
});
test('deactivation preserves all user data', function () {
$client = User::factory()->individual()->create(['status' => UserStatus::Active]);
// Create related data
Consultation::factory()->create(['user_id' => $client->id]);
$timeline = Timeline::factory()->create(['user_id' => $client->id]);
TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'admin_id' => $this->admin->id,
]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->call('deactivate');
// Verify data is still there
expect(Consultation::where('user_id', $client->id)->count())->toBe(1);
expect(Timeline::where('user_id', $client->id)->count())->toBe(1);
expect($client->fresh())->not->toBeNull();
});
// ===========================================
// Reactivation Tests
// ===========================================
test('admin can reactivate a deactivated individual client', function () {
$client = User::factory()->individual()->deactivated()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openReactivateModal')
->assertSet('showReactivateModal', true)
->call('reactivate')
->assertHasNoErrors();
expect($client->fresh()->status)->toBe(UserStatus::Active);
});
test('admin can reactivate a deactivated company client', function () {
$client = User::factory()->company()->deactivated()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openReactivateModal')
->call('reactivate')
->assertHasNoErrors();
expect($client->fresh()->status)->toBe(UserStatus::Active);
});
test('reactivated user can login successfully', function () {
$client = User::factory()->individual()->create([
'status' => UserStatus::Deactivated,
'email' => 'reactivate-test@example.com',
]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openReactivateModal')
->call('reactivate');
$this->post('/login', [
'email' => 'reactivate-test@example.com',
'password' => 'password',
])->assertSessionHasNoErrors();
});
test('email notification queued on reactivation', function () {
NotificationFacade::fake();
$client = User::factory()->individual()->deactivated()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openReactivateModal')
->call('reactivate')
->assertHasNoErrors();
NotificationFacade::assertSentTo($client, AccountReactivatedNotification::class);
});
test('admin log entry created on reactivation', function () {
$client = User::factory()->individual()->deactivated()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openReactivateModal')
->call('reactivate');
expect(AdminLog::where('action', 'reactivate')
->where('target_type', 'user')
->where('target_id', $client->id)
->where('admin_id', $this->admin->id)
->exists())->toBeTrue();
$log = AdminLog::where('action', 'reactivate')->where('target_id', $client->id)->first();
expect($log->old_values)->toHaveKey('status', 'deactivated');
expect($log->new_values)->toHaveKey('status', 'active');
});
// ===========================================
// Permanent Deletion Tests
// ===========================================
test('delete requires email confirmation', function () {
$client = User::factory()->individual()->create(['email' => 'delete-test@example.com']);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->assertSet('showDeleteModal', true)
->set('deleteConfirmation', 'wrong@email.com')
->call('delete')
->assertHasErrors(['deleteConfirmation']);
expect(User::find($client->id))->not->toBeNull();
});
test('successful deletion removes user record permanently', function () {
$client = User::factory()->individual()->create(['email' => 'permanent-delete@example.com']);
$clientId = $client->id;
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'permanent-delete@example.com')
->call('delete')
->assertHasNoErrors()
->assertRedirect(route('admin.clients.individual.index'));
expect(User::find($clientId))->toBeNull();
});
test('cascade deletion removes user consultations', function () {
$client = User::factory()->individual()->create(['email' => 'cascade-test@example.com']);
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'cascade-test@example.com')
->call('delete');
expect(Consultation::find($consultation->id))->toBeNull();
});
test('cascade deletion removes user timelines and timeline updates', function () {
$client = User::factory()->individual()->create(['email' => 'cascade-timeline@example.com']);
$timeline = Timeline::factory()->create(['user_id' => $client->id]);
$update = TimelineUpdate::factory()->create([
'timeline_id' => $timeline->id,
'admin_id' => $this->admin->id,
]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'cascade-timeline@example.com')
->call('delete');
expect(Timeline::find($timeline->id))->toBeNull();
expect(TimelineUpdate::find($update->id))->toBeNull();
});
test('cascade deletion removes user notifications', function () {
$client = User::factory()->individual()->create(['email' => 'cascade-notify@example.com']);
$notification = Notification::factory()->create(['user_id' => $client->id]);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'cascade-notify@example.com')
->call('delete');
expect(Notification::find($notification->id))->toBeNull();
});
test('admin log entry preserved after user deletion', function () {
$client = User::factory()->individual()->create(['email' => 'log-preserve@example.com']);
$clientId = $client->id;
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'log-preserve@example.com')
->call('delete');
expect(AdminLog::where('action', 'delete')
->where('target_type', 'user')
->where('target_id', $clientId)
->where('admin_id', $this->admin->id)
->exists())->toBeTrue();
});
test('company client redirects to company index after deletion', function () {
$client = User::factory()->company()->create(['email' => 'company-delete@example.com']);
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeleteModal')
->set('deleteConfirmation', 'company-delete@example.com')
->call('delete')
->assertRedirect(route('admin.clients.company.index'));
});
// ===========================================
// Password Reset Tests
// ===========================================
test('admin can reset user password', function () {
$client = User::factory()->individual()->create();
$originalPasswordHash = $client->password;
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openPasswordResetModal')
->assertSet('showPasswordResetModal', true)
->call('resetPassword')
->assertHasNoErrors();
expect($client->fresh()->password)->not->toBe($originalPasswordHash);
});
test('new password meets minimum requirements', function () {
$client = User::factory()->individual()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openPasswordResetModal')
->call('resetPassword');
// Password should be 12 characters, hashed
// We can't check the length directly, but we can verify it's hashed
expect(strlen($client->fresh()->password))->toBeGreaterThan(50);
});
test('password reset email sent to user', function () {
NotificationFacade::fake();
$client = User::factory()->individual()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openPasswordResetModal')
->call('resetPassword');
NotificationFacade::assertSentTo($client, PasswordResetByAdminNotification::class);
});
test('admin log entry created for password reset', function () {
$client = User::factory()->individual()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openPasswordResetModal')
->call('resetPassword');
expect(AdminLog::where('action', 'password_reset')
->where('target_type', 'user')
->where('target_id', $client->id)
->where('admin_id', $this->admin->id)
->exists())->toBeTrue();
});
test('user can login with new password after reset', function () {
NotificationFacade::fake();
$client = User::factory()->individual()->create([
'email' => 'newpass-test@example.com',
]);
$this->actingAs($this->admin);
// Reset the password
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openPasswordResetModal')
->call('resetPassword');
// Get the new password from the notification
NotificationFacade::assertSentTo(
$client,
PasswordResetByAdminNotification::class,
function ($notification) use ($client) {
// Test that the new password works
$newPassword = $notification->newPassword;
auth()->logout();
// Try logging in with new password
$response = $this->post('/login', [
'email' => $client->email,
'password' => $newPassword,
]);
return true;
}
);
});
// ===========================================
// Authorization Tests
// ===========================================
test('non-admin cannot access lifecycle actions', function () {
$client = User::factory()->individual()->create();
$this->actingAs($client);
$this->get(route('admin.clients.individual.show', $client))
->assertForbidden();
});
test('admin cannot deactivate their own account', function () {
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $this->admin])
->call('openDeactivateModal')
->call('deactivate')
->assertSet('showDeactivateModal', false);
// Verify status was not changed
expect($this->admin->fresh()->status)->toBe(UserStatus::Active);
// Verify no admin log was created for this action
expect(AdminLog::where('action', 'deactivate')
->where('target_id', $this->admin->id)
->exists())->toBeFalse();
});
test('admin cannot delete their own account', function () {
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $this->admin])
->call('openDeleteModal')
->set('deleteConfirmation', $this->admin->email)
->call('delete')
->assertHasErrors(['deleteConfirmation']);
expect(User::find($this->admin->id))->not->toBeNull();
});
// ===========================================
// Bilingual Tests
// ===========================================
test('confirmation dialogs display in Arabic when locale is ar', function () {
app()->setLocale('ar');
$client = User::factory()->individual()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->assertSee(__('clients.confirm_deactivate'));
});
test('confirmation dialogs display in English when locale is en', function () {
app()->setLocale('en');
$client = User::factory()->individual()->create();
$this->actingAs($this->admin);
Volt::test('admin.clients.lifecycle-actions', ['client' => $client])
->call('openDeactivateModal')
->assertSee('Confirm Deactivation');
});
// ===========================================
// Visual Indicator Tests
// ===========================================
test('deactivated user shows visual indicator in individual client list', function () {
$activeClient = User::factory()->individual()->create([
'full_name' => 'Active Client',
'status' => UserStatus::Active,
]);
$deactivatedClient = User::factory()->individual()->deactivated()->create([
'full_name' => 'Deactivated Client',
]);
$this->actingAs($this->admin);
Volt::test('admin.clients.individual.index')
->assertSee('Active Client')
->assertSee('Deactivated Client');
});
test('deactivated user shows visual indicator in company client list', function () {
$activeCompany = User::factory()->company()->create([
'company_name' => 'Active Company',
'status' => UserStatus::Active,
]);
$deactivatedCompany = User::factory()->company()->deactivated()->create([
'company_name' => 'Deactivated Company',
]);
$this->actingAs($this->admin);
Volt::test('admin.clients.company.index')
->assertSee('Active Company')
->assertSee('Deactivated Company');
});
// ===========================================
// User Model Method Tests
// ===========================================
test('user model isDeactivated method works correctly', function () {
$activeUser = User::factory()->individual()->create(['status' => UserStatus::Active]);
$deactivatedUser = User::factory()->individual()->deactivated()->create();
expect($activeUser->isDeactivated())->toBeFalse();
expect($deactivatedUser->isDeactivated())->toBeTrue();
});
test('user model deactivate method changes status', function () {
$user = User::factory()->individual()->create(['status' => UserStatus::Active]);
$user->deactivate();
expect($user->fresh()->status)->toBe(UserStatus::Deactivated);
});
test('user model reactivate method changes status', function () {
$user = User::factory()->individual()->deactivated()->create();
$user->reactivate();
expect($user->fresh()->status)->toBe(UserStatus::Active);
});