Files
crewli/api/tests/Feature/Auth/TrustedDeviceControllerTest.php
bert.hausmans 948687f27e feat: enterprise MFA with TOTP, email codes, backup codes, and trusted devices
Three verification methods (TOTP authenticator, email code, backup codes),
trusted device management with 30-day expiry, role-based enforcement for
super_admin and org_admin, admin reset capability, and full test coverage
(46 tests). Modifies login flow to support MFA challenge/response with
temporary session tokens stored in cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 20:45:55 +02:00

94 lines
3.1 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Feature\Auth;
use App\Models\TrustedDevice;
use App\Models\User;
use App\Services\MfaService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class TrustedDeviceControllerTest extends TestCase
{
use RefreshDatabase;
public function test_index_returns_active_devices(): void
{
$user = User::factory()->create();
$mfaService = app(MfaService::class);
$mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Chrome on macOS');
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Firefox on Windows');
$response = $this->actingAs($user)->getJson('/api/v1/auth/trusted-devices');
$response->assertOk();
$this->assertCount(2, $response->json('data'));
}
public function test_index_excludes_expired_devices(): void
{
$user = User::factory()->create();
$mfaService = app(MfaService::class);
$device = $mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Old Device');
$device->update(['trusted_until' => now()->subDay()]);
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Active Device');
$response = $this->actingAs($user)->getJson('/api/v1/auth/trusted-devices');
$response->assertOk();
$this->assertCount(1, $response->json('data'));
}
public function test_destroy_revokes_device(): void
{
$user = User::factory()->create();
$device = app(MfaService::class)->trustDevice($user, 'fp-1', '192.168.1.1', 'Chrome');
$response = $this->actingAs($user)->deleteJson("/api/v1/auth/trusted-devices/{$device->id}");
$response->assertNoContent();
$this->assertDatabaseMissing('trusted_devices', ['id' => $device->id]);
}
public function test_destroy_all_revokes_all_devices(): void
{
$user = User::factory()->create();
$mfaService = app(MfaService::class);
$mfaService->trustDevice($user, 'fp-1', '192.168.1.1', 'Device 1');
$mfaService->trustDevice($user, 'fp-2', '192.168.1.2', 'Device 2');
$response = $this->actingAs($user)->deleteJson('/api/v1/auth/trusted-devices');
$response->assertNoContent();
$this->assertDatabaseCount('trusted_devices', 0);
}
public function test_cannot_manage_other_users_devices(): void
{
$user = User::factory()->create();
$otherUser = User::factory()->create();
$device = app(MfaService::class)->trustDevice($otherUser, 'fp-1', '192.168.1.1', 'Other Device');
// Try to delete other user's device — should succeed silently (no record found for this user)
$response = $this->actingAs($user)->deleteJson("/api/v1/auth/trusted-devices/{$device->id}");
$response->assertNoContent();
// Other user's device should still exist
$this->assertDatabaseHas('trusted_devices', ['id' => $device->id]);
}
public function test_unauthenticated_cannot_access_devices(): void
{
$this->getJson('/api/v1/auth/trusted-devices')
->assertUnauthorized();
}
}