Tests/Unit/Phase2.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } BeforeAll { $moduleRoot = Resolve-Path (Join-Path $PSScriptRoot '..\..') $publicDir = Join-Path $moduleRoot 'Public' $privateDir = Join-Path $moduleRoot 'Private' # Dot-source required private helpers and public cmdlets if (Test-Path (Join-Path $privateDir 'Measure-SPCScoreInternal.ps1')) { . (Join-Path $privateDir 'Measure-SPCScoreInternal.ps1') } elseif (Test-Path (Join-Path $privateDir 'Calculate-SPCScoreInternal.ps1')) { . (Join-Path $privateDir 'Calculate-SPCScoreInternal.ps1') } if (Test-Path (Join-Path $privateDir 'PnPWrappers.ps1')) { . (Join-Path $privateDir 'PnPWrappers.ps1') } if (Test-Path (Join-Path $privateDir 'Connect-SPCSiteInternal.ps1')) { . (Join-Path $privateDir 'Connect-SPCSiteInternal.ps1') } . (Join-Path $privateDir 'Get-SPCLibraryBrokenInheritanceInternal.ps1') . (Join-Path $privateDir 'Test-SPCConnection.ps1') if (Test-Path (Join-Path $publicDir 'Report\Get-SPCPermissionHealthScore.ps1')) { . (Join-Path $publicDir 'Report\Get-SPCPermissionHealthScore.ps1') } if (Test-Path (Join-Path $publicDir 'Scan\Get-SPCBrokenInheritance.ps1')) { . (Join-Path $publicDir 'Scan\Get-SPCBrokenInheritance.ps1') } if (Test-Path (Join-Path $publicDir 'Report\Compare-SPCPermissionSnapshot.ps1')) { . (Join-Path $publicDir 'Report\Compare-SPCPermissionSnapshot.ps1') } # Define score helper wrapper for backward/forward compatibility during test execution function Invoke-ScoreTest { param( [int]$OrphanedUserCount, [int]$HighRiskGuestCount, [int]$OverPermissionedUserCount, [int]$BrokenInheritanceSiteCount, [int]$MissingOwnerSiteCount ) if (Get-Command 'Measure-SPCScoreInternal' -ErrorAction SilentlyContinue) { Measure-SPCScoreInternal @PSBoundParameters } else { Calculate-SPCScoreInternal @PSBoundParameters } } # Standardized mock module context schema conforming to FR-TEST-03 / AC-TEST-04 $script:SPCContext = [PSCustomObject]@{ TenantName = 'contoso' TenantId = 'tid-12345' IsConnected = $true AuthMethod = 'Interactive' ConnectedAt = (Get-Date).ToUniversalTime() GraphTokenRefreshedAt = (Get-Date).ToUniversalTime() PnPContext = [PSCustomObject]@{ Url = 'https://contoso-admin.sharepoint.com' } GraphAccessToken = 'mock_access_token' _ClientId = 'mock_client_id' _CertificateThumbprint = $null _CertificatePath = $null _CertificatePassword = $null _ClientSecret = $null } } Describe 'Phase 2 Architecture & Health Analytics Unit Tests' { Context 'Measure-SPCScoreInternal (Health Score Engine)' { It 'AC-STD-04: Returns score 100 with zero risk counts' { $res = Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 0 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 100 $res.Breakdown.OrphanedUserDeduction | Should -Be 0 $res.Breakdown.HighRiskGuestDeduction | Should -Be 0 $res.Breakdown.OverPermissionedDeduction | Should -Be 0 $res.Breakdown.BrokenInheritanceDeduction | Should -Be 0 $res.Breakdown.MissingOwnerDeduction | Should -Be 0 } It 'AC-STD-04: Deducts Orphaned User points below the 30-point cap' { $res = Invoke-ScoreTest -OrphanedUserCount 10 -HighRiskGuestCount 0 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 80 $res.Breakdown.OrphanedUserDeduction | Should -Be 20 } It 'AC-STD-04: Caps Orphaned User deduction at maximum 30 points' { $res = Invoke-ScoreTest -OrphanedUserCount 20 -HighRiskGuestCount 0 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 70 $res.Breakdown.OrphanedUserDeduction | Should -Be 30 } It 'AC-STD-04: Deducts High-Risk Guest points below the 25-point cap' { $res = Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 10 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 85 $res.Breakdown.HighRiskGuestDeduction | Should -Be 15 } It 'AC-STD-04: Caps High-Risk Guest deduction at maximum 25 points' { $res = Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 20 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 75 $res.Breakdown.HighRiskGuestDeduction | Should -Be 25 } It 'AC-STD-04: Caps Over-Permissioned User deduction at maximum 20 points' { $res = Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 0 -OverPermissionedUserCount 15 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 $res.OverallScore | Should -Be 80 $res.Breakdown.OverPermissionedDeduction | Should -Be 20 } It 'AC-STD-04: Correctly calculates Broken Inheritance and Missing Owner deductions' { $res = Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 0 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 2 -MissingOwnerSiteCount 1 $res.OverallScore | Should -Be 60 $res.Breakdown.BrokenInheritanceDeduction | Should -Be 30 $res.Breakdown.MissingOwnerDeduction | Should -Be 10 } It 'AC-STD-04: Floors overall health score to 0 in worst-case risk combination' { $res = Invoke-ScoreTest -OrphanedUserCount 100 -HighRiskGuestCount 100 -OverPermissionedUserCount 100 -BrokenInheritanceSiteCount 10 -MissingOwnerSiteCount 10 $res.OverallScore | Should -Be 0 } } Context 'Get-SPCLibraryBrokenInheritanceInternal' { BeforeEach { $script:SPCContext = [PSCustomObject]@{ TenantName = 'contoso' TenantId = 'tid-12345' IsConnected = $true AuthMethod = 'Interactive' ConnectedAt = (Get-Date).ToUniversalTime() GraphTokenRefreshedAt = (Get-Date).ToUniversalTime() PnPContext = [PSCustomObject]@{ Url = 'https://contoso-admin.sharepoint.com' } GraphAccessToken = 'mock_access_token' _ClientId = 'mock_client_id' _CertificateThumbprint = $null _CertificatePath = $null _CertificatePassword = $null _ClientSecret = $null } Mock Get-PnPAccessToken { return 'mock_token' } Mock Connect-PnPOnline { return [PSCustomObject]@{ Url = 'https://test.sharepoint.com/sites/test' } } } It 'AC-ARCH-03: Detects folders with broken permission inheritance' { Mock Get-PnPList { return @( [PSCustomObject]@{ Title = 'DocLib1' BaseType = 'DocumentLibrary' Hidden = $false HasUniqueRoleAssignments = $false RootFolder = [PSCustomObject]@{ ServerRelativeUrl = '/sites/test/DocLib1' } } ) } Mock Get-PnPListItem { return @( [PSCustomObject]@{ Id = 1 HasUniqueRoleAssignments = $true FileSystemObjectType = 'Folder' FieldValues = @{ FileRef = '/sites/test/DocLib1/Folder1' } } ) } $res = @(Get-SPCLibraryBrokenInheritanceInternal -SiteUrl 'https://test.sharepoint.com/sites/test') $res.Count | Should -Be 1 $res[0].Path | Should -Be '/sites/test/DocLib1/Folder1' $res[0].Type | Should -Be 'Folder' } } Context 'Compare-SPCPermissionSnapshot' { BeforeAll { $baseline = @( @{ user = @{ upn = 'admin@test.com' }; isSCA = $true; isOwner = $false; permissions = @() }, @{ user = @{ upn = 'owner@test.com' }; isSCA = $false; isOwner = $true; permissions = @() }, @{ user = @{ upn = 'user1@test.com' }; isSCA = $false; isOwner = $false; permissions = @('Full Control') }, @{ user = @{ upn = 'guest#EXT#@external.com' }; isSCA = $false; isOwner = $false; permissions = @() } ) | ConvertTo-Json -Depth 5 $current = @( @{ user = @{ upn = 'admin@test.com' }; isSCA = $true; isOwner = $false; permissions = @() }, @{ user = @{ upn = 'newadmin@test.com' }; isSCA = $true; isOwner = $false; permissions = @() }, @{ user = @{ upn = 'owner@test.com' }; isSCA = $false; isOwner = $true; permissions = @() }, @{ user = @{ upn = 'newowner@test.com' }; isSCA = $false; isOwner = $true; permissions = @() }, @{ user = @{ upn = 'user1@test.com' }; isSCA = $false; isOwner = $false; permissions = @('Full Control') }, @{ user = @{ upn = 'user2@test.com' }; isSCA = $false; isOwner = $false; permissions = @('Full Control') }, @{ user = @{ upn = 'guest#EXT#@external.com' }; isSCA = $false; isOwner = $false; permissions = @() }, @{ user = @{ upn = 'newguest#EXT#@external.com' }; isSCA = $false; isOwner = $false; permissions = @() } ) | ConvertTo-Json -Depth 5 $script:tempBaseline = Join-Path $TestDrive 'baseline.json' $script:tempCurrent = Join-Path $TestDrive 'current.json' Set-Content -Path $script:tempBaseline -Value $baseline -Encoding UTF8 Set-Content -Path $script:tempCurrent -Value $current -Encoding UTF8 } It 'AC-ARCH-05: Accurately detects permission drift across SCAs, Owners, Direct Full Control, and Guests' { Mock Test-SPCConnection { return $true } $res = Compare-SPCPermissionSnapshot -BaselineSnapshotPath $script:tempBaseline -CurrentSnapshotPath $script:tempCurrent $res.NewSCAOrOwners | Should -Contain 'newadmin@test.com' $res.NewSCAOrOwners | Should -Contain 'newowner@test.com' $res.NewDirectFullControlAssignments | Should -Contain 'user2@test.com' $res.NewGuestAccounts | Should -Contain 'newguest#EXT#@external.com' } } Context 'Security & Stream Isolation' { It 'AC-SEC-02: Does not leak secrets or bearer tokens into Verbose stream' { $script:verboseLog = @() Mock Write-Verbose { param($Message) $script:verboseLog += $Message } Invoke-ScoreTest -OrphanedUserCount 0 -HighRiskGuestCount 0 -OverPermissionedUserCount 0 -BrokenInheritanceSiteCount 0 -MissingOwnerSiteCount 0 -Verbose $leak = $script:verboseLog | Where-Object { $_ -match 'password|secret|token|bearer' } $leak | Should -BeNullOrEmpty } } } |