TenantAtlas/tests/Unit/Support/Diff/RbacRoleDefinitionDiffBuilderTest.php
ahmido 3f6f80f7af feat: refine onboarding draft flow and RBAC diff UX (#171)
## Summary
- add the RBAC role definition diff UX upgrade as the first concrete consumer of the shared diff presentation foundation
- refine managed tenant onboarding draft routing, CTA labeling, and cancellation redirect behavior
- tighten related Filament and diff rendering regression coverage

## Testing
- updated focused Pest coverage for onboarding draft routing and lifecycle behavior
- updated focused Pest coverage for shared diff partials and RBAC finding rendering

## Notes
- Livewire v4.0+ compliance is preserved within the existing Filament v5 surfaces
- provider registration remains unchanged in bootstrap/providers.php
- no new Filament assets were added; existing deployment practice still relies on php artisan filament:assets when assets change

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #171
2026-03-14 20:09:54 +00:00

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;
}