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]], ]); $scope = BaselineScope::fromJsonb([ 'policy_types' => [], 'foundation_types' => [], ])->expandDefaults(); expect($scope->policyTypes)->toBe([ 'deviceCompliancePolicy', 'deviceConfiguration', ]); expect($scope->foundationTypes)->toBe([]); expect($scope->allTypes())->toBe([ 'deviceCompliancePolicy', 'deviceConfiguration', ]); }); it('filters unknown types and does not allow foundations inside policy_types', function () { config()->set('tenantpilot.supported_policy_types', [ ['type' => 'deviceConfiguration'], ]); config()->set('tenantpilot.foundation_types', [ ['type' => 'assignmentFilter', 'baseline_compare' => ['supported' => true]], ]); $scope = BaselineScope::fromJsonb([ 'policy_types' => ['deviceConfiguration', 'assignmentFilter', 'unknown'], 'foundation_types' => ['assignmentFilter', 'unknown'], ])->expandDefaults(); expect($scope->policyTypes)->toBe(['deviceConfiguration']); expect($scope->foundationTypes)->toBe(['assignmentFilter']); expect($scope->allTypes())->toBe(['assignmentFilter', 'deviceConfiguration']); }); it('normalizes canonical v2 entries and preserves canonical storage', 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]], ]); $scope = BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceConfiguration', 'deviceCompliancePolicy', 'deviceConfiguration'], 'filters' => [], ], [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceCompliancePolicy'], 'filters' => [], ], [ 'domain_key' => 'platform_foundation', 'subject_class' => 'configuration_resource', 'subject_type_keys' => ['assignmentFilter'], 'filters' => [], ], ], ]); expect($scope->policyTypes)->toBe(['deviceCompliancePolicy', 'deviceConfiguration']) ->and($scope->foundationTypes)->toBe(['assignmentFilter']) ->and($scope->toStoredJsonb())->toBe([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceCompliancePolicy', 'deviceConfiguration'], 'filters' => [], ], [ 'domain_key' => 'platform_foundation', 'subject_class' => 'configuration_resource', 'subject_type_keys' => ['assignmentFilter'], 'filters' => [], ], ], ]) ->and($scope->normalizationLineage())->toMatchArray([ 'source_shape' => 'canonical_v2', 'normalized_on_read' => false, 'save_forward_required' => false, ]); }); it('treats a missing legacy bucket like its empty default when the other bucket is present', function (): void { config()->set('tenantpilot.supported_policy_types', [ ['type' => 'deviceConfiguration'], ['type' => 'deviceCompliancePolicy'], ]); config()->set('tenantpilot.foundation_types', [ ['type' => 'assignmentFilter', 'baseline_compare' => ['supported' => true]], ]); $policyOnly = BaselineScope::fromJsonb([ 'policy_types' => ['deviceConfiguration'], ]); $foundationOnly = BaselineScope::fromJsonb([ 'foundation_types' => ['assignmentFilter'], ]); expect($policyOnly->policyTypes)->toBe(['deviceConfiguration']) ->and($policyOnly->foundationTypes)->toBe([]) ->and($foundationOnly->policyTypes)->toBe(['deviceCompliancePolicy', 'deviceConfiguration']) ->and($foundationOnly->foundationTypes)->toBe(['assignmentFilter']); }); it('rejects mixed legacy and canonical payloads', function (): void { expect(fn () => BaselineScope::fromJsonb([ 'policy_types' => ['deviceConfiguration'], 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceConfiguration'], ], ], ]))->toThrow(InvalidArgumentException::class, 'must not mix legacy buckets'); }); it('rejects unsupported filters for current domains', function (): void { config()->set('tenantpilot.supported_policy_types', [ ['type' => 'deviceConfiguration'], ]); expect(fn () => BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceConfiguration'], 'filters' => ['tenant_ids' => ['tenant-a']], ], ], ]))->toThrow(InvalidArgumentException::class, 'Filters are not supported'); }); it('treats empty legacy override payloads as no override when requested', function (): void { $scope = BaselineScope::fromJsonb([ 'policy_types' => [], 'foundation_types' => [], ], allowEmptyLegacyAsNoOverride: true); expect($scope->isEmpty())->toBeTrue(); }); it('rejects unknown governance domains', function (): void { expect(fn () => BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'unknown_domain', 'subject_class' => 'policy', 'subject_type_keys' => ['deviceConfiguration'], 'filters' => [], ], ], ]))->toThrow(InvalidArgumentException::class, 'Unknown governance domain'); }); it('rejects invalid subject classes for known domains', function (): void { expect(fn () => BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'intune', 'subject_class' => 'configuration_resource', 'subject_type_keys' => ['deviceConfiguration'], 'filters' => [], ], ], ]))->toThrow(InvalidArgumentException::class, 'is not valid for domain'); }); it('rejects inactive subject types in canonical scope entries', function (): void { expect(fn () => BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'platform_foundation', 'subject_class' => 'configuration_resource', 'subject_type_keys' => ['intuneRoleAssignment'], 'filters' => [], ], ], ]))->toThrow(InvalidArgumentException::class, 'Inactive subject type'); }); it('rejects future-domain selections that have no active subject type mapping yet', function (): void { expect(fn () => BaselineScope::fromJsonb([ 'version' => 2, 'entries' => [ [ 'domain_key' => 'entra', 'subject_class' => 'control', 'subject_type_keys' => ['conditionalAccessPolicy'], 'filters' => [], ], ], ]))->toThrow(InvalidArgumentException::class, 'Unknown subject type'); });