active()->create([ 'workspace_id' => $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration']], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => $tenant->workspace_id, 'baseline_profile_id' => $profile->getKey(), ]); $profile->update(['active_snapshot_id' => $snapshot->getKey()]); // Baseline has policyA and policyB BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'policy-a-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'content-a'), 'meta_jsonb' => ['display_name' => 'Policy A'], ]); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'policy-b-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'content-b'), 'meta_jsonb' => ['display_name' => 'Policy B'], ]); // Tenant has policyA (different content) and policyC (unexpected) InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, 'external_id' => 'policy-a-uuid', 'policy_type' => 'deviceConfiguration', 'meta_jsonb' => ['different_content' => true], 'display_name' => 'Policy A modified', ]); InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, 'external_id' => 'policy-c-uuid', 'policy_type' => 'deviceConfiguration', 'meta_jsonb' => ['new_policy' => true], 'display_name' => 'Policy C unexpected', ]); $opService = app(OperationRunService::class); $run = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job = new CompareBaselineToTenantJob($run); $job->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $run->refresh(); expect($run->status)->toBe('completed'); expect($run->outcome)->toBe('succeeded'); $scopeKey = 'baseline_profile:' . $profile->getKey(); $findings = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->get(); // policyB missing (high), policyA different (medium), policyC unexpected (low) = 3 findings expect($findings->count())->toBe(3); $severities = $findings->pluck('severity')->sort()->values()->all(); expect($severities)->toContain(Finding::SEVERITY_HIGH); expect($severities)->toContain(Finding::SEVERITY_MEDIUM); expect($severities)->toContain(Finding::SEVERITY_LOW); }); it('produces idempotent fingerprints so re-running compare updates existing findings', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration']], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => $tenant->workspace_id, 'baseline_profile_id' => $profile->getKey(), ]); $profile->update(['active_snapshot_id' => $snapshot->getKey()]); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'policy-x-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'baseline-content'), 'meta_jsonb' => ['display_name' => 'Policy X'], ]); // Tenant does NOT have policy-x → missing_policy finding $opService = app(OperationRunService::class); // First run $run1 = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job1 = new CompareBaselineToTenantJob($run1); $job1->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $scopeKey = 'baseline_profile:' . $profile->getKey(); $countAfterFirst = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->count(); expect($countAfterFirst)->toBe(1); // Second run - new OperationRun so we can dispatch again // Mark first run as completed so ensureRunWithIdentity creates a new one $run1->update(['status' => 'completed', 'completed_at' => now()]); $run2 = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job2 = new CompareBaselineToTenantJob($run2); $job2->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $countAfterSecond = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('source', 'baseline.compare') ->where('scope_key', $scopeKey) ->count(); // Same fingerprint → same finding updated, not duplicated expect($countAfterSecond)->toBe(1); }); it('creates zero findings when baseline matches tenant inventory exactly', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration']], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => $tenant->workspace_id, 'baseline_profile_id' => $profile->getKey(), ]); $profile->update(['active_snapshot_id' => $snapshot->getKey()]); // Baseline item $metaContent = ['policy_key' => 'value123']; $driftHasher = app(DriftHasher::class); $contentHash = $driftHasher->hashNormalized($metaContent); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'matching-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => $contentHash, 'meta_jsonb' => ['display_name' => 'Matching Policy'], ]); // Tenant inventory with same content → same hash InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, 'external_id' => 'matching-uuid', 'policy_type' => 'deviceConfiguration', 'meta_jsonb' => $metaContent, 'display_name' => 'Matching Policy', ]); $opService = app(OperationRunService::class); $run = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job = new CompareBaselineToTenantJob($run); $job->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $run->refresh(); expect($run->status)->toBe('completed'); expect($run->outcome)->toBe('succeeded'); $counts = is_array($run->summary_counts) ? $run->summary_counts : []; expect((int) ($counts['total'] ?? -1))->toBe(0); $findings = Finding::query() ->where('tenant_id', $tenant->getKey()) ->where('source', 'baseline.compare') ->count(); expect($findings)->toBe(0); }); // --- T042: Summary counts severity breakdown tests --- it('writes severity breakdown in summary_counts', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration']], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => $tenant->workspace_id, 'baseline_profile_id' => $profile->getKey(), ]); $profile->update(['active_snapshot_id' => $snapshot->getKey()]); // 2 baseline items: one will be missing (high), one will be different (medium) BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'missing-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'missing-content'), 'meta_jsonb' => ['display_name' => 'Missing Policy'], ]); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'changed-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'original-content'), 'meta_jsonb' => ['display_name' => 'Changed Policy'], ]); // Tenant only has changed-uuid with different content + extra-uuid (unexpected) InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, 'external_id' => 'changed-uuid', 'policy_type' => 'deviceConfiguration', 'meta_jsonb' => ['modified_content' => true], 'display_name' => 'Changed Policy', ]); InventoryItem::factory()->create([ 'tenant_id' => $tenant->getKey(), 'workspace_id' => $tenant->workspace_id, 'external_id' => 'extra-uuid', 'policy_type' => 'deviceConfiguration', 'meta_jsonb' => ['extra_content' => true], 'display_name' => 'Extra Policy', ]); $opService = app(OperationRunService::class); $run = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job = new CompareBaselineToTenantJob($run); $job->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $run->refresh(); $counts = is_array($run->summary_counts) ? $run->summary_counts : []; expect((int) ($counts['total'] ?? -1))->toBe(3); expect((int) ($counts['high'] ?? -1))->toBe(1); // missing-uuid expect((int) ($counts['medium'] ?? -1))->toBe(1); // changed-uuid expect((int) ($counts['low'] ?? -1))->toBe(1); // extra-uuid }); it('writes result context with findings breakdown', function () { [$user, $tenant] = createUserWithTenant(role: 'owner'); $profile = BaselineProfile::factory()->active()->create([ 'workspace_id' => $tenant->workspace_id, 'scope_jsonb' => ['policy_types' => ['deviceConfiguration']], ]); $snapshot = BaselineSnapshot::factory()->create([ 'workspace_id' => $tenant->workspace_id, 'baseline_profile_id' => $profile->getKey(), ]); $profile->update(['active_snapshot_id' => $snapshot->getKey()]); // One missing policy BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => $snapshot->getKey(), 'subject_type' => 'policy', 'subject_external_id' => 'gone-uuid', 'policy_type' => 'deviceConfiguration', 'baseline_hash' => hash('sha256', 'gone-content'), ]); $opService = app(OperationRunService::class); $run = $opService->ensureRunWithIdentity( tenant: $tenant, type: 'baseline_compare', identityInputs: ['baseline_profile_id' => (int) $profile->getKey()], context: [ 'baseline_profile_id' => (int) $profile->getKey(), 'baseline_snapshot_id' => (int) $snapshot->getKey(), 'effective_scope' => ['policy_types' => ['deviceConfiguration']], ], initiator: $user, ); $job = new CompareBaselineToTenantJob($run); $job->handle( app(DriftHasher::class), app(BaselineSnapshotIdentity::class), app(AuditLogger::class), $opService, ); $run->refresh(); $context = is_array($run->context) ? $run->context : []; $result = $context['result'] ?? []; expect($result)->toHaveKey('findings_total'); expect($result)->toHaveKey('findings_upserted'); expect($result)->toHaveKey('severity_breakdown'); expect((int) $result['findings_total'])->toBe(1); expect((int) $result['findings_upserted'])->toBe(1); });