Tests/Unit/Get-SPCOverPermissionedUser.Tests.ps1
|
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' } Describe 'Get-SPCOverPermissionedUser' { BeforeAll { $script:sut = "$PSScriptRoot\..\..\Public\Scan\Get-SPCOverPermissionedUser.ps1" . $PSScriptRoot\..\..\Public\Scan\Get-SPCOverPermissionedUser.ps1 function Test-SPCConnection {} function Get-PnPTenantSite {} function Connect-PnPOnline {} function Invoke-PnPSPRestMethod {} function Start-Sleep {} } BeforeEach { $script:SPCContext = [PSCustomObject]@{ TenantName = 'tenant' AuthMethod = 'Interactive' PnPContext = $null } } Context 'When calculating EAS' { It 'AC-F3-01 Should correctly calculate EAS based on formula' { Mock Get-PnPTenantSite { return @([PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/site1" }) } Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline {} Mock Invoke-PnPSPRestMethod { $assignments = @() for ($i=0; $i -lt 5; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|usera@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Full Control' }) } } for ($i=0; $i -lt 10; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|usera@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Edit' }) } } for ($i=0; $i -lt 15; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|usera@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Read' }) } } return [PSCustomObject]@{ value = $assignments } } $result = Get-SPCOverPermissionedUser $result.Count | Should -Be 1 $result[0].EAS | Should -Be 50 # 5*3 + 10*2 + 15*1 = 50 } } Context 'When EAS is greater than 100' { It 'AC-F3-02 Should flag user with Red Alert if EAS > 100' { Mock Get-PnPTenantSite { return @([PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/site1" }) } Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline {} Mock Invoke-PnPSPRestMethod { $assignments = @() for ($i=0; $i -lt 40; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|userb@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Full Control' }) } } return [PSCustomObject]@{ value = $assignments } } $result = Get-SPCOverPermissionedUser $result.Count | Should -Be 1 $result[0].EAS | Should -Be 120 $result[0].IsRedAlert | Should -Be $true } } Context 'When EAS is exactly 100 or less' { It 'AC-F3-03 Should not flag user with Red Alert if EAS <= 100' { Mock Get-PnPTenantSite { return @([PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/site1" }) } Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline {} Mock Invoke-PnPSPRestMethod { $assignments = @() for ($i=0; $i -lt 30; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|userc@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Full Control' }) } } for ($i=0; $i -lt 5; $i++) { $assignments += [PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|userc@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Edit' }) } } return [PSCustomObject]@{ value = $assignments } } $result = Get-SPCOverPermissionedUser $result.Count | Should -Be 1 $result[0].EAS | Should -Be 100 $result[0].IsRedAlert | Should -Be $false } } Context 'Negative Scenarios' { It 'AC-NEG-01 Should throw Terminating Error ERR-AUTH-001 when connection fails' { Mock Get-PnPTenantSite { throw "Connection failed" } { Get-SPCOverPermissionedUser } | Should -Throw -ErrorId "ERR-AUTH-001*" } It 'AC-NEG-02 Should write non-terminating error and continue for inaccessible sites' { Mock Get-PnPTenantSite { return @( [PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/errorSite" }, [PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/goodSite" } ) } Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline { if ($args[0] -match "errorSite") { throw "Access Denied" } return "MockConnection" } Mock Invoke-PnPSPRestMethod { return [PSCustomObject]@{ value = @([PSCustomObject]@{ Member = [PSCustomObject]@{ PrincipalType = 'User'; LoginName = "i:0#.f|membership|good@domain.com" }; RoleDefinitionBindings = @([PSCustomObject]@{ Name = 'Full Control' }) }) } } $result = Get-SPCOverPermissionedUser -ErrorAction SilentlyContinue $result.Count | Should -Be 1 $result[0].UPN | Should -Be "good@domain.com" } It 'AC-NEG-03 Should implement exponential backoff retry on 429/503 errors' { Mock Get-PnPTenantSite { return @([PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/site1" }) } $global:retryCount = 0 Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline { $global:retryCount++ if ($global:retryCount -lt 3) { throw "429 Too Many Requests" } return "MockConnection" } Mock Invoke-PnPSPRestMethod { return [PSCustomObject]@{ value = @() } } Mock Start-Sleep { return $true } # Mock Start-Sleep to speed up test $result = Get-SPCOverPermissionedUser $global:retryCount | Should -Be 3 } } Context 'Security Scenarios' { It 'AC-SEC-01 Should not leak credentials in verbose stream' { Mock Get-PnPTenantSite { return @([PSCustomObject]@{ Url = "https://tenant.sharepoint.com/sites/site1" }) } Mock Get-PnPAccessToken { return "MockToken" } Mock Connect-PnPOnline {} Mock Invoke-PnPSPRestMethod { return [PSCustomObject]@{ value = @() } } $verboseOutput = "" try { Get-SPCOverPermissionedUser -Verbose 4>&1 | ForEach-Object { $verboseOutput += $_.ToString() } } catch { } $verboseOutput | Should -Not -Match 'password|secret|token|pfx|credential' } It 'AC-SEC-03 Should not execute dynamic string via IEX' { $scriptContent = Get-Content $script:sut -Raw $scriptContent -match 'iex|Invoke-Expression' | Should -BeFalse } } } |