TenantAtlas/tests/Unit/EntraAdminRolesReportServiceTest.php
ahmido 32c3a64147 feat(112): LIST $expand parity + Entra principal names (#136)
Implements LIST `$expand` parity with GET by forwarding caller-provided, contract-allowlisted expands.

Key changes:
- Entra Admin Roles scan now requests `expand=principal` for role assignments so `principal.displayName` can render.
- `$expand` normalization/sanitization: top-level comma split (commas inside balanced parentheses preserved), trim, dedupe, allowlist exact match, caps (max 10 tokens, max 200 chars/token).
- Diagnostics when expands are removed/truncated (non-prod warning, production low-noise).

Tests:
- Adds/extends unit coverage for Graph contract sanitization, list request shaping, and the EntraAdminRolesReportService.

Spec artifacts included under `specs/112-list-expand-parity/`.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #136
2026-02-25 23:54:20 +00:00

119 lines
4.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\Tenant;
use App\Services\EntraAdminRoles\EntraAdminRolesReportService;
use App\Services\Graph\GraphClientInterface;
use App\Services\Graph\GraphResponse;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
it('requests principal expansion and uses principal display name when present', function () {
$tenant = Tenant::factory()->create();
ensureDefaultProviderConnection($tenant, 'microsoft');
$roleDefinitions = [
[
'id' => 'role-def-1',
'templateId' => '62e90394-69f5-4237-9190-012177145e10',
'displayName' => 'Global Administrator',
'isBuiltIn' => true,
],
];
$roleAssignments = [
[
'id' => 'role-assign-1',
'roleDefinitionId' => 'role-def-1',
'principalId' => 'principal-1',
'directoryScopeId' => '/',
'principal' => [
'@odata.type' => '#microsoft.graph.user',
'displayName' => 'Ada Lovelace',
],
],
];
$calls = [];
$this->mock(GraphClientInterface::class, function ($mock) use (&$calls, $roleDefinitions, $roleAssignments) {
$mock->shouldReceive('listPolicies')
->twice()
->andReturnUsing(function (string $policyType, array $options) use (&$calls, $roleDefinitions, $roleAssignments): GraphResponse {
$calls[] = [$policyType, $options];
if ($policyType === 'entraRoleDefinitions') {
return new GraphResponse(true, $roleDefinitions);
}
return new GraphResponse(true, $roleAssignments);
});
});
$result = app(EntraAdminRolesReportService::class)->generate($tenant);
expect($calls)->toHaveCount(2);
expect($calls[0][0])->toBe('entraRoleDefinitions');
expect($calls[0][1])->not->toHaveKey('expand');
expect($calls[1][0])->toBe('entraRoleAssignments');
expect($calls[1][1]['expand'] ?? null)->toBe('principal');
expect($calls[1][1])->toMatchArray(array_merge($calls[0][1], [
'expand' => 'principal',
]));
expect($result->payload['high_privilege'])->toHaveCount(1);
expect($result->payload['high_privilege'][0]['principal_display_name'])->toBe('Ada Lovelace');
});
it('falls back to Unknown when principal details are missing upstream', function () {
$tenant = Tenant::factory()->create();
ensureDefaultProviderConnection($tenant, 'microsoft');
$roleDefinitions = [
[
'id' => 'role-def-1',
'templateId' => '62e90394-69f5-4237-9190-012177145e10',
'displayName' => 'Global Administrator',
'isBuiltIn' => true,
],
];
$roleAssignments = [
[
'id' => 'role-assign-1',
'roleDefinitionId' => 'role-def-1',
'principalId' => 'principal-1',
'directoryScopeId' => '/',
],
];
$calls = [];
$this->mock(GraphClientInterface::class, function ($mock) use (&$calls, $roleDefinitions, $roleAssignments) {
$mock->shouldReceive('listPolicies')
->twice()
->andReturnUsing(function (string $policyType, array $options) use (&$calls, $roleDefinitions, $roleAssignments): GraphResponse {
$calls[] = [$policyType, $options];
if ($policyType === 'entraRoleDefinitions') {
return new GraphResponse(true, $roleDefinitions);
}
return new GraphResponse(true, $roleAssignments);
});
});
$result = app(EntraAdminRolesReportService::class)->generate($tenant);
expect($calls)->toHaveCount(2);
expect($calls[0][0])->toBe('entraRoleDefinitions');
expect($calls[0][1])->not->toHaveKey('expand');
expect($calls[1][0])->toBe('entraRoleAssignments');
expect($calls[1][1]['expand'] ?? null)->toBe('principal');
expect($result->payload['high_privilege'])->toHaveCount(1);
expect($result->payload['high_privilege'][0]['principal_display_name'])->toBe('Unknown');
});