559 lines
18 KiB
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);
|
|
});
|