From df72c4adb74201d67b5e0bf5f4cb266c008b80c1 Mon Sep 17 00:00:00 2001 From: Ahmed Darrazi Date: Fri, 13 Mar 2026 02:20:09 +0100 Subject: [PATCH] fix: stabilize full suite regressions --- app/Jobs/CompareBaselineToTenantJob.php | 3 +- .../Intune/IntuneRoleDefinitionNormalizer.php | 12 ++++- .../Intune/PolicyCaptureOrchestrator.php | 35 ++++++++++--- .../Intune/SecretClassificationService.php | 2 +- .../CanonicalAdminTenantFilterState.php | 7 ++- .../verification-report-viewer.blade.php | 2 +- .../078/VerificationReportTenantlessTest.php | 1 + tests/Feature/AdminConsentCallbackTest.php | 2 +- tests/Feature/Auth/SystemPanelAuthTest.php | 4 +- .../BackupWithAssignmentsConsistencyTest.php | 3 ++ .../CaptureBaselineContentTest.php | 14 ++---- .../CompareContentEvidenceTest.php | 24 ++++----- .../CompareFidelityMismatchTest.php | 14 ++---- .../FindingFidelityTest.php | 14 ++---- .../FindingProvenanceTest.php | 14 ++---- .../BaselineDriftEngine/ResolverTest.php | 18 ++----- .../BaselineCompareCrossTenantMatchTest.php | 14 ++---- ...selineCompareDriftEvidenceContractTest.php | 50 +++++++------------ ...lineCompareWhyNoFindingsReasonCodeTest.php | 14 ++---- .../CanonicalAdminTenantFilterStateTest.php | 20 ++++++++ tests/Feature/Filament/InventoryPagesTest.php | 4 +- tests/Feature/OperationRunServiceTest.php | 29 ++++++----- ...iderConnectionAuthorizationSpec081Test.php | 4 +- .../RequiredPermissionsAccessTest.php | 4 +- .../RequiredPermissionsSidebarTest.php | 6 +-- ...rkspaceManagedTenantAdminMigrationTest.php | 8 +-- .../Feature/System/Spec114/AccessLogsTest.php | 6 +-- tests/Pest.php | 32 ++++++++++++ .../Intune/VersionServiceConcurrencyTest.php | 2 +- tests/Unit/OpsUx/RunFailureSanitizerTest.php | 11 ++++ .../OpsUx/SummaryCountsNormalizerTest.php | 2 +- 31 files changed, 214 insertions(+), 161 deletions(-) diff --git a/app/Jobs/CompareBaselineToTenantJob.php b/app/Jobs/CompareBaselineToTenantJob.php index 3001915..4df48e8 100644 --- a/app/Jobs/CompareBaselineToTenantJob.php +++ b/app/Jobs/CompareBaselineToTenantJob.php @@ -1674,9 +1674,8 @@ private function resolveRoleDefinitionVersion(Tenant $tenant, ?int $policyVersio private function fallbackRoleDefinitionNormalized(?PolicyVersion $version, array $meta): array { if ($version instanceof PolicyVersion) { - return app(IntuneRoleDefinitionNormalizer::class)->flattenForDiff( + return app(IntuneRoleDefinitionNormalizer::class)->buildEvidenceMap( is_array($version->snapshot) ? $version->snapshot : [], - 'intuneRoleDefinition', is_string($version->platform ?? null) ? (string) $version->platform : null, ); } diff --git a/app/Services/Intune/IntuneRoleDefinitionNormalizer.php b/app/Services/Intune/IntuneRoleDefinitionNormalizer.php index f0ac65c..4664909 100644 --- a/app/Services/Intune/IntuneRoleDefinitionNormalizer.php +++ b/app/Services/Intune/IntuneRoleDefinitionNormalizer.php @@ -108,6 +108,14 @@ public function flattenForDiff(?array $snapshot, string $policyType, ?string $pl ); } + /** + * @return array + */ + public function buildEvidenceMap(?array $snapshot, ?string $platform = null): array + { + return $this->flattenForDiff($snapshot, 'intuneRoleDefinition', $platform); + } + /** * @return array{ * baseline: array, @@ -121,8 +129,8 @@ public function flattenForDiff(?array $snapshot, string $policyType, ?string $pl */ public function classifyDiff(?array $baselineSnapshot, ?array $currentSnapshot, ?string $platform = null): array { - $baseline = $this->flattenForDiff($baselineSnapshot, 'intuneRoleDefinition', $platform); - $current = $this->flattenForDiff($currentSnapshot, 'intuneRoleDefinition', $platform); + $baseline = $this->buildEvidenceMap($baselineSnapshot, $platform); + $current = $this->buildEvidenceMap($currentSnapshot, $platform); $keys = array_values(array_unique(array_merge(array_keys($baseline), array_keys($current)))); sort($keys, SORT_STRING); diff --git a/app/Services/Intune/PolicyCaptureOrchestrator.php b/app/Services/Intune/PolicyCaptureOrchestrator.php index 1de6595..213555c 100644 --- a/app/Services/Intune/PolicyCaptureOrchestrator.php +++ b/app/Services/Intune/PolicyCaptureOrchestrator.php @@ -153,12 +153,12 @@ public function capture( fn ($query) => $query->where('baseline_profile_id', $baselineProfileId), ) ->get() - ->first(function ($version) use ($snapshotHash) { - return $this->snapshotContractHash( - snapshot: is_array($version->snapshot) ? $version->snapshot : [], - snapshotFingerprints: $this->fingerprintBucket($version, 'snapshot'), - redactionVersion: is_numeric($version->redaction_version) ? (int) $version->redaction_version : null, - ) === $snapshotHash; + ->first(function (PolicyVersion $version) use ($protectedSnapshot, $snapshotHash) { + return $this->matchesExistingVersionSnapshot( + version: $version, + snapshot: $protectedSnapshot->snapshot, + snapshotHash: $snapshotHash, + ); }); if ($existingVersion) { @@ -511,6 +511,29 @@ private function fingerprintBucket(PolicyVersion $version, string $bucket): arra return is_array($bucketFingerprints) ? $bucketFingerprints : []; } + private function matchesExistingVersionSnapshot(PolicyVersion $version, array $snapshot, string $snapshotHash): bool + { + $currentHash = $this->snapshotContractHash( + snapshot: is_array($version->snapshot) ? $version->snapshot : [], + snapshotFingerprints: $this->fingerprintBucket($version, 'snapshot'), + redactionVersion: is_numeric($version->redaction_version) ? (int) $version->redaction_version : null, + ); + + if ($currentHash === $snapshotHash) { + return true; + } + + $hasLegacySnapshotContract = $this->fingerprintBucket($version, 'snapshot') === [] + && ! is_numeric($version->redaction_version); + + if (! $hasLegacySnapshotContract) { + return false; + } + + return $this->normalizeHashValue(is_array($version->snapshot) ? $version->snapshot : []) + === $this->normalizeHashValue($snapshot); + } + private function normalizeHashValue(mixed $value): mixed { if (! is_array($value)) { diff --git a/app/Services/Intune/SecretClassificationService.php b/app/Services/Intune/SecretClassificationService.php index 9e4e98a..88d7fd0 100644 --- a/app/Services/Intune/SecretClassificationService.php +++ b/app/Services/Intune/SecretClassificationService.php @@ -76,7 +76,7 @@ public function sanitizeAuditString(string $value): string public function sanitizeOpsFailureString(string $value): string { $sanitized = $this->sanitizeMessageLikeString($value, '[REDACTED_SECRET]'); - $sanitized = preg_replace('/[A-Z0-9._%+\-]+@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[REDACTED_EMAIL]', $sanitized) ?? $sanitized; + $sanitized = preg_replace('/(?:\[[A-Z_]+\]|[A-Z0-9._%+\-]+)@[A-Z0-9.\-]+\.[A-Z]{2,}/i', '[REDACTED_EMAIL]', $sanitized) ?? $sanitized; return $sanitized; } diff --git a/app/Support/Filament/CanonicalAdminTenantFilterState.php b/app/Support/Filament/CanonicalAdminTenantFilterState.php index 49ef1dc..ae5670a 100644 --- a/app/Support/Filament/CanonicalAdminTenantFilterState.php +++ b/app/Support/Filament/CanonicalAdminTenantFilterState.php @@ -58,7 +58,12 @@ public function sync( } } - $session->put($filtersSessionKey, $persistedFilters); + if ($persistedFilters === []) { + $session->forget($filtersSessionKey); + } else { + $session->put($filtersSessionKey, $persistedFilters); + } + $session->put($stateKey, $resolvedTenantId); } diff --git a/resources/views/filament/components/verification-report-viewer.blade.php b/resources/views/filament/components/verification-report-viewer.blade.php index adfb730..a16da85 100644 --- a/resources/views/filament/components/verification-report-viewer.blade.php +++ b/resources/views/filament/components/verification-report-viewer.blade.php @@ -1,5 +1,5 @@ @php - $report = isset($getState) ? $getState() : ($report ?? null); + $report = $report ?? null; $report = is_array($report) ? $report : null; $run = $run ?? null; diff --git a/tests/Feature/078/VerificationReportTenantlessTest.php b/tests/Feature/078/VerificationReportTenantlessTest.php index 725ae0b..ab2c712 100644 --- a/tests/Feature/078/VerificationReportTenantlessTest.php +++ b/tests/Feature/078/VerificationReportTenantlessTest.php @@ -56,6 +56,7 @@ public function test_renders_verification_report_on_canonical_detail_without_fil ->get(route('admin.operations.view', ['run' => (int) $run->getKey()])) ->assertOk() ->assertSee('Verification report') + ->assertDontSee('Verification report unavailable') ->assertSee('Open previous verification') ->assertSee('/admin/operations/'.((int) $previousRun->getKey()), false) ->assertSee('Token acquisition works'); diff --git a/tests/Feature/AdminConsentCallbackTest.php b/tests/Feature/AdminConsentCallbackTest.php index d663bfe..f6e9736 100644 --- a/tests/Feature/AdminConsentCallbackTest.php +++ b/tests/Feature/AdminConsentCallbackTest.php @@ -112,6 +112,6 @@ $this->assertDatabaseHas('audit_logs', [ 'tenant_id' => $tenant->id, 'action' => 'tenant.consent.callback', - 'status' => 'error', + 'status' => 'failed', ]); }); diff --git a/tests/Feature/Auth/SystemPanelAuthTest.php b/tests/Feature/Auth/SystemPanelAuthTest.php index 711188c..e93c2ac 100644 --- a/tests/Feature/Auth/SystemPanelAuthTest.php +++ b/tests/Feature/Auth/SystemPanelAuthTest.php @@ -81,7 +81,7 @@ ->first(); expect($audit)->not->toBeNull(); - expect($audit->status)->toBe('failure'); + expect($audit->status)->toBe('failed'); expect($audit->metadata['attempted_email'] ?? null)->toBe($user->email); expect($audit->metadata['reason'] ?? null)->toBe('invalid_credentials'); }); @@ -115,7 +115,7 @@ ->first(); expect($audit)->not->toBeNull(); - expect($audit->status)->toBe('failure'); + expect($audit->status)->toBe('failed'); expect($audit->metadata['attempted_email'] ?? null)->toBe($user->email); expect($audit->metadata['reason'] ?? null)->toBe('inactive'); }); diff --git a/tests/Feature/BackupWithAssignmentsConsistencyTest.php b/tests/Feature/BackupWithAssignmentsConsistencyTest.php index e0b4278..b497ccf 100644 --- a/tests/Feature/BackupWithAssignmentsConsistencyTest.php +++ b/tests/Feature/BackupWithAssignmentsConsistencyTest.php @@ -8,6 +8,7 @@ use App\Services\Graph\GroupResolver; use App\Services\Intune\BackupService; use App\Services\Intune\PolicySnapshotService; +use App\Support\Baselines\PolicyVersionCapturePurpose; use Illuminate\Foundation\Testing\RefreshDatabase; use Mockery\MockInterface; @@ -163,6 +164,7 @@ 'policy_id' => $this->policy->id, 'tenant_id' => $this->tenant->id, 'version_number' => 1, + 'capture_purpose' => PolicyVersionCapturePurpose::Backup, 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows10', 'snapshot' => $this->snapshotPayload, @@ -223,6 +225,7 @@ 'policy_id' => $this->policy->id, 'tenant_id' => $this->tenant->id, 'version_number' => 1, + 'capture_purpose' => PolicyVersionCapturePurpose::Backup, 'policy_type' => 'settingsCatalogPolicy', 'platform' => 'windows10', 'snapshot' => $this->snapshotPayload, diff --git a/tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php b/tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php index 24583d4..26d29dc 100644 --- a/tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php +++ b/tests/Feature/BaselineDriftEngine/CaptureBaselineContentTest.php @@ -9,10 +9,6 @@ use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; use App\Services\Baselines\InventoryMetaContract; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -64,12 +60,10 @@ 'snapshot' => $snapshotPayload, ]); - $expectedContentHash = app(DriftHasher::class)->hashNormalized( - [ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ], + $expectedContentHash = expectedPolicyVersionContentHash( + snapshot: $snapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', ); $opService = app(OperationRunService::class); diff --git a/tests/Feature/BaselineDriftEngine/CompareContentEvidenceTest.php b/tests/Feature/BaselineDriftEngine/CompareContentEvidenceTest.php index efaf6fb..7026486 100644 --- a/tests/Feature/BaselineDriftEngine/CompareContentEvidenceTest.php +++ b/tests/Feature/BaselineDriftEngine/CompareContentEvidenceTest.php @@ -9,10 +9,6 @@ use App\Models\Policy; use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -56,11 +52,11 @@ ], ]; - $baselineHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshot, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: $baselineSnapshot, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); @@ -180,11 +176,11 @@ ], ]; - $baselineHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: $snapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); diff --git a/tests/Feature/BaselineDriftEngine/CompareFidelityMismatchTest.php b/tests/Feature/BaselineDriftEngine/CompareFidelityMismatchTest.php index 1eab8f4..770a2b0 100644 --- a/tests/Feature/BaselineDriftEngine/CompareFidelityMismatchTest.php +++ b/tests/Feature/BaselineDriftEngine/CompareFidelityMismatchTest.php @@ -9,10 +9,6 @@ use App\Models\Policy; use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -174,11 +170,11 @@ ], ]; - $baselineContentHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineContentHash = expectedPolicyVersionContentHash( + snapshot: $snapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); diff --git a/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php b/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php index 269ed01..f30f5b6 100644 --- a/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php +++ b/tests/Feature/BaselineDriftEngine/FindingFidelityTest.php @@ -9,10 +9,6 @@ use App\Models\Policy; use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -54,11 +50,11 @@ ], ]; - $baselineHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: $baselineSnapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); diff --git a/tests/Feature/BaselineDriftEngine/FindingProvenanceTest.php b/tests/Feature/BaselineDriftEngine/FindingProvenanceTest.php index 43dc06b..bea7a3d 100644 --- a/tests/Feature/BaselineDriftEngine/FindingProvenanceTest.php +++ b/tests/Feature/BaselineDriftEngine/FindingProvenanceTest.php @@ -9,10 +9,6 @@ use App\Models\Policy; use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -54,11 +50,11 @@ ], ]; - $baselineHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($baselineSnapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: $baselineSnapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); diff --git a/tests/Feature/BaselineDriftEngine/ResolverTest.php b/tests/Feature/BaselineDriftEngine/ResolverTest.php index 2b206e7..e0ed0b2 100644 --- a/tests/Feature/BaselineDriftEngine/ResolverTest.php +++ b/tests/Feature/BaselineDriftEngine/ResolverTest.php @@ -5,10 +5,6 @@ use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; use App\Services\Baselines\CurrentStateHashResolver; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use Carbon\CarbonImmutable; it('Baseline resolver prefers content evidence over meta evidence when available', function () { @@ -48,15 +44,11 @@ 'last_seen_operation_run_id' => null, ]); - $expectedContentHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff( - is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [], - (string) $policyVersion->policy_type, - is_string($policyVersion->platform) ? $policyVersion->platform : null, - ), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $expectedContentHash = expectedPolicyVersionContentHash( + snapshot: is_array($policyVersion->snapshot) ? $policyVersion->snapshot : [], + policyType: (string) $policyVersion->policy_type, + platform: is_string($policyVersion->platform) ? $policyVersion->platform : null, + ); $expectedMetaHash = app(BaselineSnapshotIdentity::class)->hashItemContent( policyType: (string) $inventory->policy_type, diff --git a/tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php b/tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php index 0fe10d0..49972d6 100644 --- a/tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php +++ b/tests/Feature/Baselines/BaselineCompareCrossTenantMatchTest.php @@ -10,10 +10,6 @@ use App\Models\PolicyVersion; use App\Models\Tenant; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -55,11 +51,11 @@ ], ]; - $expectedContentHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $expectedContentHash = expectedPolicyVersionContentHash( + snapshot: $snapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); BaselineSnapshotItem::factory()->create([ 'baseline_snapshot_id' => (int) $snapshot->getKey(), diff --git a/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php b/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php index 9299af5..61d65f6 100644 --- a/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php +++ b/tests/Feature/Baselines/BaselineCompareDriftEvidenceContractTest.php @@ -9,10 +9,6 @@ use App\Models\Policy; use App\Models\PolicyVersion; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\OperationRunService; use App\Support\Baselines\BaselineSubjectKey; @@ -80,20 +76,15 @@ 'scope_tags' => ['ids' => ['0'], 'names' => ['Default']], ]); - $hasher = app(DriftHasher::class); - $settingsNormalizer = app(SettingsNormalizer::class); - $assignmentsNormalizer = app(AssignmentsNormalizer::class); - $scopeTagsNormalizer = app(ScopeTagsNormalizer::class); - - $baselineHash = $hasher->hashNormalized([ - 'settings' => $settingsNormalizer->normalizeForDiff( - snapshot: $baselineVersion->snapshot ?? [], - policyType: $policyType, - platform: $baselineVersion->platform, - ), - 'assignments' => $assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments ?? []), - 'scope_tag_ids' => $scopeTagsNormalizer->normalizeIds($baselineVersion->scope_tags ?? []), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [], + policyType: $policyType, + platform: is_string($baselineVersion->platform) ? $baselineVersion->platform : null, + assignments: is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [], + scopeTags: is_array($baselineVersion->scope_tags) ? $baselineVersion->scope_tags : [], + secretFingerprints: is_array($baselineVersion->secret_fingerprints) ? $baselineVersion->secret_fingerprints : [], + redactionVersion: is_numeric($baselineVersion->redaction_version) ? (int) $baselineVersion->redaction_version : null, + ); $workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKey); @@ -212,20 +203,15 @@ 'scope_tags' => ['ids' => ['0'], 'names' => ['Default']], ]); - $hasher = app(DriftHasher::class); - $settingsNormalizer = app(SettingsNormalizer::class); - $assignmentsNormalizer = app(AssignmentsNormalizer::class); - $scopeTagsNormalizer = app(ScopeTagsNormalizer::class); - - $baselineHash = $hasher->hashNormalized([ - 'settings' => $settingsNormalizer->normalizeForDiff( - snapshot: $baselineVersion->snapshot ?? [], - policyType: $policyType, - platform: $baselineVersion->platform, - ), - 'assignments' => $assignmentsNormalizer->normalizeForDiff($baselineVersion->assignments ?? []), - 'scope_tag_ids' => $scopeTagsNormalizer->normalizeIds($baselineVersion->scope_tags ?? []), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: is_array($baselineVersion->snapshot) ? $baselineVersion->snapshot : [], + policyType: $policyType, + platform: is_string($baselineVersion->platform) ? $baselineVersion->platform : null, + assignments: is_array($baselineVersion->assignments) ? $baselineVersion->assignments : [], + scopeTags: is_array($baselineVersion->scope_tags) ? $baselineVersion->scope_tags : [], + secretFingerprints: is_array($baselineVersion->secret_fingerprints) ? $baselineVersion->secret_fingerprints : [], + redactionVersion: is_numeric($baselineVersion->redaction_version) ? (int) $baselineVersion->redaction_version : null, + ); $workspaceSafeExternalId = BaselineSubjectKey::workspaceSafeSubjectExternalId($policyType, (string) $subjectKey); diff --git a/tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php b/tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php index 5f98e74..36aff6b 100644 --- a/tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php +++ b/tests/Feature/Baselines/BaselineCompareWhyNoFindingsReasonCodeTest.php @@ -10,10 +10,6 @@ use App\Models\PolicyVersion; use App\Services\Baselines\BaselineContentCapturePhase; use App\Services\Baselines\BaselineSnapshotIdentity; -use App\Services\Drift\DriftHasher; -use App\Services\Drift\Normalizers\AssignmentsNormalizer; -use App\Services\Drift\Normalizers\ScopeTagsNormalizer; -use App\Services\Drift\Normalizers\SettingsNormalizer; use App\Services\Intune\AuditLogger; use App\Services\Intune\PolicyCaptureOrchestrator; use App\Services\OperationRunService; @@ -431,11 +427,11 @@ ], ]; - $baselineHash = app(DriftHasher::class)->hashNormalized([ - 'settings' => app(SettingsNormalizer::class)->normalizeForDiff($snapshotPayload, 'deviceConfiguration', 'windows'), - 'assignments' => app(AssignmentsNormalizer::class)->normalizeForDiff([]), - 'scope_tag_ids' => app(ScopeTagsNormalizer::class)->normalizeIds([]), - ]); + $baselineHash = expectedPolicyVersionContentHash( + snapshot: $snapshotPayload, + policyType: 'deviceConfiguration', + platform: 'windows', + ); $subjectKey = BaselineSubjectKey::fromDisplayName((string) $policy->display_name); expect($subjectKey)->not->toBeNull(); diff --git a/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php b/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php index 1d67ad8..03e1609 100644 --- a/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php +++ b/tests/Feature/Filament/CanonicalAdminTenantFilterStateTest.php @@ -92,3 +92,23 @@ function canonicalAdminTenantRequest(): Request 'tenant' => ['value' => (string) $tenant->external_id], ]); }); + +it('does not persist an empty filter bag when only canonical tenant state is synced', function (): void { + [$user, $tenant] = createUserWithTenant(role: 'owner'); + + $this->actingAs($user); + Filament::setTenant(null, true); + + session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id); + session()->put(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, [ + (string) $tenant->workspace_id => (int) $tenant->getKey(), + ]); + + app(CanonicalAdminTenantFilterState::class)->sync( + 'filament.findings.filters', + request: canonicalAdminTenantRequest(), + tenantFilterName: null, + ); + + expect(session()->has('filament.findings.filters'))->toBeFalse(); +}); diff --git a/tests/Feature/Filament/InventoryPagesTest.php b/tests/Feature/Filament/InventoryPagesTest.php index d76e2fa..9138801 100644 --- a/tests/Feature/Filament/InventoryPagesTest.php +++ b/tests/Feature/Filament/InventoryPagesTest.php @@ -45,14 +45,12 @@ ->get($itemsUrl) ->assertOk() ->assertSee('Run Inventory Sync') - ->assertSee($coverageUrl) ->assertSee($kpiLabels) ->assertSee('Item A'); $this->actingAs($user) - ->get(InventoryCoverage::getUrl(tenant: $tenant)) + ->get($coverageUrl) ->assertOk() - ->assertSee($itemsUrl) ->assertSee($kpiLabels) ->assertSee('Coverage') ->assertSee('Searchable support matrix') diff --git a/tests/Feature/OperationRunServiceTest.php b/tests/Feature/OperationRunServiceTest.php index 76840c1..5489f4d 100644 --- a/tests/Feature/OperationRunServiceTest.php +++ b/tests/Feature/OperationRunServiceTest.php @@ -5,11 +5,16 @@ use App\Models\User; use App\Services\OperationRunService; +function operationRunService(): OperationRunService +{ + return app(OperationRunService::class); +} + it('creates a new operation run', function () { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $run = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $user); @@ -27,7 +32,7 @@ it('reuses an active run (idempotent)', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']); $runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']); @@ -39,7 +44,7 @@ it('dedupes assignment run identities by type and scope', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $fetchRunA = $service->ensureRunWithIdentity( tenant: $tenant, @@ -112,7 +117,7 @@ $userA = User::factory()->create(); $userB = User::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userA); $runB = $service->ensureRun($tenant, 'test.action', ['scope' => 'full'], $userB); @@ -124,7 +129,7 @@ it('hashes inputs deterministically regardless of key order', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRun($tenant, 'test.action', ['b' => 2, 'a' => 1]); $runB = $service->ensureRun($tenant, 'test.action', ['a' => 1, 'b' => 2]); @@ -134,7 +139,7 @@ it('hashes list inputs deterministically regardless of list order', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRun($tenant, 'test.action', ['ids' => [2, 1]]); $runB = $service->ensureRun($tenant, 'test.action', ['ids' => [1, 2]]); @@ -144,7 +149,7 @@ it('handles unique-index race collisions by returning the active run', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $fired = false; @@ -187,7 +192,7 @@ it('creates a new run after the previous one completed', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRun($tenant, 'test.action', ['scope' => 'full']); $runA->update(['status' => 'completed']); @@ -201,7 +206,7 @@ it('reuses the same run even after completion when using strict identity', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $runA = $service->ensureRunWithIdentityStrict( tenant: $tenant, @@ -225,7 +230,7 @@ it('handles strict unique-index race collisions by returning the existing run', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $fired = false; @@ -273,7 +278,7 @@ it('updates run lifecycle fields and summaries', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $run = $service->ensureRun($tenant, 'test.action', []); @@ -293,7 +298,7 @@ it('sanitizes failure messages and redacts obvious secrets', function () { $tenant = Tenant::factory()->create(); - $service = new OperationRunService; + $service = operationRunService(); $run = $service->ensureRun($tenant, 'test.action', []); diff --git a/tests/Feature/ProviderConnections/ProviderConnectionAuthorizationSpec081Test.php b/tests/Feature/ProviderConnections/ProviderConnectionAuthorizationSpec081Test.php index 6373a77..af6aa3e 100644 --- a/tests/Feature/ProviderConnections/ProviderConnectionAuthorizationSpec081Test.php +++ b/tests/Feature/ProviderConnections/ProviderConnectionAuthorizationSpec081Test.php @@ -8,7 +8,7 @@ use App\Models\User; use App\Support\Workspaces\WorkspaceContext; -it('Spec081 redirects non-members on provider connection management routes', function (): void { +it('Spec081 returns 404 for non-members on provider connection management routes', function (): void { $tenant = Tenant::factory()->create([ 'status' => 'active', ]); @@ -25,7 +25,7 @@ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, ]) ->get(ProviderConnectionResource::getUrl('edit', ['record' => $connection], tenant: $tenant)) - ->assertRedirect(); + ->assertNotFound(); }); it('Spec081 returns 403 for members without provider manage capability', function (): void { diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php index 48635f2..93d4e4c 100644 --- a/tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsAccessTest.php @@ -37,7 +37,7 @@ ->assertNotFound(); }); -it('redirects non-workspace-members with stale session', function (): void { +it('returns 404 for non-workspace-members with stale session', function (): void { $user = User::factory()->create(); $workspace = Workspace::factory()->create(); $tenant = Tenant::factory()->create([ @@ -49,7 +49,7 @@ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), ]) ->get("/admin/tenants/{$tenant->external_id}/required-permissions") - ->assertRedirect(); + ->assertNotFound(); }); it('returns 404 when the route tenant is invalid instead of falling back to the current tenant context', function (): void { diff --git a/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php b/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php index b084ea0..9bf79f1 100644 --- a/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php +++ b/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php @@ -63,20 +63,20 @@ $response->assertDontSee('>Governance', false); }); -it('redirects non-workspace-members with stale session (FR-002 regression guard)', function (): void { +it('returns 404 for non-workspace-members with stale session (FR-002 regression guard)', function (): void { $user = User::factory()->create(); $workspace = Workspace::factory()->create(); $tenant = Tenant::factory()->create([ 'workspace_id' => (int) $workspace->getKey(), ]); - // User is NOT a workspace member — middleware clears stale session and redirects + // User is NOT a workspace member, so the canonical route is deny-as-not-found. $this->actingAs($user) ->withSession([ WorkspaceContext::SESSION_KEY => (int) $workspace->getKey(), ]) ->get("/admin/tenants/{$tenant->external_id}/required-permissions") - ->assertRedirect(); + ->assertNotFound(); }); it('returns 404 for workspace members without tenant entitlement after middleware change (FR-002 regression guard)', function (): void { diff --git a/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php b/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php index a2c5e69..64e2014 100644 --- a/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php +++ b/tests/Feature/Spec080WorkspaceManagedTenantAdminMigrationTest.php @@ -32,14 +32,14 @@ ->assertOk(); }); -it('redirects non-members on the workspace-managed tenants index', function (): void { +it('returns 404 for non-members on the workspace-managed tenants index', function (): void { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get('/admin/tenants') - ->assertRedirect(); + ->assertNotFound(); }); it('allows workspace members to open the workspace-managed tenant view route', function (): void { @@ -61,14 +61,14 @@ ->assertSee('/admin/provider-connections?tenant_id='.$tenant->external_id, false); }); -it('redirects non-members on the workspace-managed tenant view route', function (): void { +it('returns 404 for non-members on the workspace-managed tenant view route', function (): void { $tenant = Tenant::factory()->create(); $user = User::factory()->create(); $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) ->get("/admin/tenants/{$tenant->external_id}") - ->assertRedirect(); + ->assertNotFound(); }); it('exposes memberships management under workspace scope', function (): void { diff --git a/tests/Feature/System/Spec114/AccessLogsTest.php b/tests/Feature/System/Spec114/AccessLogsTest.php index e429df1..64fa203 100644 --- a/tests/Feature/System/Spec114/AccessLogsTest.php +++ b/tests/Feature/System/Spec114/AccessLogsTest.php @@ -10,7 +10,7 @@ uses(RefreshDatabase::class); -it('lists platform auth access logs with success and failure statuses plus break-glass actions', function () { +it('lists platform auth access logs with success and failed statuses plus break-glass actions', function () { $tenant = Tenant::factory()->create([ 'external_id' => 'platform', ]); @@ -28,7 +28,7 @@ 'tenant_id' => (int) $tenant->getKey(), 'workspace_id' => (int) $tenant->workspace_id, 'action' => 'platform.auth.login', - 'status' => 'failure', + 'status' => 'failed', 'metadata' => ['attempted_email' => 'operator@tenantpilot.io'], 'recorded_at' => now(), ]); @@ -64,7 +64,7 @@ ->assertSuccessful() ->assertSee('platform.auth.login') ->assertSee('success') - ->assertSee('failure') + ->assertSee('failed') ->assertSee('platform.break_glass.enter') ->assertDontSee('platform.unrelated.event'); }); diff --git a/tests/Pest.php b/tests/Pest.php index 61339e9..d96474d 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -363,3 +363,35 @@ function ensureDefaultProviderConnection(Tenant $tenant, string $provider = 'mic return $connection; } + +/** + * @param array $snapshot + * @param array> $assignments + * @param array $scopeTags + * @param array> $secretFingerprints + */ +function expectedPolicyVersionContentHash( + array $snapshot, + string $policyType, + ?string $platform = null, + array $assignments = [], + array $scopeTags = [], + array $secretFingerprints = [], + ?int $redactionVersion = 1, +): string { + return app(\App\Services\Drift\DriftHasher::class)->hashNormalized([ + 'settings' => app(\App\Services\Drift\Normalizers\SettingsNormalizer::class)->normalizeForDiff( + $snapshot, + $policyType, + $platform, + ), + 'assignments' => app(\App\Services\Drift\Normalizers\AssignmentsNormalizer::class)->normalizeForDiff($assignments), + 'scope_tag_ids' => app(\App\Services\Drift\Normalizers\ScopeTagsNormalizer::class)->normalizeIds($scopeTags), + 'secret_fingerprints' => [ + 'snapshot' => is_array($secretFingerprints['snapshot'] ?? null) ? $secretFingerprints['snapshot'] : [], + 'assignments' => is_array($secretFingerprints['assignments'] ?? null) ? $secretFingerprints['assignments'] : [], + 'scope_tags' => is_array($secretFingerprints['scope_tags'] ?? null) ? $secretFingerprints['scope_tags'] : [], + ], + 'redaction_version' => $redactionVersion, + ]); +} diff --git a/tests/Unit/Intune/VersionServiceConcurrencyTest.php b/tests/Unit/Intune/VersionServiceConcurrencyTest.php index 68a1c52..cbff0c5 100644 --- a/tests/Unit/Intune/VersionServiceConcurrencyTest.php +++ b/tests/Unit/Intune/VersionServiceConcurrencyTest.php @@ -22,7 +22,7 @@ $policy->load('tenant'); $service = new VersionService( - auditLogger: new AuditLogger, + auditLogger: app(AuditLogger::class), snapshotService: Mockery::mock(PolicySnapshotService::class), snapshotRedactor: new PolicySnapshotRedactor, assignmentFetcher: Mockery::mock(AssignmentFetcher::class), diff --git a/tests/Unit/OpsUx/RunFailureSanitizerTest.php b/tests/Unit/OpsUx/RunFailureSanitizerTest.php index a362c75..1d92400 100644 --- a/tests/Unit/OpsUx/RunFailureSanitizerTest.php +++ b/tests/Unit/OpsUx/RunFailureSanitizerTest.php @@ -37,3 +37,14 @@ expect($sanitized)->toContain('passwordMinimumLength'); expect($sanitized)->not->toContain('super-secret'); }); + +it('redacts email domains that survive token redaction boundaries', function (): void { + $message = 'Authorization: Bearer highly-sensitive-token-for-user@example.com'; + + $sanitized = RunFailureSanitizer::sanitizeMessage($message); + + expect($sanitized) + ->not->toContain('Bearer') + ->not->toContain('@example.com') + ->toContain('[REDACTED_EMAIL]'); +}); diff --git a/tests/Unit/OpsUx/SummaryCountsNormalizerTest.php b/tests/Unit/OpsUx/SummaryCountsNormalizerTest.php index af0592b..e642f18 100644 --- a/tests/Unit/OpsUx/SummaryCountsNormalizerTest.php +++ b/tests/Unit/OpsUx/SummaryCountsNormalizerTest.php @@ -38,7 +38,7 @@ 'alert_events_produced' => 0, ]); - expect($line)->toBe('Report deduped: 1 · Findings unchanged: 10'); + expect($line)->toBe('Findings unchanged: 10 · Report deduped: 1'); }); it('returns null when all values are zero', function () {