clusev/tests/Feature/MultiUserTest.php

263 lines
9.4 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_with_an_operator_password_uses_it_and_skips_rotation(): void
{
$admin = User::factory()->create();
Livewire::actingAs($admin)->test(CreateUser::class)
->set('name', 'Set Pw')
->set('email', 'setpw@example.com')
->set('password', 'Chosen-Pass-123')
->call('save')
->assertHasNoErrors()
->assertSet('tempPassword', null) // no reveal — the operator already knows it
->assertDispatched('usersChanged');
$new = User::where('email', 'setpw@example.com')->first();
$this->assertNotNull($new);
$this->assertFalse($new->must_change_password); // operator-set = the user's own login
$this->assertTrue(Hash::check('Chosen-Pass-123', $new->password));
}
public function test_create_rejects_a_weak_operator_password(): void
{
$admin = User::factory()->create();
Livewire::actingAs($admin)->test(CreateUser::class)
->set('name', 'Weak')
->set('email', 'weak@example.com')
->set('password', 'short')
->call('save')
->assertHasErrors('password');
$this->assertNull(User::where('email', 'weak@example.com')->first());
}
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');
}
}