TenantAtlas/apps/platform/tests/Feature/TenantConfiguration/Spec419M365RegistryExpansionTest.php
ahmido 5252398063 feat: expand m365 tcm workload registry (#486)
Automated PR provided by Codex via Gitea API.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #486
2026-06-26 22:36:24 +00:00

259 lines
11 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\TenantConfigurationResource;
use App\Models\TenantConfigurationResourceEvidence;
use App\Models\TenantConfigurationResourceType;
use App\Models\TenantConfigurationSupportedScope;
use App\Services\TenantConfiguration\ResourceTypeRegistry;
use App\Services\TenantConfiguration\SupportedScopeResolver;
use App\Support\TenantConfiguration\ClaimState;
use App\Support\TenantConfiguration\CoverageLevel;
use App\Support\TenantConfiguration\EvidenceState;
use App\Support\TenantConfiguration\IdentityState;
use App\Support\TenantConfiguration\RestoreTier;
use App\Support\TenantConfiguration\SupportState;
use App\Support\TenantConfiguration\Workload;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
it('Spec419 persists representative M365 registry rows without runtime tenant data', function (): void {
$expectedByWorkload = [
Workload::Intune->value => 8,
Workload::Entra->value => 6,
Workload::Exchange->value => 6,
Workload::Teams->value => 6,
Workload::SecurityCompliance->value => 6,
];
$counts = TenantConfigurationResourceType::query()
->selectRaw('workload, count(*) as aggregate')
->groupBy('workload')
->pluck('aggregate', 'workload')
->map(fn (int|string $count): int => (int) $count);
foreach ($expectedByWorkload as $workload => $expectedCount) {
expect($counts[$workload] ?? null)->toBe($expectedCount);
}
expect(TenantConfigurationResourceType::query()->count())->toBe(32)
->and(TenantConfigurationResourceType::query()->whereIn('workload', [
Workload::Defender->value,
Workload::Purview->value,
Workload::Tenantpilot->value,
Workload::Unknown->value,
])->exists())->toBeFalse()
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
$m365Rows = TenantConfigurationResourceType::query()
->where('workload', '!=', Workload::Intune->value)
->orderBy('canonical_type')
->get();
foreach ($m365Rows as $row) {
expect($row->support_state)->toBe(SupportState::OutOfScope)
->and($row->default_coverage_level)->toBe(CoverageLevel::Detected)
->and($row->default_evidence_state)->toBe(EvidenceState::NotCaptured)
->and($row->default_identity_state)->toBe(IdentityState::Derived)
->and($row->default_claim_state)->toBe(ClaimState::InternalOnly)
->and($row->allows_beta_claims)->toBeFalse()
->and($row->allows_graph_fallback_claims)->toBeFalse()
->and($row->allows_certified_claims)->toBeFalse()
->and($row->metadata['registry_only'])->toBeTrue()
->and($row->metadata['is_full_catalog'])->toBeFalse()
->and($row->metadata['customer_claims_allowed'])->toBeFalse();
expect([
RestoreTier::NotRestorable,
RestoreTier::PreviewOnly,
])->toContain($row->restore_tier);
}
});
it('Spec419 persists inactive M365 planning scopes and leaves active Intune scopes unchanged', function (): void {
$requiredScopeKeys = [
'm365_tcm_registry_detected',
'entra_tcm_registry_detected',
'exchange_tcm_registry_detected',
'teams_tcm_registry_detected',
'security_compliance_tcm_registry_detected',
'm365_tcm_generic_future',
'm365_tcm_certified_none',
];
$scopes = TenantConfigurationSupportedScope::query()
->whereIn('scope_key', $requiredScopeKeys)
->get()
->keyBy('scope_key');
expect($scopes->keys()->sort()->values()->all())->toBe(collect($requiredScopeKeys)->sort()->values()->all());
foreach ($scopes as $scope) {
expect($scope->minimum_coverage_level)->toBe(CoverageLevel::Detected)
->and($scope->customer_claims_allowed)->toBeFalse()
->and($scope->is_active)->toBeFalse()
->and($scope->metadata['registry_only'])->toBeTrue()
->and($scope->metadata['is_full_catalog'])->toBeFalse();
}
expect(TenantConfigurationSupportedScope::query()->active()->orderBy('scope_key')->pluck('scope_key')->all())
->toBe([
'intune_tcm_core',
'intune_tcm_core_with_graph_fallback',
])
->and(TenantConfigurationSupportedScope::query()
->whereIn('scope_key', [
'm365_full_coverage',
'm365_certified',
'all_microsoft_365_supported',
'full_tenant_coverage',
'full_m365_restore_ready',
])
->exists())->toBeFalse();
$aggregate = $scopes['m365_tcm_registry_detected'];
expect($aggregate->included_resource_types)->toHaveCount(24)
->and($aggregate->included_resource_types)->toContain('conditionalAccessPolicy')
->toContain('transportRule')
->toContain('meetingPolicy')
->toContain('dlpCompliancePolicy');
});
it('Spec419 keeps registry sync idempotent and non-capturing', function (): void {
(new ResourceTypeRegistry)->syncDefaults();
(new ResourceTypeRegistry)->syncDefaults();
(new SupportedScopeResolver)->syncDefaults();
(new SupportedScopeResolver)->syncDefaults();
expect(TenantConfigurationResourceType::query()->count())->toBe(32)
->and(TenantConfigurationSupportedScope::query()->count())->toBe(9)
->and(TenantConfigurationResource::query()->count())->toBe(0)
->and(TenantConfigurationResourceEvidence::query()->count())->toBe(0);
});
it('Spec419 rollback deletes only exact Spec419 rows and preserves later non-Intune promotions', function (): void {
$futureResourceType = TenantConfigurationResourceType::query()
->where('canonical_type', 'conditionalAccessPolicy')
->where('source_class', 'tcm')
->firstOrFail();
$futureResourceType->update([
'metadata' => [
...$futureResourceType->metadata,
'catalog_import_batch' => 'future_entra_promotion',
'registry_only' => false,
'future_spec' => 'preserve_non_intune_after_spec_419',
],
]);
$futureScope = TenantConfigurationSupportedScope::query()
->where('scope_key', 'm365_tcm_registry_detected')
->firstOrFail();
$futureScope->update([
'metadata' => [
...$futureScope->metadata,
'catalog_import_batch' => 'future_m365_scope_promotion',
'future_spec' => 'preserve_non_intune_after_spec_419',
],
]);
$migration = require database_path('migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php');
$migration->down();
expect(TenantConfigurationResourceType::query()
->where('canonical_type', 'conditionalAccessPolicy')
->where('source_class', 'tcm')
->exists())->toBeTrue()
->and(TenantConfigurationResourceType::query()
->where('canonical_type', 'securityDefaults')
->where('source_class', 'tcm')
->exists())->toBeFalse()
->and(TenantConfigurationSupportedScope::query()
->where('scope_key', 'm365_tcm_registry_detected')
->exists())->toBeTrue()
->and(TenantConfigurationSupportedScope::query()
->where('scope_key', 'entra_tcm_registry_detected')
->exists())->toBeFalse();
if (DB::getDriverName() === 'pgsql') {
$definition = DB::scalar(<<<'SQL'
SELECT pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'tenant_configuration_resource_types'::regclass
AND conname = 'tenant_config_resource_types_workload_check'
SQL);
expect((string) $definition)->toContain('intune')
->toContain('entra');
}
});
it('Spec419 does not add tenant ownership columns to registry definition tables', function (): void {
foreach (['tenant_configuration_resource_types', 'tenant_configuration_supported_scopes'] as $table) {
expect(Schema::getColumnListing($table))
->not->toContain('tenant_id')
->not->toContain('provider_tenant_id')
->not->toContain('entra_tenant_id')
->not->toContain('workspace_id')
->not->toContain('managed_environment_id')
->not->toContain('provider_connection_id');
}
});
it('Spec419 stays out of capture clients, v1 adapters, and workload mini-platforms', function (): void {
$files = [
app_path('Services/TenantConfiguration/ResourceTypeRegistry.php'),
app_path('Services/TenantConfiguration/SupportedScopeResolver.php'),
app_path('Services/TenantConfiguration/ClaimGuard.php'),
app_path('Support/TenantConfiguration/Workload.php'),
database_path('migrations/2026_06_26_000419_expand_tenant_configuration_workloads.php'),
];
$content = collect($files)
->map(fn (string $file): string => file_get_contents($file) ?: '')
->implode("\n");
expect($content)
->not->toContain('GraphClientInterface')
->not->toContain('ProviderGateway')
->not->toContain('StartTenantConfigurationCapture')
->not->toContain('CaptureTenantConfigurationEvidenceJob')
->not->toContain('TenantConfigurationResource::query()->create')
->not->toContain('TenantConfigurationResourceEvidence::query()->create')
->not->toContain('Http::')
->not->toContain('tenant_id')
->not->toContain('ProviderV1')
->not->toContain('LegacyAdapter')
->not->toContain('dual_write')
->not->toContain('namespace App\\Services\\TenantConfiguration\\Entra')
->not->toContain('namespace App\\Services\\TenantConfiguration\\Exchange')
->not->toContain('namespace App\\Services\\TenantConfiguration\\Teams')
->not->toContain('namespace App\\Services\\TenantConfiguration\\Defender')
->not->toContain('namespace App\\Services\\TenantConfiguration\\Purview')
->not->toContain('namespace App\\Services\\TenantConfiguration\\SecurityCompliance')
->not->toContain('create_entra_')
->not->toContain('create_exchange_')
->not->toContain('create_teams_')
->not->toContain('create_defender_')
->not->toContain('create_purview_')
->not->toContain('create_security_compliance_');
});
it('Spec419 updates the PostgreSQL workload check constraint', function (): void {
if (DB::getDriverName() !== 'pgsql') {
$this->markTestSkipped('PostgreSQL-specific workload check proof runs in the pgsql lane.');
}
$definition = DB::scalar(<<<'SQL'
SELECT pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'tenant_configuration_resource_types'::regclass
AND conname = 'tenant_config_resource_types_workload_check'
SQL);
foreach (Workload::values() as $workload) {
expect((string) $definition)->toContain($workload);
}
});