## Summary - extract baseline compare orchestration behind an explicit strategy contract and registry - preserve the current Intune compare path through a dedicated `IntuneCompareStrategy` - harden compare launch and review surfaces for mixed, unsupported, incomplete, and strategy-failure truth - add Spec 203 artifacts, focused regression coverage, and future-domain strategy proof tests ## Testing - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Unit/Baselines/CompareStrategyRegistryTest.php tests/Unit/Baselines/CompareSubjectResultContractTest.php tests/Feature/Baselines/BaselineCompareStrategySelectionTest.php tests/Feature/Baselines/BaselineComparePreconditionsTest.php tests/Feature/Baselines/BaselineCompareExecutionGuardTest.php tests/Feature/Baselines/BaselineCompareMatrixCompareAllActionTest.php tests/Feature/Filament/BaselineProfileCompareStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingStartSurfaceTest.php tests/Feature/Filament/BaselineCompareLandingWhyNoFindingsTest.php tests/Feature/Filament/BaselineCompareMatrixPageTest.php tests/Feature/Filament/OperationRunBaselineTruthSurfaceTest.php` - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` ## Notes - no new Filament panel/provider registration changes - no global-search resource changes - no new asset registration or deployment step changes Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #233
183 lines
7.2 KiB
PHP
183 lines
7.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
require_once __DIR__.'/Support/FakeCompareStrategy.php';
|
|
|
|
use App\Filament\Pages\BaselineCompareMatrix;
|
|
use App\Jobs\CompareBaselineToTenantJob;
|
|
use App\Models\OperationRun;
|
|
use App\Models\Tenant;
|
|
use App\Services\Baselines\BaselineCompareService;
|
|
use App\Support\Baselines\BaselineReasonCodes;
|
|
use App\Support\Baselines\Compare\CompareStrategyRegistry;
|
|
use App\Support\Baselines\Compare\IntuneCompareStrategy;
|
|
use App\Support\Governance\GovernanceSubjectTaxonomyRegistry;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Livewire\Livewire;
|
|
use Tests\Feature\Baselines\Support\FakeCompareStrategy;
|
|
use Tests\Feature\Baselines\Support\FakeGovernanceSubjectTaxonomyRegistry;
|
|
use Tests\Feature\Concerns\BuildsBaselineCompareMatrixFixtures;
|
|
|
|
uses(RefreshDatabase::class, BuildsBaselineCompareMatrixFixtures::class);
|
|
|
|
it('fans out compare starts across the visible assigned set without creating a workspace umbrella run', function (): void {
|
|
Queue::fake();
|
|
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$readonlyTenant = Tenant::factory()->create([
|
|
'workspace_id' => (int) $fixture['workspace']->getKey(),
|
|
'name' => 'Readonly Contoso',
|
|
]);
|
|
|
|
$fixture['user']->tenants()->syncWithoutDetaching([
|
|
(int) $readonlyTenant->getKey() => ['role' => 'readonly'],
|
|
]);
|
|
|
|
$this->assignTenantToBaselineProfile($fixture['profile'], $readonlyTenant);
|
|
|
|
$service = app(BaselineCompareService::class);
|
|
|
|
$existingRunResult = $service->startCompareForProfile(
|
|
$fixture['profile'],
|
|
$fixture['visibleTenantTwo'],
|
|
$fixture['user'],
|
|
);
|
|
|
|
expect($existingRunResult['ok'] ?? false)->toBeTrue();
|
|
|
|
$result = $service->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']);
|
|
|
|
expect($result['visibleAssignedTenantCount'])->toBe(3)
|
|
->and($result['queuedCount'])->toBe(1)
|
|
->and($result['alreadyQueuedCount'])->toBe(1)
|
|
->and($result['blockedCount'])->toBe(1);
|
|
|
|
$launchStates = collect($result['targets'])
|
|
->mapWithKeys(static fn (array $target): array => [(int) $target['tenantId'] => (string) $target['launchState']])
|
|
->all();
|
|
|
|
expect($launchStates[(int) $fixture['visibleTenant']->getKey()] ?? null)->toBe('queued')
|
|
->and($launchStates[(int) $fixture['visibleTenantTwo']->getKey()] ?? null)->toBe('already_queued')
|
|
->and($launchStates[(int) $readonlyTenant->getKey()] ?? null)->toBe('blocked');
|
|
|
|
Queue::assertPushed(CompareBaselineToTenantJob::class);
|
|
|
|
$activeRuns = OperationRun::query()
|
|
->where('workspace_id', (int) $fixture['workspace']->getKey())
|
|
->where('type', 'baseline_compare')
|
|
->get();
|
|
|
|
expect($activeRuns)->toHaveCount(2)
|
|
->and($activeRuns->every(static fn (OperationRun $run): bool => $run->tenant_id !== null))->toBeTrue()
|
|
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->status === OperationRunStatus::Queued->value))->toBeTrue()
|
|
->and($activeRuns->every(static fn (OperationRun $run): bool => (string) $run->outcome === OperationRunOutcome::Pending->value))->toBeTrue()
|
|
->and(OperationRun::query()->whereNull('tenant_id')->where('type', 'baseline_compare')->count())->toBe(0);
|
|
});
|
|
|
|
it('runs compare assigned tenants from the matrix page and keeps feedback on tenant-owned runs', function (): void {
|
|
Queue::fake();
|
|
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
$this->setAdminWorkspaceContext($fixture['user'], $fixture['workspace']);
|
|
|
|
Livewire::actingAs($fixture['user'])
|
|
->test(BaselineCompareMatrix::class, ['record' => $fixture['profile']->getKey()])
|
|
->assertActionVisible('compareAssignedTenants')
|
|
->assertActionEnabled('compareAssignedTenants')
|
|
->callAction('compareAssignedTenants')
|
|
->assertStatus(200);
|
|
|
|
Queue::assertPushed(CompareBaselineToTenantJob::class, 2);
|
|
|
|
expect(OperationRun::query()
|
|
->where('workspace_id', (int) $fixture['workspace']->getKey())
|
|
->where('type', 'baseline_compare')
|
|
->whereNull('tenant_id')
|
|
->count())->toBe(0);
|
|
});
|
|
|
|
it('blocks visible assignment fanout when the baseline scope spans multiple compare strategy families', function (): void {
|
|
Queue::fake();
|
|
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
app()->instance(GovernanceSubjectTaxonomyRegistry::class, new FakeGovernanceSubjectTaxonomyRegistry);
|
|
app()->instance(CompareStrategyRegistry::class, new CompareStrategyRegistry([
|
|
app(IntuneCompareStrategy::class),
|
|
app(FakeCompareStrategy::class),
|
|
]));
|
|
|
|
$fixture['profile']->update([
|
|
'scope_jsonb' => [
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'intune',
|
|
'subject_class' => 'policy',
|
|
'subject_type_keys' => ['deviceConfiguration'],
|
|
'filters' => [],
|
|
],
|
|
[
|
|
'domain_key' => 'entra',
|
|
'subject_class' => 'control',
|
|
'subject_type_keys' => ['conditionalAccessPolicy'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$result = app(BaselineCompareService::class)->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']);
|
|
|
|
expect($result['visibleAssignedTenantCount'])->toBe(2)
|
|
->and($result['queuedCount'])->toBe(0)
|
|
->and($result['alreadyQueuedCount'])->toBe(0)
|
|
->and($result['blockedCount'])->toBe(2)
|
|
->and(collect($result['targets'])->pluck('reasonCode')->unique()->values()->all())
|
|
->toBe([BaselineReasonCodes::COMPARE_MIXED_SCOPE]);
|
|
|
|
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
|
|
});
|
|
|
|
it('blocks visible assignment fanout when the baseline scope has no compatible compare strategy family', function (): void {
|
|
Queue::fake();
|
|
|
|
$fixture = $this->makeBaselineCompareMatrixFixture();
|
|
|
|
app()->instance(GovernanceSubjectTaxonomyRegistry::class, new FakeGovernanceSubjectTaxonomyRegistry);
|
|
app()->instance(CompareStrategyRegistry::class, new CompareStrategyRegistry([
|
|
app(IntuneCompareStrategy::class),
|
|
]));
|
|
|
|
$fixture['profile']->update([
|
|
'scope_jsonb' => [
|
|
'version' => 2,
|
|
'entries' => [
|
|
[
|
|
'domain_key' => 'entra',
|
|
'subject_class' => 'control',
|
|
'subject_type_keys' => ['conditionalAccessPolicy'],
|
|
'filters' => [],
|
|
],
|
|
],
|
|
],
|
|
]);
|
|
|
|
$result = app(BaselineCompareService::class)->startCompareForVisibleAssignments($fixture['profile'], $fixture['user']);
|
|
|
|
expect($result['visibleAssignedTenantCount'])->toBe(2)
|
|
->and($result['queuedCount'])->toBe(0)
|
|
->and($result['alreadyQueuedCount'])->toBe(0)
|
|
->and($result['blockedCount'])->toBe(2)
|
|
->and(collect($result['targets'])->pluck('reasonCode')->unique()->values()->all())
|
|
->toBe([BaselineReasonCodes::COMPARE_UNSUPPORTED_SCOPE]);
|
|
|
|
Queue::assertNotPushed(CompareBaselineToTenantJob::class);
|
|
});
|