## Summary - introduce the governance subject taxonomy registry and canonical Baseline Scope V2 normalization and persistence - update baseline profile Filament surfaces, validation, capture/compare gating, and add the optional scope backfill command with audit logging - add focused unit, feature, Filament, and browser smoke coverage for save-forward behavior, operation truth, authorization continuity, and invalid-scope rendering - remove the duplicate legacy spec plan under `specs/001-governance-subject-taxonomy/plan.md` ## Verification - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec202GovernanceSubjectTaxonomySmokeTest.php` - focused Spec 202 regression pack: `56 passed (300 assertions)` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - no schema migration required - no new Filament asset registration required - branch includes the final browser smoke test coverage for the current feature Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #232
227 lines
9.1 KiB
PHP
227 lines
9.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Filament\Resources\BaselineProfileResource;
|
|
use App\Filament\Resources\BaselineProfileResource\Pages\CreateBaselineProfile;
|
|
use App\Filament\Resources\BaselineProfileResource\Pages\EditBaselineProfile;
|
|
use App\Filament\Resources\BaselineProfileResource\Pages\ViewBaselineProfile;
|
|
use App\Models\BaselineProfile;
|
|
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use Illuminate\Validation\ValidationException;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Livewire\Livewire;
|
|
|
|
it('persists canonical v2 scope when creating a baseline profile through the current selectors', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]],
|
|
['type' => 'intuneRoleAssignment', 'label' => 'Intune RBAC Role Assignment', 'baseline_compare' => ['supported' => false]],
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(CreateBaselineProfile::class)
|
|
->fillForm([
|
|
'name' => 'Canonical baseline profile',
|
|
'scope_jsonb.policy_types' => ['deviceConfiguration'],
|
|
'scope_jsonb.foundation_types' => ['assignmentFilter'],
|
|
])
|
|
->call('create')
|
|
->assertHasNoFormErrors()
|
|
->assertNotified();
|
|
|
|
$profile = BaselineProfile::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('name', 'Canonical baseline profile')
|
|
->sole();
|
|
|
|
expect($profile->scope_jsonb)->toBe([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => ['assignmentFilter'],
|
|
]);
|
|
|
|
expect($profile->canonicalScopeJsonb())->toBe([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'platform_foundation',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['assignmentFilter'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
]);
|
|
});
|
|
|
|
it('normalizes legacy scope on read and saves it forward as canonical v2 on edit', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
$profileId = BaselineProfile::query()->insertGetId([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Legacy baseline profile',
|
|
'description' => null,
|
|
'version_label' => null,
|
|
'status' => 'active',
|
|
'capture_mode' => 'opportunistic',
|
|
'scope_jsonb' => json_encode([
|
|
'policy_types' => [],
|
|
'foundation_types' => ['assignmentFilter'],
|
|
], JSON_THROW_ON_ERROR),
|
|
'active_snapshot_id' => null,
|
|
'created_by_user_id' => (int) $user->getKey(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$profile = BaselineProfile::query()->findOrFail($profileId);
|
|
|
|
expect($profile->normalizedScope()->normalizationLineage())->toMatchArray([
|
|
'source_shape' => 'legacy',
|
|
'normalized_on_read' => true,
|
|
'save_forward_required' => true,
|
|
'legacy_keys_present' => ['policy_types', 'foundation_types'],
|
|
])->and($profile->scope_jsonb)->toBe([
|
|
'policy_types' => [],
|
|
'foundation_types' => ['assignmentFilter'],
|
|
]);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(EditBaselineProfile::class, ['record' => $profileId])
|
|
->fillForm([
|
|
'description' => 'Updated after normalization',
|
|
])
|
|
->call('save')
|
|
->assertHasNoFormErrors()
|
|
->assertNotified();
|
|
|
|
$profile->refresh();
|
|
$registry = app(GovernanceSubjectTaxonomyRegistry::class);
|
|
|
|
expect($profile->canonicalScopeJsonb())->toBe([
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => $registry->activeLegacyBucketKeys('policy_types'),
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'platform_foundation',
|
|
'subject_class' => 'configuration_resource',
|
|
'subject_type_keys' => ['assignmentFilter'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
])->and($profile->normalizedScope()->normalizationLineage())->toMatchArray([
|
|
'source_shape' => 'canonical_v2',
|
|
'normalized_on_read' => false,
|
|
'save_forward_required' => false,
|
|
]);
|
|
});
|
|
|
|
it('summarizes governed subjects, readiness, and save-forward feedback for current selector payloads', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
config()->set('tenantpilot.foundation_types', [
|
|
['type' => 'assignmentFilter', 'label' => 'Assignment Filter', 'baseline_compare' => ['supported' => true]],
|
|
]);
|
|
|
|
$payload = [
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => ['assignmentFilter'],
|
|
];
|
|
|
|
expect(BaselineProfileResource::scopeSummaryText($payload))
|
|
->toBe('Intune policies: Device Configuration; Platform foundation configuration resources: Assignment Filter')
|
|
->and(BaselineProfileResource::scopeSupportReadinessText($payload))
|
|
->toBe('Capture: ready. Compare: ready.')
|
|
->and(BaselineProfileResource::scopeSelectionFeedbackText($payload))
|
|
->toBe('This Intune-first selection will be saved forward as canonical governed-subject scope V2.');
|
|
});
|
|
|
|
it('shows normalization lineage on the baseline profile detail surface before a legacy row is saved forward', function (): void {
|
|
config()->set('tenantpilot.supported_policy_types', [
|
|
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
|
|
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
|
|
]);
|
|
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
$profileId = BaselineProfile::query()->insertGetId([
|
|
'workspace_id' => (int) $tenant->workspace_id,
|
|
'name' => 'Legacy lineage profile',
|
|
'description' => null,
|
|
'version_label' => null,
|
|
'status' => 'active',
|
|
'capture_mode' => 'opportunistic',
|
|
'scope_jsonb' => json_encode([
|
|
'policy_types' => ['deviceConfiguration'],
|
|
'foundation_types' => [],
|
|
], JSON_THROW_ON_ERROR),
|
|
'active_snapshot_id' => null,
|
|
'created_by_user_id' => (int) $user->getKey(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
Livewire::actingAs($user)
|
|
->test(ViewBaselineProfile::class, ['record' => $profileId])
|
|
->assertSee('Governed subject summary')
|
|
->assertSee('Intune policies: Device Configuration')
|
|
->assertSee('Legacy Intune buckets are being normalized and will be saved forward as canonical V2 on the next successful save.');
|
|
});
|
|
|
|
it('rejects unsupported canonical filters when creating a baseline profile', function (): void {
|
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
|
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
|
|
|
$component = Livewire::actingAs($user)
|
|
->test(CreateBaselineProfile::class);
|
|
|
|
$page = $component->instance();
|
|
$method = new \ReflectionMethod($page, 'mutateFormDataBeforeCreate');
|
|
$method->setAccessible(true);
|
|
|
|
expect(fn () => $method->invoke($page, [
|
|
'name' => 'Invalid filtered baseline',
|
|
'scope_jsonb' => [
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => ['tenant_ids' => ['tenant-a']],
|
|
],
|
|
],
|
|
],
|
|
]))->toThrow(ValidationException::class, 'Filters are not supported');
|
|
|
|
expect(BaselineProfile::query()->where('name', 'Invalid filtered baseline')->exists())->toBeFalse();
|
|
}); |