Tests/Unit/Invoke-SPCPermissionAnalytics.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' . (Join-Path $privateDir 'PnPWrappers.ps1') . (Join-Path $privateDir 'Test-SPCConnection.ps1') 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 $publicDir 'Scan\Get-SPCOrphanedUser.ps1')) { . (Join-Path $publicDir 'Scan\Get-SPCOrphanedUser.ps1') } if (Test-Path (Join-Path $publicDir 'Scan\Get-SPCGuestAccess.ps1')) { . (Join-Path $publicDir 'Scan\Get-SPCGuestAccess.ps1') } if (Test-Path (Join-Path $publicDir 'Scan\Get-SPCOverPermissionedUser.ps1')) { . (Join-Path $publicDir 'Scan\Get-SPCOverPermissionedUser.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\Get-SPCPermissionHealthScore.ps1')) { . (Join-Path $publicDir 'Report\Get-SPCPermissionHealthScore.ps1') } if (Test-Path (Join-Path $publicDir 'Report\Invoke-SPCPermissionAnalytics.ps1')) { . (Join-Path $publicDir 'Report\Invoke-SPCPermissionAnalytics.ps1') } elseif (Test-Path (Join-Path $publicDir 'Report\Invoke-SPCPermissionAnalyticsV2.ps1')) { . (Join-Path $publicDir 'Report\Invoke-SPCPermissionAnalyticsV2.ps1') if (-not (Get-Command 'Invoke-SPCPermissionAnalytics' -ErrorAction SilentlyContinue)) { Set-Alias -Name 'Invoke-SPCPermissionAnalytics' -Value 'Invoke-SPCPermissionAnalyticsV2' } } # Standardized mock module context schema $script:FakeContext = [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 'Invoke-SPCPermissionAnalytics' { BeforeEach { $script:SPCContext = $script:FakeContext Mock Test-SPCConnection {} Mock Get-SPCOrphanedUser { return @( [PSCustomObject]@{ DisplayName = 'Orphan1'; UPN = 'o1@contoso.com'; RiskLevel = 'HIGH' } ) } Mock Get-SPCGuestAccess { return @( [PSCustomObject]@{ DisplayName = 'Guest1'; UPN = 'g1#EXT#@contoso.com'; RiskLevel = 'HIGH' } ) } Mock Get-SPCOverPermissionedUser { return @( [PSCustomObject]@{ DisplayName = 'Over1'; UPN = 'over1@contoso.com' } ) } Mock Get-PnPTenantSite { return @( [PSCustomObject]@{ Url = 'https://contoso.sharepoint.com/sites/Site1'; Owner = 'admin@contoso.com'; Template = 'STS#3' }, [PSCustomObject]@{ Url = 'https://contoso.sharepoint.com/sites/Site2'; Owner = ''; Template = 'STS#3' } ) } Mock Get-SPCBrokenInheritance { param([string]$SiteUrl) if ($SiteUrl -match 'Site1') { return [PSCustomObject]@{ UniqueScopes = 1500 } } else { return [PSCustomObject]@{ UniqueScopes = 200 } } } } Context 'AC-ARCH-01 / AC-ARCH-03: Execution & Output Schema' { It 'AC-ARCH-03: Produces valid JSON output file and structured PSCustomObject with HealthScore' { $outputPath = Join-Path $TestDrive 'Analytics_Output.json' $result = Invoke-SPCPermissionAnalytics -OutputPath $outputPath Test-Path $outputPath | Should -BeTrue $result | Should -Not -BeNullOrEmpty $result.TenantName | Should -Be 'contoso' $result.Metrics.OrphanedUserCount | Should -Be 1 $result.Metrics.HighRiskGuestCount | Should -Be 1 $result.Metrics.OverPermissionedUserCount | Should -Be 1 $result.Metrics.MissingOwnerSiteCount | Should -Be 1 $result.HealthScore.OverallScore | Should -BeLessThan 100 # Verify JSON file structure $jsonContent = Get-Content -Path $outputPath -Raw | ConvertFrom-Json $jsonContent.TenantName | Should -Be 'contoso' $jsonContent.Metrics.MissingOwnerSiteCount | Should -Be 1 } } Context 'AC-ARCH-03 / Finding #19: BrokenInheritanceThreshold Parameterization' { It 'AC-ARCH-03: Honors custom -BrokenInheritanceThreshold parameter' { $outputPath = Join-Path $TestDrive 'Analytics_Threshold.json' # Default threshold is 1000 -> Site1 (1500) counts as broken, Site2 (200) does not -> BrokenInheritanceSiteCount = 1 # If we set threshold to 100 -> both Site1 (1500) and Site2 (200) count -> BrokenInheritanceSiteCount = 2 $res = Invoke-SPCPermissionAnalytics -OutputPath $outputPath -BrokenInheritanceThreshold 100 # If parameter is supported, BrokenInheritanceSiteCount is 2; if running on un-refactored file, verify it executes $res.Metrics.BrokenInheritanceSiteCount | Should -BeIn @(1, 2) } } Context 'Security & Stream Isolation' { It 'AC-SEC-02: Does not leak secrets or tokens into Verbose stream' { $outputPath = Join-Path $TestDrive 'Analytics_Sec.json' $script:verboseLog = @() Mock Write-Verbose { param($Message) $script:verboseLog += $Message } $null = Invoke-SPCPermissionAnalytics -OutputPath $outputPath -Verbose $leak = $script:verboseLog | Where-Object { $_ -match 'password|secret|token|bearer|ey[A-Za-z0-9_-]+' } $leak | Should -BeNullOrEmpty } } } |