Tests/Get-MsecEntraConditionalAccessPolicy.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecEntraConditionalAccessPolicy. Verifies the deeply-nested # Graph response (conditions.users.*, conditions.applications.*, grantControls.*) # is flattened to the documented PSCustomObject shape, with array fields always # coerced to actual arrays (never $null) so Where-Object -contains works. BeforeAll { $modulePath = Join-Path $PSScriptRoot '..' 'Msec.psm1' Import-Module $modulePath -Force -ErrorAction Stop $script:TestThumbBytes = [byte[]](1..20) } AfterAll { Remove-Module Msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecEntraConditionalAccessPolicy' { BeforeEach { InModuleScope Msec -Parameters @{ Thumb = $script:TestThumbBytes } { param($Thumb) $script:MsecSession = @{ TenantId = 'tenant' ClientId = 'client' KeyVaultName = 'kv-test' KeyName = 'msec-app' ThumbprintBytes = $Thumb Tokens = @{} } } } It 'flattens conditions/grantControls to top-level columns and projects arrays as arrays' { $rows = InModuleScope Msec { Mock Invoke-MsecKeyVaultSign -MockWith { [byte[]](1..10) } Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'oauth2/v2.0/token' } -MockWith { [pscustomobject]@{ access_token = 'mock'; expires_in = 3600 } } Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/identity/conditionalAccess/policies' } -MockWith { [pscustomobject]@{ value = @( [pscustomobject]@{ id = 'pol-1' displayName = 'Require MFA for all admins' state = 'enabled' createdDateTime = '2024-08-12T10:00:00Z' modifiedDateTime = '2026-04-22T13:00:00Z' conditions = [pscustomobject]@{ users = [pscustomobject]@{ includeUsers = @('All') excludeUsers = @('break-glass-guid') includeGroups = @('admins-group-guid') excludeGroups = @() includeRoles = @('global-admin-role-guid') excludeRoles = @() } applications = [pscustomobject]@{ includeApplications = @('All') excludeApplications = @() includeUserActions = @() } platforms = [pscustomobject]@{ includePlatforms = @('all') excludePlatforms = @() } locations = [pscustomobject]@{ includeLocations = @('All') excludeLocations = @('trusted-ip-loc-guid') } clientAppTypes = @('all') signInRiskLevels = @('high','medium') userRiskLevels = @() } grantControls = [pscustomobject]@{ operator = 'OR' builtInControls = @('mfa','compliantDevice') } } [pscustomobject]@{ # A report-only policy: state value distinct from 'enabled'. id = 'pol-2' displayName = 'Block legacy auth (report-only)' state = 'enabledForReportingButNotEnforced' createdDateTime = '2026-02-01T10:00:00Z' modifiedDateTime = '2026-05-01T10:00:00Z' conditions = [pscustomobject]@{ users = [pscustomobject]@{ includeUsers = @('All'); excludeUsers = @() } applications = [pscustomobject]@{ includeApplications = @('All') } # No platforms / locations restriction on this one - those nested # objects can legitimately be $null in the Graph response. } grantControls = [pscustomobject]@{ operator = 'OR' builtInControls = @('block') } } ) } } Get-MsecEntraConditionalAccessPolicy } $rows.Count | Should -Be 2 $p1 = $rows | Where-Object Id -eq 'pol-1' $p1.DisplayName | Should -Be 'Require MFA for all admins' $p1.State | Should -Be 'enabled' $p1.CreatedDateTime | Should -BeOfType [datetime] # Nested arrays flatten to top-level columns $p1.IncludedUsers | Should -Be @('All') $p1.ExcludedUsers | Should -Be @('break-glass-guid') $p1.IncludedGroups | Should -Be @('admins-group-guid') $p1.IncludedRoles | Should -Be @('global-admin-role-guid') $p1.IncludedApps | Should -Be @('All') $p1.SignInRiskLevels | Should -Be @('high','medium') $p1.ExcludedLocations | Should -Be @('trusted-ip-loc-guid') # Grant controls $p1.GrantOperator | Should -Be 'OR' $p1.Requires | Should -Contain 'mfa' $p1.Requires | Should -Contain 'compliantDevice' # The audit-friendly query pattern works. ($rows | Where-Object Requires -contains 'mfa').DisplayName | Should -Be 'Require MFA for all admins' # Raw holds the full Graph object so consumers can JSON-export / diff / # access fields that aren't flattened (sessionControls, devices filter, ...). $p1.Raw | Should -Not -BeNullOrEmpty $p1.Raw.id | Should -Be 'pol-1' $p1.Raw.conditions.users.includeUsers | Should -Contain 'All' # And the row is tagged with the PowerShell type that owns the default # display set - so Format-Table doesn't render Raw by default. $p1.PSObject.TypeNames | Should -Contain 'MsecEntraConditionalAccessPolicy' # Report-only policy state surfaces verbatim (so consumer can filter on it) $p2 = $rows | Where-Object Id -eq 'pol-2' $p2.State | Should -Be 'enabledForReportingButNotEnforced' # When the platforms / locations conditions are absent in the Graph # response, the projected columns surface as null-or-empty (PowerShell # unrolls empty arrays on PSCustomObject property access - that's a # language quirk, not a function bug). What matters is the # Where-Object -contains pattern downstream consumers use doesn't blow # up on those policies. $p2.IncludedPlatforms | Should -BeNullOrEmpty { $rows | Where-Object IncludedPlatforms -contains 'iOS' } | Should -Not -Throw # And the policy with no platform restriction is correctly NOT matched by it. ($rows | Where-Object IncludedPlatforms -contains 'iOS').Id | Should -Not -Contain 'pol-2' } It 'rewrites a 403 to mention the missing Policy.Read.All permission' { InModuleScope Msec { Mock Invoke-MsecKeyVaultSign -MockWith { [byte[]](1..10) } Mock Invoke-RestMethod -ParameterFilter { $Uri -match 'oauth2/v2.0/token' } -MockWith { [pscustomobject]@{ access_token = 'mock'; expires_in = 3600 } } Mock Invoke-RestMethod -ParameterFilter { $Uri -match '/identity/conditionalAccess/policies' } -MockWith { throw 'Response status code does not indicate success: 403 (Forbidden).' } { Get-MsecEntraConditionalAccessPolicy } | Should -Throw -ExpectedMessage '*Policy.Read.All*' } } } |