tests/Get-MsecSharePointSite.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecSharePointSite, and specifically for -IncludeSettings. # # The traps: # # TWO SHARING SETTINGS DECIDE AN EXTERNAL SHARE AND NEITHER IS READABLE HERE. A site's own # sharing level and its own domain allow-list live on the tenant-admin surface, behind # Sites.FullControl.All - a WRITE permission this module does not hold. -IncludeSettings must # never look like it covers them. # # "COULD NOT READ" MUST NOT LOOK LIKE "ALL OFF". Settings is $null on failure, never a blank # object whose every flag reads as False. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'msec.psm1') -Force -ErrorAction Stop # DISCOVERABLE IS NOT USABLE. Pester's Mock needs the command to exist AND to yield # CommandMetadata; PnP.PowerShell is not a dependency of msec, so on a runner without it # the mock fails to build and the test reads as a bug in the code. $script:StubbedPnP = @() $usable = $false $found = Get-Command Get-PnPTenantSite -ErrorAction SilentlyContinue if ($found) { try { $null = [System.Management.Automation.CommandMetadata]::new($found); $usable = $true } catch { $usable = $false } } if (-not $usable) { Set-Item 'function:global:Get-PnPTenantSite' -Value { [CmdletBinding()] param([string] $Identity) } -Force $script:StubbedPnP += 'Get-PnPTenantSite' } } AfterAll { foreach ($c in $script:StubbedPnP) { Remove-Item "function:$c" -ErrorAction SilentlyContinue } Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecSharePointSite -CreatedSince' { BeforeEach { InModuleScope msec { Mock Assert-MsecSession -MockWith { } Mock Invoke-MsecGraphRequest -MockWith { @( [pscustomobject]@{ displayName='Old'; name='old'; webUrl='https://contoso.sharepoint.com/sites/old' id='contoso.sharepoint.com,1,1'; createdDateTime='2020-01-01T00:00:00Z' siteCollection=[pscustomobject]@{ hostname='contoso.sharepoint.com' } }, [pscustomobject]@{ displayName='New'; name='new'; webUrl='https://contoso.sharepoint.com/sites/new' id='contoso.sharepoint.com,2,2'; createdDateTime='2026-09-25T00:00:00Z' siteCollection=[pscustomobject]@{ hostname='contoso.sharepoint.com' } }, [pscustomobject]@{ displayName='Undated'; name='undated'; webUrl='https://contoso.sharepoint.com/sites/undated' id='contoso.sharepoint.com,3,3'; createdDateTime=$null siteCollection=[pscustomobject]@{ hostname='contoso.sharepoint.com' } } ) } } } It 'returns only sites created on or after the date' { $r = InModuleScope msec { Get-MsecSharePointSite -CreatedSince ([datetime]'2026-01-01') -WarningAction SilentlyContinue } @($r).DisplayName | Should -Contain 'New' @($r).DisplayName | Should -Not -Contain 'Old' } It 'includes a site with no creation date rather than dropping it' { # ABSENT IS NOT OLD. A site nobody can date is exactly the one worth looking at, so a # date filter must not silently remove it from a review worklist. $r = InModuleScope msec { Get-MsecSharePointSite -CreatedSince ([datetime]'2026-01-01') -WarningAction SilentlyContinue } @($r).DisplayName | Should -Contain 'Undated' } It 'warns about the undated site, so its inclusion is not silent' { $w = InModuleScope msec { $null = Get-MsecSharePointSite -CreatedSince ([datetime]'2026-01-01') -WarningVariable wv -WarningAction SilentlyContinue ($wv -join ' ') } $w | Should -BeLike '*undated*' } It 'filters nothing when the parameter is omitted' { $r = InModuleScope msec { Get-MsecSharePointSite } @($r).Count | Should -Be 3 } } Describe 'Get-MsecSharePointSite -IncludeSettings' { BeforeEach { InModuleScope msec { Mock Assert-MsecSession -MockWith { } Mock Invoke-MsecGraphRequest -MockWith { @([pscustomobject]@{ displayName = 'Contoso Translations'; name = 'ContosoTranslations' webUrl = 'https://contoso.sharepoint.com/sites/ContosoTranslations' id = 'contoso.sharepoint.com,11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222' createdDateTime = '2025-01-27T15:16:23Z'; lastModifiedDateTime = '2026-06-04T17:13:39Z' siteCollection = [pscustomobject]@{ hostname = 'contoso.sharepoint.com' } }) } Mock Get-MsecAccessToken -MockWith { 'token' } } } It 'does not call the site API unless asked' { # One request per site; on a large tenant that is the difference between seconds and # minutes, so it stays opt-in. InModuleScope msec { Mock Invoke-MsecRetryableRequest -MockWith { @{} } $null = Get-MsecSharePointSite Should -Invoke Invoke-MsecRetryableRequest -Times 0 } } It 'projects the site settings when asked' { $r = InModuleScope msec { Mock Invoke-MsecRetryableRequest -MockWith { @{ ShareByEmailEnabled = $true; ShareByLinkEnabled = $false DisableCompanyWideSharingLinks = $false; ExternalSharingTipsEnabled = $true IsRestrictedAccessControlPolicyEnforcedOnSite = $false SensitivityLabelId = '00000000-0000-0000-0000-000000000000' GroupId = '00000000-0000-0000-0000-000000000000'; ReadOnly = $false } } Get-MsecSharePointSite -IncludeSettings } $r.Settings | Should -Not -BeNullOrEmpty $r.Settings.ShareByEmailEnabled | Should -BeTrue $r.Settings.ShareByLinkEnabled | Should -BeFalse # All-zero GUIDs are "not set", not a value worth reporting. $r.Settings.SensitivityLabelId | Should -BeNullOrEmpty $r.Settings.GroupId | Should -BeNullOrEmpty } It 'leaves Settings $null when the site could not be read, never a blank object' { # THE BUG THIS PREVENTS. A blank object reads as "every sharing flag is off", which is # a confident wrong answer about a site nobody could actually query. $r = InModuleScope msec { Mock Invoke-MsecRetryableRequest -MockWith { throw 'Response status code does not indicate success: 403 (Forbidden).' } Get-MsecSharePointSite -IncludeSettings -WarningAction SilentlyContinue } $r.Settings | Should -BeNullOrEmpty $r.WebUrl | Should -Be 'https://contoso.sharepoint.com/sites/ContosoTranslations' } It 'warns when a site could not be read, so a gap is not silent' { $w = InModuleScope msec { Mock Invoke-MsecRetryableRequest -MockWith { throw 'boom' } $null = Get-MsecSharePointSite -IncludeSettings -WarningVariable wv -WarningAction SilentlyContinue ($wv -join ' ') } $w | Should -BeLike '*ContosoTranslations*' } It 'never claims to carry the sharing level or the domain list' { # Those need Sites.FullControl.All - a write permission msec will not hold - so no # property here may be named as though it answered them. $names = InModuleScope msec { Mock Invoke-MsecRetryableRequest -MockWith { @{ ShareByEmailEnabled = $true; SharingCapability = 'ExternalUserSharingOnly' SharingAllowedDomainList = 'contoso.com' } } @((Get-MsecSharePointSite -IncludeSettings).Settings.PSObject.Properties.Name) } $names | Should -Not -Contain 'SharingCapability' $names | Should -Not -Contain 'SharingAllowedDomainList' $names | Should -Contain 'ShareByEmailEnabled' } } |