tests/Get-MsecContext.Tests.ps1
|
#Requires -Module Pester # # Tests for Get-MsecContext. # # The traps: # # A STALE SESSION IS THE FAILURE, NOT A MISSING ONE. An absent session fails loudly; one # left on the tenant you switched away from SUCCEEDS and returns the wrong tenant's data. # The Aligned column is the reason the command exists, so it is what these tests cover. # # "NOT CONNECTED" AND "CANNOT TELL" MUST NOT COLLAPSE. ModuleNotLoaded and Unknown exist so # that "I did not look" never renders as "there is nothing there" - which is precisely what # would hide a stale session. # # THE GRAPH SDK AND THE WRITE SESSION SHARE Get-MgContext, told apart by AuthType. Reading # one result as both would report a write session that does not exist. BeforeAll { Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop $script:StubbedAz = @() $stubs = @{ 'Get-AzContext' = { [CmdletBinding()] param() } # Mocked by the app-only-vs-write-session test. Not a dependency of msec, and NOT # installed on the CI runners. 'Get-MgContext' = { [CmdletBinding()] param() } } foreach ($c in $stubs.Keys) { # DISCOVERABLE IS NOT USABLE. On the ubuntu and windows runners Get-MgContext RESOLVES # as a command - the module manifest is there - while its backing assembly # (Microsoft.Graph.Authentication.Core) cannot be loaded. A presence check therefore # skips the stub, and Pester's Mock then dies building CommandMetadata: # "Cannot convert value Get-MgContext to type CommandMetadata" # which points at the mock rather than at the half-installed module behind it. macOS # has the module properly installed, so this passes locally and fails only in CI. # Same probe Mock performs: build the metadata, and stub when that fails. $usable = $false $found = Get-Command $c -ErrorAction SilentlyContinue if ($found) { try { $null = [System.Management.Automation.CommandMetadata]::new($found); $usable = $true } catch { $usable = $false } } if (-not $usable) { Set-Item "function:global:$c" -Value $stubs[$c] -Force $script:StubbedAz += $c } } } AfterAll { foreach ($c in $script:StubbedAz) { Remove-Item "function:$c" -ErrorAction SilentlyContinue } Remove-Module Msec -Force -ErrorAction SilentlyContinue } Describe 'Get-MsecContext' { BeforeEach { InModuleScope Msec { Mock Get-AzContext -MockWith { [pscustomobject]@{ Account = [pscustomobject]@{ Id = 'me@contoso.com' } Tenant = [pscustomobject]@{ Id = 'tenant-1' } Subscription = [pscustomobject]@{ Name = 'PROD' } } } } } It 'flags a session bound to a different tenant than the Azure context' { # THE WHOLE POINT. Get-EXOMailbox against last tenant's connection does not complain - # it returns correct-looking data from the wrong place. $rows = InModuleScope Msec { $script:MsecSession = @{ TenantId = 'tenant-1'; ClientId = 'cid'; KeyVaultName = 'kv' } $script:MsecAdminSession = [pscustomobject]@{ Account = 'admin@contoso.com' TenantId = 'OTHER-TENANT'; GrantedScope = @('a','b') } Get-MsecContext } ($rows | Where-Object Surface -eq 'App (Connect-Msec)').Aligned | Should -Be 'Aligned' ($rows | Where-Object Surface -like 'Write*').Aligned | Should -Be 'DIFFERENT' ($rows | Where-Object Surface -like 'Write*').TenantId | Should -Be 'OTHER-TENANT' } It 'reports a module that is not loaded as such, not as "not connected"' { # "I did not look" and "there is nothing there" are different answers, and conflating # them is what makes a stale session invisible. $rows = InModuleScope Msec { $script:MsecSession = @{ TenantId = 'tenant-1'; ClientId = 'cid'; KeyVaultName = 'kv' } $script:MsecAdminSession = $null Mock Get-Module -MockWith { $null } Get-MsecContext -IncludeWorkload } $exo = $rows | Where-Object Surface -eq 'Exchange Online' $exo.Status | Should -Be 'ModuleNotLoaded' $exo.Status | Should -Not -Be 'NotConnected' } It 'never reports Teams as connected or disconnected, because it cannot cheaply tell' { # Get-CsTenant is a round trip. A command built to end guesses about session state # must not make one itself. $teams = InModuleScope Msec { $script:MsecSession = @{ TenantId = 'tenant-1'; ClientId = 'cid'; KeyVaultName = 'kv' } Mock Get-Module -MockWith { [pscustomobject]@{ Name = 'MicrosoftTeams' } } -ParameterFilter { $Name -eq 'MicrosoftTeams' } Mock Get-Module -MockWith { $null } (Get-MsecContext -IncludeWorkload | Where-Object Surface -eq 'Teams') } $teams.Status | Should -Be 'Unknown' } It 'does not claim a write session from an app-only Graph context' { # Both read Get-MgContext; AuthType is the only thing separating them. $rows = InModuleScope Msec { $script:MsecSession = @{ TenantId = 'tenant-1'; ClientId = 'cid'; KeyVaultName = 'kv' } $script:MsecAdminSession = $null Mock Get-Module -MockWith { [pscustomobject]@{ Name = 'Microsoft.Graph.Authentication' } } ` -ParameterFilter { $Name -eq 'Microsoft.Graph.Authentication' } Mock Get-Module -MockWith { $null } Mock Get-MgContext -MockWith { [pscustomobject]@{ AuthType = 'AppOnly'; ClientId = 'cid'; TenantId = 'tenant-1' } } Get-MsecContext -IncludeWorkload } ($rows | Where-Object Surface -like 'Write*').Status | Should -Be 'NotConnected' ($rows | Where-Object Surface -like 'Graph SDK*').Status | Should -Be 'Connected' } It 'omits the workload rows unless asked, because probing costs other modules' { $rows = InModuleScope Msec { $script:MsecSession = @{ TenantId = 'tenant-1'; ClientId = 'cid'; KeyVaultName = 'kv' } Get-MsecContext } @($rows).Count | Should -Be 3 @($rows.Surface) | Should -Not -Contain 'Exchange Online' } } |