tests/functions/Get-MtUser.Tests.ps1
|
Describe 'Get-MtUser' { BeforeAll { Import-Module $PSScriptRoot/../../Maester.psd1 -Force # GUIDs used as test fixtures. Also available as literals in ParameterFilter blocks. $script:smallGroupId = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" # small/valid EA group $script:largeGroupId = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" # large/wrong group $script:dummyGroupId = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee" # second group for detection # The detection algorithm requires exactly 2 candidate groups (PossibleEmergencyAccessGroups.Count -eq 2). # Each helper creates policies ensuring at least 2 distinct groups appear in excludeGroups. # # Non-tied case: primary group excluded from all 3 policies, dummy only from 2. # → primary count=3, dummy count=2 → primary is selected alone. # # Tied case: both groups excluded from all 3 policies. # → both count=3 → both are selected (triggering the size-guard logic). function New-MockCaPolicy { param([string[]]$ExcludeGroups = @(), [string[]]$ExcludeUsers = @()) [PSCustomObject]@{ id = [guid]::NewGuid().ToString() state = 'enabled' conditions = [PSCustomObject]@{ applications = [PSCustomObject]@{ includeApplications = @('All') includeAuthenticationContextClassReferences = $null } users = [PSCustomObject]@{ includeUsers = @('All') excludeUsers = $ExcludeUsers excludeGroups = $ExcludeGroups } } } } function New-MockMembers { param([int]$Count) 1..$Count | ForEach-Object { @{ id = [guid]::NewGuid().ToString(); userPrincipalName = "user$_@contoso.com"; userType = 'Member' } } } # Wraps members in the raw Graph response shape that Invoke-MtGraphRequest returns when -DisablePaging is used. # Format-Result passes the raw wrapper through (RawOutput=$true), so production code must extract .value. function New-MockGroupMembersResponse { param([int]$Count) @{ '@odata.context' = 'https://graph.microsoft.com/v1.0/$metadata#directoryObjects' 'value' = @(New-MockMembers -Count $Count) } } } Context 'When CA policies have no group exclusions' { BeforeAll { $policies = @((New-MockCaPolicy), (New-MockCaPolicy)) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { return $null } } It 'Should return an empty result without calling the members endpoint' { $result = Get-MtUser -UserType EmergencyAccess $result | Should -BeNullOrEmpty Should -Invoke Invoke-MtGraphRequest -ModuleName Maester -Exactly 0 -ParameterFilter { $RelativeUri -like 'groups/*/members' } } } Context 'When the auto-detected group is small (valid emergency access group)' { BeforeAll { # Primary (small) in all 3 policies, dummy only in 2 — primary wins detection $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -like 'groups/*/members') { return New-MockGroupMembersResponse -Count 2 } } } It 'Should return members and mark them as EmergencyAccess' { $result = Get-MtUser -UserType EmergencyAccess $result | Should -Not -BeNullOrEmpty $result | ForEach-Object { $_.userType | Should -Be 'EmergencyAccess' } } It 'Should call the members endpoint for the correct group' { $null = Get-MtUser -UserType EmergencyAccess # Use inline literal GUID to avoid ParameterFilter closure-scope issues Should -Invoke Invoke-MtGraphRequest -ModuleName Maester -ParameterFilter { $RelativeUri -eq 'groups/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/members' } } } Context 'When the auto-detected group has more than 20 members (large / wrong group)' { BeforeAll { # Large group in all 3 policies, dummy only in 2 — large group is detected $policies = @( New-MockCaPolicy -ExcludeGroups @($script:largeGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:largeGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:largeGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -like 'groups/*/members') { return New-MockGroupMembersResponse -Count 25 } } } It 'Should return an empty result when the detected group is too large' { $result = Get-MtUser -UserType EmergencyAccess $result | Should -BeNullOrEmpty } It 'Should issue a warning when skipping a large group' { $null = Get-MtUser -UserType EmergencyAccess -WarningVariable warnings $warnings | Should -Not -BeNullOrEmpty # Warning must reference the rejected group GUID (inline literal to avoid scope issue) $warnings[0] | Should -Match 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb' } } Context 'When two groups are tied in CA policy exclusion count (one large, one small)' { BeforeAll { # Both excluded from all 3 policies — tied, so both are selected $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:largeGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:largeGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:largeGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -eq 'groups/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/members') { return New-MockGroupMembersResponse -Count 2 } if ($RelativeUri -eq 'groups/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/members') { return New-MockGroupMembersResponse -Count 50 } } } It 'Should return members only from the small group' { # -Count 5 ensures we retrieve all small group members without early exit $result = Get-MtUser -UserType EmergencyAccess -Count 5 $result | Should -Not -BeNullOrEmpty # Large group has 50 members which is rejected; only 2 from the small group should be here $result.Count | Should -Be 2 } It 'Should call the large group members endpoint (but reject the results)' { $null = Get-MtUser -UserType EmergencyAccess -Count 5 Should -Invoke Invoke-MtGraphRequest -ModuleName Maester -ParameterFilter { $RelativeUri -eq 'groups/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/members' } } } Context 'When group members endpoint is called with -DisablePaging' { BeforeAll { $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { return New-MockGroupMembersResponse -Count 2 } } It 'Should pass -DisablePaging to Invoke-MtGraphRequest to prevent infinite paging loops' { $null = Get-MtUser -UserType EmergencyAccess Should -Invoke Invoke-MtGraphRequest -ModuleName Maester -ParameterFilter { $RelativeUri -like 'groups/*/members' -and $DisablePaging -eq $true } } } Context 'When -Count parameter is specified' { BeforeAll { $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -like 'groups/*/members') { return New-MockGroupMembersResponse -Count 5 } } } It 'Should return no more users than the -Count limit' { $result = Get-MtUser -UserType EmergencyAccess -Count 3 $result | Should -Not -BeNullOrEmpty $result.Count | Should -BeLessOrEqual 3 } } Context 'When an error occurs fetching group members' { BeforeAll { $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -like 'groups/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/members') { throw "Graph API error: group not found" } } } It 'Should issue a warning that contains the group GUID (not a user GUID)' { $null = Get-MtUser -UserType EmergencyAccess -WarningVariable warnings $warnings | Should -Not -BeNullOrEmpty $warnings[0] | Should -Match 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' } It 'Should not throw when the Graph call fails' { { Get-MtUser -UserType EmergencyAccess } | Should -Not -Throw } } Context 'When a null or empty group GUID reaches the fetch loop' { BeforeAll { # Simulate the edge case where Group-Object produces an empty-string bucket $policies = @( New-MockCaPolicy -ExcludeGroups @('', $script:smallGroupId) New-MockCaPolicy -ExcludeGroups @('', $script:smallGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { return New-MockGroupMembersResponse -Count 2 } } It 'Should never call groups//members with an empty GUID segment' { $null = Get-MtUser -UserType EmergencyAccess Should -Invoke Invoke-MtGraphRequest -ModuleName Maester -Exactly 0 -ParameterFilter { $RelativeUri -like 'groups//members' } } } Context 'When using BreakGlass alias for UserType' { BeforeAll { $policies = @( New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId, $script:dummyGroupId) New-MockCaPolicy -ExcludeGroups @($script:smallGroupId) ) Mock -ModuleName Maester Get-MtConditionalAccessPolicy { return $policies } Mock -ModuleName Maester Invoke-MtGraphRequest { if ($RelativeUri -like 'groups/*/members') { return New-MockGroupMembersResponse -Count 2 } } } It 'Should return results and set userType to EmergencyAccess (function hardcodes this value)' { $result = Get-MtUser -UserType BreakGlass $result | Should -Not -BeNullOrEmpty # The function always sets userType = "EmergencyAccess" regardless of -UserType parameter value $result | ForEach-Object { $_.userType | Should -Be 'EmergencyAccess' } } } } |