seed(RoleSeeder::class); // 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([ 'mfa_enabled' => true, 'mfa_method' => MfaMethod::TOTP->value, 'mfa_secret' => encrypt((new Google2FA)->generateSecretKey(32)), 'mfa_confirmed_at' => now(), ]); $this->otherSuperAdmin->assignRole('super_admin'); } private function validTotpCode(): string { return (new Google2FA)->getCurrentOtp($this->totpSecret); } 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}", $this->startPayload(), ); $response->assertOk(); $response->assertJsonStructure([ '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.session.admin_id', $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}", $this->startPayload(), ); $response->assertForbidden(); } public function test_start_denied_when_target_is_super_admin(): void { Sanctum::actingAs($this->superAdmin); $response = $this->postJson( "/api/v1/admin/impersonate/{$this->otherSuperAdmin->id}", $this->startPayload(), ); $response->assertForbidden(); $response->assertJsonFragment(['message' => 'Cannot impersonate another super admin.']); } public function test_start_denied_when_nesting(): void { Sanctum::actingAs($this->superAdmin); // Start first session $this->postJson( "/api/v1/admin/impersonate/{$this->targetUser->id}", $this->startPayload(), )->assertOk(); // 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); // 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)); } public function test_stop_without_session_returns_400(): void { Sanctum::actingAs($this->superAdmin); $response = $this->postJson('/api/v1/admin/stop-impersonation'); $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'], ['POST', '/api/v1/auth/refresh'], ]; 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_read_only_routes_allowed_during_impersonation(): void { Sanctum::actingAs($this->superAdmin); $this->postJson( "/api/v1/admin/impersonate/{$this->targetUser->id}", $this->startPayload(), )->assertOk(); // GET /auth/me should be allowed (profile viewing) $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_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('X-Impersonate-User', $this->targetUser->id) ->getJson('/api/v1/auth/me') ->assertOk(); $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(); } }