230 lines
8.1 KiB
PHP
230 lines
8.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Livewire\Modals\CreateUser;
|
|
use App\Livewire\Settings\Users;
|
|
use App\Models\AuditEvent;
|
|
use App\Models\User;
|
|
use App\Support\Confirm\ConfirmToken;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Session;
|
|
use Livewire\Livewire;
|
|
use Tests\TestCase;
|
|
|
|
class MultiUserTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/** Insert a raw session row for $user (mirrors SessionService's table contract). */
|
|
private function seedSession(User $user, string $id): void
|
|
{
|
|
DB::table('sessions')->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');
|
|
}
|
|
}
|