213 lines
8.4 KiB
PHP
213 lines
8.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Support\Diff\DiffRowStatus;
|
|
use App\Support\Diff\RbacRoleDefinitionDiffBuilder;
|
|
|
|
it('builds RBAC rows in deterministic review order with matching summary counts', function (): void {
|
|
$presentation = app(RbacRoleDefinitionDiffBuilder::class)->build(rbacBuilderEvidenceFixture());
|
|
|
|
expect(array_map(
|
|
static fn ($row): string => $row->key,
|
|
$presentation->rows,
|
|
))->toBe([
|
|
'Role definition > Display name',
|
|
'Role definition > Description',
|
|
'Role definition > Role source',
|
|
'Role definition > Permission blocks',
|
|
'Role definition > Scope tag IDs',
|
|
'Permission block 1 > Allowed actions',
|
|
'Permission block 1 > Denied actions',
|
|
'Permission block 1 > Conditions',
|
|
]);
|
|
|
|
$rows = collect($presentation->rows)->keyBy('key');
|
|
|
|
expect($presentation->summary->changedCount)->toBe(2)
|
|
->and($presentation->summary->addedCount)->toBe(1)
|
|
->and($presentation->summary->removedCount)->toBe(1)
|
|
->and($presentation->summary->unchangedCount)->toBe(4)
|
|
->and($presentation->summary->message)->toBeNull()
|
|
->and($rows->get('Role definition > Description')?->status)->toBe(DiffRowStatus::Changed)
|
|
->and($rows->get('Permission block 1 > Allowed actions')?->status)->toBe(DiffRowStatus::Changed)
|
|
->and($rows->get('Permission block 1 > Denied actions')?->status)->toBe(DiffRowStatus::Removed)
|
|
->and($rows->get('Permission block 1 > Conditions')?->status)->toBe(DiffRowStatus::Added)
|
|
->and($rows->get('Permission block 1 > Allowed actions')?->isListLike)->toBeTrue()
|
|
->and($rows->get('Permission block 1 > Allowed actions')?->addedItems)->toBe([
|
|
'Microsoft.Intune/deviceConfigurations/create',
|
|
])
|
|
->and($rows->get('Permission block 1 > Allowed actions')?->removedItems)->toBe([
|
|
'Microsoft.Intune/deviceConfigurations/delete',
|
|
])
|
|
->and($rows->get('Permission block 1 > Allowed actions')?->unchangedItems)->toBe([
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
]);
|
|
});
|
|
|
|
it('preserves null boolean scalar and empty-list values for shared formatting', function (): void {
|
|
$presentation = app(RbacRoleDefinitionDiffBuilder::class)->build(rbacBuilderEvidenceFixture([
|
|
'changed_keys' => [
|
|
'Role definition > Description',
|
|
'Role definition > Preview enabled',
|
|
'Role definition > Scope tag IDs',
|
|
],
|
|
'baseline' => [
|
|
'normalized' => [
|
|
'Role definition > Description' => null,
|
|
'Role definition > Preview enabled' => false,
|
|
'Role definition > Scope tag IDs' => [],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 0,
|
|
],
|
|
'current' => [
|
|
'normalized' => [
|
|
'Role definition > Description' => 'Updated description',
|
|
'Role definition > Preview enabled' => true,
|
|
'Role definition > Scope tag IDs' => ['scope-1'],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 0,
|
|
],
|
|
]));
|
|
|
|
$rows = collect($presentation->rows)->keyBy('key');
|
|
|
|
expect($rows->get('Role definition > Description')?->oldValue)->toBeNull()
|
|
->and($rows->get('Role definition > Description')?->newValue)->toBe('Updated description')
|
|
->and($rows->get('Role definition > Preview enabled')?->oldValue)->toBeFalse()
|
|
->and($rows->get('Role definition > Preview enabled')?->newValue)->toBeTrue()
|
|
->and($rows->get('Role definition > Scope tag IDs')?->oldValue)->toBe([])
|
|
->and($rows->get('Role definition > Scope tag IDs')?->newValue)->toBe(['scope-1'])
|
|
->and($rows->get('Role definition > Scope tag IDs')?->isListLike)->toBeTrue()
|
|
->and($rows->get('Role definition > Scope tag IDs')?->addedItems)->toBe(['scope-1'])
|
|
->and($rows->get('Role definition > Scope tag IDs')?->removedItems)->toBe([])
|
|
->and($rows->get('Role definition > Role source')?->newValue)->toBe('Custom')
|
|
->and($rows->get('Role definition > Permission blocks')?->newValue)->toBe(0);
|
|
});
|
|
|
|
it('derives identical fallback rows into a no-change summary when normalized metadata is sparse', function (): void {
|
|
$presentation = app(RbacRoleDefinitionDiffBuilder::class)->build([
|
|
'changed_keys' => [],
|
|
'baseline' => [
|
|
'normalized' => [],
|
|
'is_built_in' => true,
|
|
'role_permission_count' => 1,
|
|
],
|
|
'current' => [
|
|
'normalized' => [],
|
|
'is_built_in' => true,
|
|
'role_permission_count' => 1,
|
|
],
|
|
]);
|
|
|
|
expect(array_map(
|
|
static fn ($row): string => $row->key,
|
|
$presentation->rows,
|
|
))->toBe([
|
|
'Role definition > Role source',
|
|
'Role definition > Permission blocks',
|
|
])
|
|
->and($presentation->summary->changedCount)->toBe(0)
|
|
->and($presentation->summary->addedCount)->toBe(0)
|
|
->and($presentation->summary->removedCount)->toBe(0)
|
|
->and($presentation->summary->unchangedCount)->toBe(2)
|
|
->and($presentation->summary->message)->toBe('No changes detected.');
|
|
});
|
|
|
|
it('returns a no-data presentation for empty or invalid RBAC payloads', function (): void {
|
|
$presentation = app(RbacRoleDefinitionDiffBuilder::class)->build([
|
|
'changed_keys' => ['Ghost key'],
|
|
'baseline' => ['normalized' => ['' => 'ignored']],
|
|
'current' => ['normalized' => [' ' => 'ignored']],
|
|
]);
|
|
|
|
expect($presentation->rows)->toBe([])
|
|
->and($presentation->summary->hasRows)->toBeFalse()
|
|
->and($presentation->summary->changedCount)->toBe(0)
|
|
->and($presentation->summary->addedCount)->toBe(0)
|
|
->and($presentation->summary->removedCount)->toBe(0)
|
|
->and($presentation->summary->unchangedCount)->toBe(0)
|
|
->and($presentation->summary->message)->toBe('No diff data available.');
|
|
});
|
|
|
|
/**
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function rbacBuilderEvidenceFixture(array $overrides = []): array
|
|
{
|
|
return rbacBuilderFixtureMerge([
|
|
'changed_keys' => [
|
|
'Role definition > Description',
|
|
'Permission block 1 > Allowed actions',
|
|
],
|
|
'baseline' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Baseline description',
|
|
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
|
|
'Permission block 1 > Allowed actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/delete',
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
],
|
|
'Permission block 1 > Denied actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/wipe',
|
|
],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
'current' => [
|
|
'normalized' => [
|
|
'Role definition > Display name' => 'Security Reader',
|
|
'Role definition > Description' => 'Updated description',
|
|
'Role definition > Scope tag IDs' => ['0', 'scope-1'],
|
|
'Permission block 1 > Allowed actions' => [
|
|
'Microsoft.Intune/deviceConfigurations/create',
|
|
'Microsoft.Intune/deviceConfigurations/read',
|
|
],
|
|
'Permission block 1 > Conditions' => [
|
|
'@Resource[Microsoft.Intune/deviceConfigurations] Exists',
|
|
],
|
|
],
|
|
'is_built_in' => false,
|
|
'role_permission_count' => 1,
|
|
],
|
|
], $overrides);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $base
|
|
* @param array<string, mixed> $overrides
|
|
* @return array<string, mixed>
|
|
*/
|
|
function rbacBuilderFixtureMerge(array $base, array $overrides): array
|
|
{
|
|
foreach ($overrides as $key => $value) {
|
|
if ($key === 'normalized') {
|
|
$base[$key] = $value;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (
|
|
is_string($key)
|
|
&& array_key_exists($key, $base)
|
|
&& is_array($value)
|
|
&& is_array($base[$key])
|
|
&& ! array_is_list($value)
|
|
&& ! array_is_list($base[$key])
|
|
) {
|
|
$base[$key] = rbacBuilderFixtureMerge($base[$key], $value);
|
|
|
|
continue;
|
|
}
|
|
|
|
$base[$key] = $value;
|
|
}
|
|
|
|
return $base;
|
|
}
|