TenantAtlas/apps/platform/tests/Feature/Baselines/BaselineScopeBackfillCommandTest.php

178 lines
6.0 KiB
PHP

<?php
declare(strict_types=1);
use App\Models\AuditLog;
use App\Models\BaselineProfile;
use App\Models\Workspace;
use Illuminate\Support\Facades\DB;
function spec202ForceLegacyBaselineScope(BaselineProfile $profile, array $scope): void
{
DB::table('baseline_profiles')
->where('id', (int) $profile->getKey())
->update([
'scope_jsonb' => json_encode($scope, JSON_THROW_ON_ERROR),
'updated_at' => now(),
]);
}
it('previews only legacy baseline profile scope rows in mixed datasets without mutating them', 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]],
]);
$workspace = Workspace::factory()->create();
$legacyProfile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Legacy preview profile',
]);
$canonicalProfile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Canonical preview profile',
'scope_jsonb' => [
'version' => 2,
'entries' => [
[
'domain_key' => 'intune',
'subject_class' => 'policy',
'subject_type_keys' => ['deviceCompliancePolicy'],
'filters' => [],
],
],
],
]);
spec202ForceLegacyBaselineScope($legacyProfile, [
'policy_types' => ['deviceConfiguration'],
'foundation_types' => [],
]);
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
'--workspace' => (string) $workspace->getKey(),
])
->expectsOutputToContain('Mode: preview')
->expectsOutputToContain('Scope surface: baseline_profiles_only')
->expectsOutputToContain('Candidate count: 1')
->expectsOutputToContain('Rewritten count: 0')
->expectsOutputToContain('Audit logged: no')
->expectsOutputToContain('Legacy preview profile')
->assertSuccessful();
$legacyProfile->refresh();
$canonicalProfile->refresh();
expect($legacyProfile->rawScopeJsonb())->toBe([
'policy_types' => ['deviceConfiguration'],
'foundation_types' => [],
])->and($canonicalProfile->rawScopeJsonb())->toBe([
'version' => 2,
'entries' => [
[
'domain_key' => 'intune',
'subject_class' => 'policy',
'subject_type_keys' => ['deviceCompliancePolicy'],
'filters' => [],
],
],
]);
});
it('requires explicit write confirmation before mutating baseline profile scope rows', function (): void {
$workspace = Workspace::factory()->create();
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Legacy confirm profile',
]);
spec202ForceLegacyBaselineScope($profile, [
'policy_types' => ['deviceConfiguration'],
'foundation_types' => [],
]);
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
'--workspace' => (string) $workspace->getKey(),
'--write' => true,
])
->expectsOutputToContain('Explicit write confirmation required.')
->assertFailed();
$profile->refresh();
expect($profile->rawScopeJsonb())->toBe([
'policy_types' => ['deviceConfiguration'],
'foundation_types' => [],
]);
});
it('rewrites legacy baseline profile scopes to canonical v2, logs audits, and stays idempotent on rerun', function (): void {
config()->set('tenantpilot.supported_policy_types', [
['type' => 'deviceConfiguration', 'label' => 'Device Configuration'],
['type' => 'deviceCompliancePolicy', 'label' => 'Device Compliance'],
]);
$workspace = Workspace::factory()->create();
$profile = BaselineProfile::factory()->active()->create([
'workspace_id' => (int) $workspace->getKey(),
'name' => 'Legacy commit profile',
]);
spec202ForceLegacyBaselineScope($profile, [
'policy_types' => ['deviceConfiguration'],
'foundation_types' => [],
]);
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
'--workspace' => (string) $workspace->getKey(),
'--write' => true,
'--confirm-write' => true,
])
->expectsOutputToContain('Mode: commit')
->expectsOutputToContain('Candidate count: 1')
->expectsOutputToContain('Rewritten count: 1')
->expectsOutputToContain('Audit logged: yes')
->assertSuccessful();
$profile->refresh();
expect($profile->rawScopeJsonb())->toBe([
'version' => 2,
'entries' => [
[
'domain_key' => 'intune',
'subject_class' => 'policy',
'subject_type_keys' => ['deviceConfiguration'],
'filters' => [],
],
],
])->and($profile->requiresScopeSaveForward())->toBeFalse();
$this->assertDatabaseHas('audit_logs', [
'workspace_id' => (int) $workspace->getKey(),
'action' => 'baseline_profile.scope_backfilled',
'resource_type' => 'baseline_profile',
'resource_id' => (string) $profile->getKey(),
]);
$auditLog = AuditLog::query()
->where('workspace_id', (int) $workspace->getKey())
->where('action', 'baseline_profile.scope_backfilled')
->latest('id')
->first();
expect($auditLog)->not->toBeNull();
$this->artisan('tenantpilot:baseline-scope-v2:backfill', [
'--workspace' => (string) $workspace->getKey(),
])
->expectsOutputToContain('Candidate count: 0')
->expectsOutputToContain('No baseline profile scope rows require backfill.')
->assertSuccessful();
});