feat: replace token-based impersonation with enterprise-grade header-based system
Replaces the insecure token-in-localStorage approach with a header-based impersonation system backed by cache sessions and MFA verification. Key changes: - New impersonation_sessions audit table (immutable, ULID PK) - MFA verification required to start impersonation (TOTP/email/backup) - X-Impersonate-User header + HandleImpersonation middleware - Per-request auth context swap (admin session never modified) - IP pinning, sensitive route blocking, no nesting, sliding 60-min TTL - Activity log auto-tagged with impersonated_by during sessions - Frontend: sessionStorage, BroadcastChannel sync, countdown timer - ImpersonateDialog with reason + MFA verification flow - 26 comprehensive tests covering core, middleware, audit, lifecycle Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,18 @@ declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api\V1\Admin;
|
||||
|
||||
use App\Enums\MfaMethod;
|
||||
use App\Models\ImpersonationSession;
|
||||
use App\Models\MfaBackupCode;
|
||||
use App\Models\MfaEmailCode;
|
||||
use App\Models\User;
|
||||
use App\Services\ImpersonationService;
|
||||
use Database\Seeders\RoleSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use PragmaRX\Google2FA\Google2FA;
|
||||
use Spatie\Activitylog\Models\Activity;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -18,47 +26,192 @@ class AdminImpersonationTest extends TestCase
|
||||
private User $superAdmin;
|
||||
private User $targetUser;
|
||||
private User $otherSuperAdmin;
|
||||
private string $totpSecret;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->seed(RoleSeeder::class);
|
||||
|
||||
$this->superAdmin = User::factory()->create();
|
||||
// Create super admin with MFA enabled (TOTP)
|
||||
$this->totpSecret = (new Google2FA)->generateSecretKey(32);
|
||||
$this->superAdmin = User::factory()->create([
|
||||
'mfa_enabled' => true,
|
||||
'mfa_method' => MfaMethod::TOTP->value,
|
||||
'mfa_secret' => encrypt($this->totpSecret),
|
||||
'mfa_confirmed_at' => now(),
|
||||
]);
|
||||
$this->superAdmin->assignRole('super_admin');
|
||||
|
||||
$this->targetUser = User::factory()->create();
|
||||
|
||||
$this->otherSuperAdmin = User::factory()->create();
|
||||
$this->otherSuperAdmin = User::factory()->create([
|
||||
'mfa_enabled' => true,
|
||||
'mfa_method' => MfaMethod::TOTP->value,
|
||||
'mfa_secret' => encrypt((new Google2FA)->generateSecretKey(32)),
|
||||
'mfa_confirmed_at' => now(),
|
||||
]);
|
||||
$this->otherSuperAdmin->assignRole('super_admin');
|
||||
}
|
||||
|
||||
// ─── Start ───────────────────────────────────────────────
|
||||
private function validTotpCode(): string
|
||||
{
|
||||
return (new Google2FA)->getCurrentOtp($this->totpSecret);
|
||||
}
|
||||
|
||||
public function test_start_creates_token_for_target_user(): void
|
||||
private function startPayload(array $overrides = []): array
|
||||
{
|
||||
return array_merge([
|
||||
'reason' => 'Investigating user issue',
|
||||
'mfa_code' => $this->validTotpCode(),
|
||||
'mfa_method' => 'totp',
|
||||
], $overrides);
|
||||
}
|
||||
|
||||
// ─── Core: Start ────────────────────────────────────────────
|
||||
|
||||
public function test_start_creates_redis_and_db_session(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/admin/impersonate/{$this->targetUser->id}");
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonStructure([
|
||||
'data' => ['token', 'user' => ['id', 'email'], 'admin_id'],
|
||||
'data' => [
|
||||
'session' => ['id', 'admin_id', 'target_user_id', 'reason', 'mfa_method', 'started_at', 'expires_at'],
|
||||
'user' => ['id', 'email'],
|
||||
],
|
||||
]);
|
||||
$response->assertJsonPath('data.user.id', $this->targetUser->id);
|
||||
$response->assertJsonPath('data.admin_id', $this->superAdmin->id);
|
||||
$response->assertJsonPath('data.session.admin_id', $this->superAdmin->id);
|
||||
|
||||
$this->assertDatabaseHas('personal_access_tokens', [
|
||||
'tokenable_id' => $this->targetUser->id,
|
||||
'name' => 'impersonation-by-' . $this->superAdmin->id,
|
||||
$this->assertDatabaseHas('impersonation_sessions', [
|
||||
'admin_id' => $this->superAdmin->id,
|
||||
'target_user_id' => $this->targetUser->id,
|
||||
'reason' => 'Investigating user issue',
|
||||
'mfa_method' => 'totp',
|
||||
]);
|
||||
|
||||
// Verify cache entry
|
||||
$session = ImpersonationSession::first();
|
||||
$cacheKey = "impersonation:{$this->superAdmin->id}:{$this->targetUser->id}";
|
||||
$this->assertEquals($session->id, Cache::get($cacheKey));
|
||||
}
|
||||
|
||||
public function test_start_requires_reason(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(['reason' => '']),
|
||||
);
|
||||
|
||||
$response->assertUnprocessable();
|
||||
}
|
||||
|
||||
public function test_start_requires_reason_minimum_length(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(['reason' => 'ab']),
|
||||
);
|
||||
|
||||
$response->assertUnprocessable();
|
||||
}
|
||||
|
||||
public function test_start_requires_valid_mfa_code(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(['mfa_code' => '000000']),
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_start_with_email_code(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
// Create a valid email code for the admin
|
||||
MfaEmailCode::create([
|
||||
'user_id' => $this->superAdmin->id,
|
||||
'code' => '123456',
|
||||
'expires_at' => now()->addMinutes(10),
|
||||
]);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload([
|
||||
'mfa_code' => '123456',
|
||||
'mfa_method' => 'email',
|
||||
]),
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('impersonation_sessions', [
|
||||
'mfa_method' => 'email',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_start_with_backup_code(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$plainCode = 'ABCD-EFGH';
|
||||
MfaBackupCode::create([
|
||||
'user_id' => $this->superAdmin->id,
|
||||
'code_hash' => Hash::make($plainCode),
|
||||
]);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload([
|
||||
'mfa_code' => $plainCode,
|
||||
'mfa_method' => 'backup_code',
|
||||
]),
|
||||
);
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('impersonation_sessions', [
|
||||
'mfa_method' => 'backup_code',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_start_requires_admin_mfa_enabled(): void
|
||||
{
|
||||
$adminNoMfa = User::factory()->create(['mfa_enabled' => false]);
|
||||
$adminNoMfa->assignRole('super_admin');
|
||||
Sanctum::actingAs($adminNoMfa);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertJsonFragment(['message' => 'MFA must be enabled to impersonate users.']);
|
||||
}
|
||||
|
||||
public function test_start_denied_for_non_super_admin(): void
|
||||
{
|
||||
Sanctum::actingAs($this->targetUser);
|
||||
|
||||
$response = $this->postJson("/api/v1/admin/impersonate/{$this->targetUser->id}");
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
}
|
||||
@@ -67,58 +220,331 @@ class AdminImpersonationTest extends TestCase
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson("/api/v1/admin/impersonate/{$this->otherSuperAdmin->id}");
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->otherSuperAdmin->id}",
|
||||
$this->startPayload(),
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertJsonFragment(['message' => 'Cannot impersonate another super admin.']);
|
||||
}
|
||||
|
||||
// ─── Stop ────────────────────────────────────────────────
|
||||
|
||||
public function test_stop_deletes_impersonation_token(): void
|
||||
public function test_start_denied_when_nesting(): void
|
||||
{
|
||||
// Start impersonation
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
$startResponse = $this->postJson("/api/v1/admin/impersonate/{$this->targetUser->id}");
|
||||
$token = $startResponse->json('data.token');
|
||||
|
||||
// Reset auth state so the Bearer token takes effect
|
||||
$this->app['auth']->forgetGuards();
|
||||
// Start first session
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$response = $this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/admin/stop-impersonation');
|
||||
// Try to start another session
|
||||
$anotherUser = User::factory()->create();
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$anotherUser->id}",
|
||||
$this->startPayload(['mfa_code' => $this->validTotpCode()]),
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertJsonFragment(['message' => 'You already have an active impersonation session.']);
|
||||
}
|
||||
|
||||
public function test_start_denied_when_target_already_impersonated(): void
|
||||
{
|
||||
// First admin impersonates target
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
// Second admin tries to impersonate same target
|
||||
$admin2Secret = (new Google2FA)->generateSecretKey(32);
|
||||
$admin2 = User::factory()->create([
|
||||
'mfa_enabled' => true,
|
||||
'mfa_method' => MfaMethod::TOTP->value,
|
||||
'mfa_secret' => encrypt($admin2Secret),
|
||||
'mfa_confirmed_at' => now(),
|
||||
]);
|
||||
$admin2->assignRole('super_admin');
|
||||
Sanctum::actingAs($admin2);
|
||||
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
[
|
||||
'reason' => 'Also investigating',
|
||||
'mfa_code' => (new Google2FA)->getCurrentOtp($admin2Secret),
|
||||
'mfa_method' => 'totp',
|
||||
],
|
||||
);
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertJsonFragment(['message' => 'This user is already being impersonated.']);
|
||||
}
|
||||
|
||||
public function test_session_records_mfa_method(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('impersonation_sessions', [
|
||||
'admin_id' => $this->superAdmin->id,
|
||||
'mfa_method' => 'totp',
|
||||
]);
|
||||
}
|
||||
|
||||
// ─── Core: Stop ─────────────────────────────────────────────
|
||||
|
||||
public function test_stop_clears_cache_and_updates_db(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$response = $this->postJson('/api/v1/admin/stop-impersonation');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.user.id', $this->superAdmin->id);
|
||||
|
||||
$this->assertDatabaseMissing('personal_access_tokens', [
|
||||
'tokenable_id' => $this->targetUser->id,
|
||||
'name' => 'impersonation-by-' . $this->superAdmin->id,
|
||||
]);
|
||||
// DB session should be ended
|
||||
$session = ImpersonationSession::first();
|
||||
$this->assertNotNull($session->ended_at);
|
||||
$this->assertEquals('manual', $session->end_reason);
|
||||
|
||||
// Cache should be cleared
|
||||
$cacheKey = "impersonation:{$this->superAdmin->id}:{$this->targetUser->id}";
|
||||
$this->assertNull(Cache::get($cacheKey));
|
||||
}
|
||||
|
||||
// ─── Activity Log ────────────────────────────────────────
|
||||
|
||||
public function test_activity_log_records_start_and_stop(): void
|
||||
public function test_stop_without_session_returns_400(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
$startResponse = $this->postJson("/api/v1/admin/impersonate/{$this->targetUser->id}");
|
||||
$token = $startResponse->json('data.token');
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'event' => 'admin.impersonation.started',
|
||||
'causer_id' => $this->superAdmin->id,
|
||||
'subject_id' => $this->targetUser->id,
|
||||
]);
|
||||
$response = $this->postJson('/api/v1/admin/stop-impersonation');
|
||||
|
||||
// Reset auth state so the Bearer token takes effect
|
||||
$response->assertStatus(400);
|
||||
}
|
||||
|
||||
// ─── Middleware ──────────────────────────────────────────────
|
||||
|
||||
public function test_header_swaps_auth_context(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
// Start impersonation
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
// Make a request with the impersonation header
|
||||
$response = $this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->getJson('/api/v1/auth/me');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.id', $this->targetUser->id);
|
||||
}
|
||||
|
||||
public function test_no_header_returns_normal_auth(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->getJson('/api/v1/auth/me');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.id', $this->superAdmin->id);
|
||||
}
|
||||
|
||||
public function test_invalid_session_returns_403(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
// Send header without an active session
|
||||
$response = $this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->getJson('/api/v1/auth/me');
|
||||
|
||||
$response->assertForbidden();
|
||||
$response->assertJson(['impersonation_ended' => true]);
|
||||
}
|
||||
|
||||
public function test_sensitive_routes_blocked_during_impersonation(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$sensitiveRoutes = [
|
||||
['POST', '/api/v1/me/change-password'],
|
||||
['POST', '/api/v1/me/change-email'],
|
||||
['PUT', '/api/v1/me/profile'],
|
||||
['POST', '/api/v1/auth/mfa/setup/totp'],
|
||||
['GET', '/api/v1/auth/mfa/status'],
|
||||
['GET', '/api/v1/auth/trusted-devices'],
|
||||
];
|
||||
|
||||
foreach ($sensitiveRoutes as [$method, $url]) {
|
||||
$response = $this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->json($method, $url);
|
||||
|
||||
$this->assertEquals(403, $response->status(), "Route {$method} {$url} should be blocked");
|
||||
$this->assertEquals(
|
||||
'This action is not allowed during impersonation.',
|
||||
$response->json('message'),
|
||||
"Route {$method} {$url} should have correct message",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_ip_change_terminates_session(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
// Start impersonation from IP 127.0.0.1 (default in tests)
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
// Make request from different IP
|
||||
$response = $this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->withServerVariables(['REMOTE_ADDR' => '192.168.1.100'])
|
||||
->getJson('/api/v1/auth/me');
|
||||
|
||||
$response->assertForbidden();
|
||||
|
||||
// Session should be ended with ip_changed reason
|
||||
$session = ImpersonationSession::first();
|
||||
$this->assertNotNull($session->ended_at);
|
||||
$this->assertEquals('ip_changed', $session->end_reason);
|
||||
}
|
||||
|
||||
// ─── Audit ──────────────────────────────────────────────────
|
||||
|
||||
public function test_activity_log_includes_impersonated_by_during_session(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
// Check start activity
|
||||
$startActivity = Activity::where('event', 'admin.impersonation.started')->first();
|
||||
$this->assertNotNull($startActivity);
|
||||
$this->assertEquals($this->superAdmin->id, $startActivity->causer_id);
|
||||
$this->assertEquals($this->targetUser->id, $startActivity->subject_id);
|
||||
$this->assertArrayHasKey('session_id', $startActivity->properties->toArray());
|
||||
}
|
||||
|
||||
public function test_activity_log_records_stop(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$this->postJson('/api/v1/admin/stop-impersonation')->assertOk();
|
||||
|
||||
$stopActivity = Activity::where('event', 'admin.impersonation.stopped')->first();
|
||||
$this->assertNotNull($stopActivity);
|
||||
$this->assertArrayHasKey('end_reason', $stopActivity->properties->toArray());
|
||||
}
|
||||
|
||||
public function test_session_increments_actions_count(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
// Make a request with the impersonation header
|
||||
$this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->getJson('/api/v1/auth/me')
|
||||
->assertOk();
|
||||
|
||||
// Reset auth guards so Sanctum::actingAs takes effect again
|
||||
$this->app['auth']->forgetGuards();
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->withHeader('Authorization', "Bearer {$token}")
|
||||
->postJson('/api/v1/admin/stop-impersonation');
|
||||
$this->withHeader('X-Impersonate-User', $this->targetUser->id)
|
||||
->getJson('/api/v1/auth/me')
|
||||
->assertOk();
|
||||
|
||||
$this->assertDatabaseHas('activity_log', [
|
||||
'event' => 'admin.impersonation.stopped',
|
||||
'subject_id' => $this->targetUser->id,
|
||||
$session = ImpersonationSession::first();
|
||||
$this->assertEquals(2, $session->refresh()->actions_count);
|
||||
}
|
||||
|
||||
// ─── Lifecycle ──────────────────────────────────────────────
|
||||
|
||||
public function test_status_returns_active_session(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
)->assertOk();
|
||||
|
||||
$response = $this->getJson('/api/v1/admin/impersonate/status');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.active', true);
|
||||
$response->assertJsonStructure([
|
||||
'data' => [
|
||||
'active',
|
||||
'session' => ['id', 'target_user_id', 'expires_at'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_status_returns_false_when_not_impersonating(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->getJson('/api/v1/admin/impersonate/status');
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertJsonPath('data.active', false);
|
||||
}
|
||||
|
||||
public function test_send_mfa_code_sends_email(): void
|
||||
{
|
||||
Sanctum::actingAs($this->superAdmin);
|
||||
|
||||
$response = $this->postJson('/api/v1/admin/impersonate/send-mfa-code');
|
||||
|
||||
$response->assertOk();
|
||||
|
||||
// Verify email code was created
|
||||
$this->assertDatabaseHas('mfa_email_codes', [
|
||||
'user_id' => $this->superAdmin->id,
|
||||
'used' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_unauthenticated_request_returns_401(): void
|
||||
{
|
||||
$response = $this->postJson(
|
||||
"/api/v1/admin/impersonate/{$this->targetUser->id}",
|
||||
$this->startPayload(),
|
||||
);
|
||||
|
||||
$response->assertUnauthorized();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user