insert([ 'id' => $id, 'user_id' => $user->id, 'ip_address' => '203.0.113.1', 'user_agent' => 'Mozilla/5.0 Chrome', 'payload' => base64_encode(serialize([])), 'last_activity' => now()->getTimestamp(), ]); } public function test_create_makes_a_must_change_password_user_and_audits(): void { $admin = User::factory()->create(); Livewire::actingAs($admin)->test(CreateUser::class) ->set('name', 'Erika Mustermann') ->set('email', 'erika@example.com') ->call('save') ->assertHasNoErrors() // The clear-text temp password is revealed once in component state. ->assertSet('createdName', 'Erika Mustermann') ->assertSet('tempPassword', fn ($pw) => is_string($pw) && strlen($pw) === 16); $new = User::where('email', 'erika@example.com')->first(); $this->assertNotNull($new); $this->assertTrue($new->must_change_password); // Password is hashed at rest (never the clear text). $this->assertNotSame('', $new->password); $this->assertNotSame(16, strlen($new->password)); $this->assertDatabaseHas('audit_events', [ 'action' => 'user.create', 'target' => 'erika@example.com', 'user_id' => $admin->id, ]); } public function test_create_temp_password_actually_authenticates(): void { $admin = User::factory()->create(); $component = Livewire::actingAs($admin)->test(CreateUser::class) ->set('name', 'Temp User') ->set('email', 'temp@example.com') ->call('save'); $temp = $component->get('tempPassword'); $new = User::where('email', 'temp@example.com')->first(); $this->assertTrue(Hash::check($temp, $new->password)); } public function test_create_rejects_duplicate_email(): void { $admin = User::factory()->create(); User::factory()->create(['email' => 'taken@example.com']); Livewire::actingAs($admin)->test(CreateUser::class) ->set('name', 'Dup') ->set('email', 'taken@example.com') ->call('save') ->assertHasErrors('email'); $this->assertSame(2, User::count()); } public function test_finish_dispatches_users_changed_and_closes(): void { $admin = User::factory()->create(); Livewire::actingAs($admin)->test(CreateUser::class) ->call('finish') ->assertDispatched('usersChanged') ->assertDispatched('closeModal'); } public function test_cannot_delete_self(): void { $admin = User::factory()->create(); User::factory()->create(); // keep count > 1 so only the self-guard is in play Livewire::actingAs($admin)->test(Users::class) ->call('confirmRemove', $admin->id) ->assertNotDispatched('openModal'); $this->actingAs($admin); $token = ConfirmToken::issue('userRemoved', ['userId' => $admin->id]); ConfirmToken::confirm($token); Livewire::actingAs($admin)->test(Users::class) ->call('remove', $token); $this->assertDatabaseHas('users', ['id' => $admin->id]); $this->assertSame(0, AuditEvent::where('action', 'user.delete')->count()); } public function test_cannot_delete_the_last_user(): void { $admin = User::factory()->create(); $this->assertSame(1, User::count()); Livewire::actingAs($admin)->test(Users::class) ->call('confirmRemove', $admin->id) ->assertNotDispatched('openModal'); $this->actingAs($admin); $token = ConfirmToken::issue('userRemoved', ['userId' => $admin->id]); ConfirmToken::confirm($token); Livewire::actingAs($admin)->test(Users::class) ->call('remove', $token); $this->assertDatabaseHas('users', ['id' => $admin->id]); } public function test_remove_confirm_opens_modal_with_audit_descriptor(): void { $admin = User::factory()->create(); $victim = User::factory()->create(['email' => 'victim@example.com']); Livewire::actingAs($admin)->test(Users::class) ->call('confirmRemove', $victim->id) ->assertDispatched('openModal'); } public function test_remove_deletes_user_kills_sessions_and_audits(): void { config(['session.driver' => 'database']); $admin = User::factory()->create(); $victim = User::factory()->create(['email' => 'victim@example.com', 'remember_token' => 'victim-token']); $this->seedSession($victim, 'victim-sess'); $this->actingAs($admin); $token = ConfirmToken::issue('userRemoved', ['userId' => $victim->id]); ConfirmToken::confirm($token); Livewire::actingAs($admin)->test(Users::class) ->call('remove', $token); // User gone. $this->assertDatabaseMissing('users', ['id' => $victim->id]); // SessionService::logoutUserEverywhere ran BEFORE delete — rows cleared. $this->assertSame(0, DB::table('sessions')->where('id', 'victim-sess')->count()); // Audit row written with the current actor. $this->assertDatabaseHas('audit_events', [ 'action' => 'user.delete', 'target' => 'victim@example.com', 'user_id' => $admin->id, ]); } public function test_per_row_logout_rotates_target_token_and_audits(): void { config(['session.driver' => 'database']); $admin = User::factory()->create(); $target = User::factory()->create(['email' => 'target@example.com', 'remember_token' => 'target-token']); $this->seedSession($target, 'target-sess'); $this->actingAs($admin); $token = ConfirmToken::issue('userLoggedOut', ['userId' => $target->id]); ConfirmToken::confirm($token); Livewire::actingAs($admin)->test(Users::class) ->call('logout', $token); // Target still exists but is signed out everywhere + token rotated. $this->assertDatabaseHas('users', ['id' => $target->id]); $this->assertSame(0, DB::table('sessions')->where('id', 'target-sess')->count()); $this->assertNotSame('target-token', $target->fresh()->remember_token); $this->assertNotNull($target->fresh()->remember_token); $this->assertDatabaseHas('audit_events', [ 'action' => 'user.logout', 'target' => 'target@example.com', 'user_id' => $admin->id, ]); } public function test_cannot_force_logout_self(): void { $admin = User::factory()->create(); Livewire::actingAs($admin)->test(Users::class) ->call('confirmLogout', $admin->id) ->assertNotDispatched('openModal'); $this->actingAs($admin); $token = ConfirmToken::issue('userLoggedOut', ['userId' => $admin->id]); ConfirmToken::confirm($token); Livewire::actingAs($admin)->test(Users::class) ->call('logout', $token); $this->assertSame(0, AuditEvent::where('action', 'user.logout')->count()); } public function test_list_renders_all_users(): void { $admin = User::factory()->create(['name' => 'Aaron Admin']); User::factory()->create(['name' => 'Bonnie Boss']); Livewire::actingAs($admin)->test(Users::class) ->assertOk() ->assertViewHas('users', fn ($users) => $users->count() === 2) ->assertSee('Aaron Admin') ->assertSee('Bonnie Boss'); } }