rtrim( strtr(base64_encode(json_encode($data, JSON_UNESCAPED_SLASHES) ?: ''), '+/', '-_'), '=' ); return $encode(['alg' => 'none', 'typ' => 'JWT']).'.'.$encode($claims).'.'; } } function entra_configure_services(): void { config()->set('services.microsoft.client_id', 'test-client'); config()->set('services.microsoft.client_secret', 'test-secret'); config()->set('services.microsoft.redirect', 'http://localhost/auth/entra/callback'); config()->set('services.microsoft.tenant', 'organizations'); } function entra_fake_token_exchange(string $tid, string $oid): void { Http::fake([ 'https://login.microsoftonline.com/*/oauth2/v2.0/token' => Http::response([ 'id_token' => entra_build_jwt([ 'tid' => $tid, 'oid' => $oid, 'preferred_username' => 'user@example.com', 'name' => 'Test User', ]), ]), ]); } it('routes to no-access when user has no tenant memberships', function () { entra_configure_services(); entra_fake_token_exchange('tenant-1', 'object-1'); User::factory()->create([ 'entra_tenant_id' => 'tenant-1', 'entra_object_id' => 'object-1', ]); $state = 'state-123'; $response = $this ->withSession(['entra_state' => $state]) ->get(route('auth.entra.callback', ['code' => 'code-123', 'state' => $state])); $response->assertRedirect('/admin/no-access'); }); it('routes to tenant dashboard when user has exactly one tenant membership', function () { entra_configure_services(); entra_fake_token_exchange('tenant-1', 'object-1'); $user = User::factory()->create([ 'entra_tenant_id' => 'tenant-1', 'entra_object_id' => 'object-1', ]); $tenant = Tenant::factory()->create(['status' => 'active']); TenantMembership::query()->create([ 'tenant_id' => $tenant->getKey(), 'user_id' => $user->getKey(), 'role' => 'owner', 'source' => 'manual', 'source_ref' => null, 'created_by_user_id' => null, ]); $state = 'state-123'; $response = $this ->withSession(['entra_state' => $state]) ->get(route('auth.entra.callback', ['code' => 'code-123', 'state' => $state])); $response->assertRedirect(TenantDashboard::getUrl(tenant: $tenant)); }); it('routes to choose-tenant when user has multiple tenant memberships', function () { entra_configure_services(); entra_fake_token_exchange('tenant-1', 'object-1'); $user = User::factory()->create([ 'entra_tenant_id' => 'tenant-1', 'entra_object_id' => 'object-1', ]); $tenants = Tenant::factory()->count(2)->create(['status' => 'active']); foreach ($tenants as $tenant) { TenantMembership::query()->create([ 'tenant_id' => $tenant->getKey(), 'user_id' => $user->getKey(), 'role' => 'owner', 'source' => 'manual', 'source_ref' => null, 'created_by_user_id' => null, ]); } $state = 'state-123'; $response = $this ->withSession(['entra_state' => $state]) ->get(route('auth.entra.callback', ['code' => 'code-123', 'state' => $state])); $response->assertRedirect('/admin/choose-tenant'); });