Tests/ConvertTo-MsecAssignmentTarget.Tests.ps1
|
#Requires -Module Pester # # Tests for the Intune assignment-target projection. The cases that matter are the ones a # count cannot express and a naive projection would misreport: an exclusion carved out of a # tenant-wide assignment, and a filter narrowing one. Both make a policy apply to fewer # people than its headline target claims, and both used to be invisible. # # Every column here is typed rather than a rendered phrase, so the assertions are on exact # values - which is the property that lets callers use -contains instead of -match. BeforeAll { $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1' Import-Module $modulePath -Force -ErrorAction Stop # Builds one assignment the way Graph expands it onto a policy. Passed to # InModuleScope as TEXT and rebuilt there: a scriptblock stays bound to the session # state it was written in, and would not resolve the module's private functions. $script:MakeText = @' $script:MkTarget = { param([string] $Type, [string] $GroupId, [string] $FilterType) $t = @{ '@odata.type' = "#microsoft.graph.$Type" } if ($GroupId) { $t['groupId'] = $GroupId } if ($FilterType) { $t['deviceAndAppManagementAssignmentFilterType'] = $FilterType $t['deviceAndAppManagementAssignmentFilterId'] = 'flt-1' } [pscustomobject]@{ id = "as-$Type-$GroupId"; target = [pscustomobject]$t } } '@ } AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue } Describe 'ConvertTo-MsecAssignmentTarget' { It 'returns empty arrays, not $null, for an unassigned policy' { # So a caller can pipe straight into -contains or .Count without a null check. $r = InModuleScope msec { ConvertTo-MsecAssignmentTarget @() } @($r.Type).Count | Should -Be 0 @($r.Group).Count | Should -Be 0 @($r.ExcludedGroup).Count | Should -Be 0 @($r.Detail).Count | Should -Be 0 $r.HasFilter | Should -BeFalse } It 'treats $null assignments the same as an empty collection' { # Graph omits the property rather than sending [] in some responses. $r = InModuleScope msec { ConvertTo-MsecAssignmentTarget $null } @($r.Type).Count | Should -Be 0 $r.HasFilter | Should -BeFalse } It 'names the two tenant-wide targets and orders them predictably' { $out = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) [pscustomobject]@{ Users = ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'allLicensedUsersAssignmentTarget')) Devices = ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'allDevicesAssignmentTarget')) # Deliberately supplied devices-first. Both = ConvertTo-MsecAssignmentTarget @( (& $script:MkTarget 'allDevicesAssignmentTarget'), (& $script:MkTarget 'allLicensedUsersAssignmentTarget')) } } $out.Users.Type | Should -Be @('AllUsers') $out.Devices.Type | Should -Be @('AllDevices') # Fixed order regardless of the order Graph returned them, so two policies can be # compared without sorting first. $out.Both.Type | Should -Be @('AllUsers', 'AllDevices') # Tenant-wide targets have no group. @($out.Both.Group).Count | Should -Be 0 } It 'lists every included group, and reports the type once' { $out = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) ConvertTo-MsecAssignmentTarget -GroupName @{ 'g1' = 'sg-pilot'; 'g2' = 'sg-broad' } -Assignment @( (& $script:MkTarget 'groupAssignmentTarget' 'g1'), (& $script:MkTarget 'groupAssignmentTarget' 'g2'), (& $script:MkTarget 'groupAssignmentTarget' 'g3')) } # Type is DISTINCT - three group assignments are still one kind of target. $out.Type | Should -Be @('Group') # ...and the multiplicity lives here. $out.Group | Should -Be @('sg-pilot', 'sg-broad', 'g3') @($out.Detail).Count | Should -Be 3 } It 'falls back to the group id when no name was supplied' { # -IncludeAssignmentGroup was not used, or the lookup was forbidden. A target must # never be nameless: the id is what you would act on. $r = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'groupAssignmentTarget' 'g-unknown')) } $r.Group | Should -Be @('g-unknown') $r.Detail.GroupName | Should -Be 'g-unknown' $r.Detail.GroupId | Should -Be 'g-unknown' } It 'keeps excluded groups in their own column, out of the included one' { # THE case a count cannot express: this is 2 assignments, and so is a pair of # unrelated groups. Two parallel lists would also leave the reader guessing which # name was the carve-out - hence a separate column rather than a shared one. $out = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) $names = @{ 'g9' = 'sg-vips'; 'g8' = 'sg-execs' } [pscustomobject]@{ One = ConvertTo-MsecAssignmentTarget -GroupName $names -Assignment @( (& $script:MkTarget 'allLicensedUsersAssignmentTarget'), (& $script:MkTarget 'exclusionGroupAssignmentTarget' 'g9')) Two = ConvertTo-MsecAssignmentTarget -GroupName $names -Assignment @( (& $script:MkTarget 'allLicensedUsersAssignmentTarget'), (& $script:MkTarget 'exclusionGroupAssignmentTarget' 'g9'), (& $script:MkTarget 'exclusionGroupAssignmentTarget' 'g8')) } } $out.One.Type | Should -Be @('AllUsers', 'ExclusionGroup') $out.One.ExcludedGroup | Should -Be @('sg-vips') # An excluded group is NOT an included one. @($out.One.Group).Count | Should -Be 0 $out.Two.ExcludedGroup | Should -Be @('sg-vips', 'sg-execs') ($out.One.Detail | Where-Object IsExclusion).TargetType | Should -Be 'ExclusionGroup' } It 'separates included from excluded when a policy has both' { $r = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) ConvertTo-MsecAssignmentTarget -GroupName @{ 'g1' = 'sg-pilot'; 'g9' = 'sg-vips' } -Assignment @( (& $script:MkTarget 'groupAssignmentTarget' 'g1'), (& $script:MkTarget 'exclusionGroupAssignmentTarget' 'g9')) } # The whole point of two columns: no positional guessing about which is which. $r.Type | Should -Be @('Group', 'ExclusionGroup') $r.Group | Should -Be @('sg-pilot') $r.ExcludedGroup | Should -Be @('sg-vips') } It 'flags a filtered assignment, whose stated target overstates its reach' { $out = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) [pscustomobject]@{ Filtered = ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'allDevicesAssignmentTarget' $null 'include')) Unfiltered = ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'allDevicesAssignmentTarget')) NoneLiteral = ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'allDevicesAssignmentTarget' $null 'none')) } } $out.Filtered.HasFilter | Should -BeTrue $out.Filtered.Detail.FilterType | Should -Be 'include' $out.Filtered.Detail.FilterId | Should -Be 'flt-1' # The type is unaffected - it is still an All Devices assignment, just narrowed. $out.Filtered.Type | Should -Be @('AllDevices') $out.Unfiltered.HasFilter | Should -BeFalse $out.Unfiltered.Detail.FilterType | Should -BeNullOrEmpty # Graph's literal 'none' must normalise to $null so callers test one thing. $out.NoneLiteral.HasFilter | Should -BeFalse $out.NoneLiteral.Detail.FilterType | Should -BeNullOrEmpty } It 'keeps an unrecognised target type rather than flattening it' { # Intune has added target types before. A name you can look up beats a label that # hides one, and it sorts after the kinds msec does know. $r = InModuleScope msec { ConvertTo-MsecAssignmentTarget @( [pscustomobject]@{ target = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.allDevicesAssignmentTarget' } }, [pscustomobject]@{ target = [pscustomobject]@{ '@odata.type' = '#microsoft.graph.futureThingTarget' } }) } $r.Type | Should -Be @('AllDevices', 'futureThingTarget') ($r.Detail | Where-Object TargetType -eq 'futureThingTarget').IsExclusion | Should -BeFalse } It 'reports Unknown only when there is no @odata.type at all' { # Here 'Unknown' is the accurate description rather than a shrug - and the # assignment is still counted, so the columns cannot disagree with # AssignmentCount. $r = InModuleScope msec { ConvertTo-MsecAssignmentTarget @([pscustomobject]@{ target = [pscustomobject]@{} }) } $r.Type | Should -Be @('Unknown') @($r.Detail).Count | Should -Be 1 $r.Detail.TargetType | Should -Be 'Unknown' } It 'maps a ConfigMgr collection target, which has no group' { $r = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'configurationManagerCollectionAssignmentTarget')) } $r.Type | Should -Be @('ConfigManagerCollection') @($r.Group).Count | Should -Be 0 } It 'keeps the verbatim assignment on every detail row' { $r = InModuleScope msec -Parameters @{ MakeText = $script:MakeText } { param($MakeText) & ([scriptblock]::Create($MakeText)) ConvertTo-MsecAssignmentTarget @((& $script:MkTarget 'groupAssignmentTarget' 'g1')) } $r.Detail.Raw.id | Should -Be 'as-groupAssignmentTarget-g1' $r.Detail.Raw.target.groupId | Should -Be 'g1' } } |