Tests/Select-MsecAzureContext.Tests.ps1

#Requires -Module Pester
#
# Tests for Select-MsecAzureContext: selecting among ALREADY-signed-in Azure contexts
# (Get-AzContext -ListAvailable + Select-AzContext) by subscription name/id, disambiguated
# by -User (account, which implies the tenant). Covers the app-session drift warning,
# dependent tab-completion, and clear errors.

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'Msec.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module Msec -Force -ErrorAction SilentlyContinue
}

Describe 'Select-MsecAzureContext' {

    # Saved contexts: 'shared' is signed in under two different accounts (one in another
    # tenant/cloud), so it needs -User to disambiguate; the rest are unique.
    BeforeAll {
        $script:Ctxs = @(
            [pscustomobject]@{ Name = 'ctx-dev';    Subscription = [pscustomobject]@{ Id = 'sub-1'; Name = 'dev' };      Tenant = [pscustomobject]@{ Id = 't1' }; Environment = 'AzureCloud';      Account = [pscustomobject]@{ Id = 'admin@contoso.com' } }
            [pscustomobject]@{ Name = 'ctx-prod';   Subscription = [pscustomobject]@{ Id = 'sub-2'; Name = 'prod app' }; Tenant = [pscustomobject]@{ Id = 't1' }; Environment = 'AzureCloud';      Account = [pscustomobject]@{ Id = 'admin@contoso.com' } }
            [pscustomobject]@{ Name = 'ctx-cn';     Subscription = [pscustomobject]@{ Id = 'sub-3'; Name = 'shared' };   Tenant = [pscustomobject]@{ Id = 't2' }; Environment = 'AzureChinaCloud'; Account = [pscustomobject]@{ Id = 'cn@partner.cn' } }
            [pscustomobject]@{ Name = 'ctx-shared'; Subscription = [pscustomobject]@{ Id = 'sub-4'; Name = 'shared' };   Tenant = [pscustomobject]@{ Id = 't1' }; Environment = 'AzureCloud';      Account = [pscustomobject]@{ Id = 'admin@contoso.com' } }
        )
    }

    It 'switches to a saved context matched by subscription id' {
        $captured = InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            $script:CapturedSub = $null
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { $script:CapturedSub = $InputObject.Subscription.Id }
            $script:MsecSession = $null

            $r = Select-MsecAzureContext -Subscription 'sub-1'
            [pscustomobject]@{ Captured = $script:CapturedSub; Result = $r }
        }

        $captured.Captured              | Should -Be 'sub-1'
        $captured.Result.SubscriptionId | Should -Be 'sub-1'
        $captured.Result.Account        | Should -Be 'admin@contoso.com'
    }

    It 'switches to a saved context matched by subscription name when it is unique' {
        $sub = InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            $script:CapturedSub = $null
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { $script:CapturedSub = $InputObject.Subscription.Id }
            $script:MsecSession = $null

            $null = Select-MsecAzureContext -Subscription 'prod app'
            $script:CapturedSub
        }
        $sub | Should -Be 'sub-2'
    }

    It 'disambiguates a shared subscription name by -User (which carries the tenant/cloud)' {
        $captured = InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            $script:CapturedSub = $null
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { $script:CapturedSub = $InputObject.Subscription.Id }
            $script:MsecSession = $null

            $r = Select-MsecAzureContext -Subscription 'shared' -User 'cn@partner.cn'
            [pscustomobject]@{ Captured = $script:CapturedSub; Result = $r }
        }

        $captured.Captured           | Should -Be 'sub-3'        # the cn@partner.cn context
        $captured.Result.TenantId    | Should -Be 't2'
        $captured.Result.Environment | Should -Be 'AzureChinaCloud'
    }

    It 'throws, naming the accounts, when a shared subscription name has no -User' {
        InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { }

            { Select-MsecAzureContext -Subscription 'shared' } |
                Should -Throw -ExpectedMessage '*signed in under more than one account*'
        }
    }

    It 'throws a clear error when no saved context matches' {
        InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { }

            { Select-MsecAzureContext -Subscription 'nope' } |
                Should -Throw -ExpectedMessage "*No signed-in context matches subscription 'nope'*"
        }
    }

    It 'warns when the chosen context tenant/cloud differs from the msec app session' {
        $warnings = InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { }
            # App session pinned to commercial / t1; we switch into the t2 / China context.
            $script:MsecSession = @{ TenantId = 't1'; Endpoints = [pscustomobject]@{ EnvironmentName = 'AzureCloud' } }

            Select-MsecAzureContext -Subscription 'shared' -User 'cn@partner.cn' -WarningVariable w -WarningAction SilentlyContinue | Out-Null
            $w
        }
        ($warnings -join ' ') | Should -Match 'Connect-Msec again'
    }

    It 'does NOT warn when the chosen context tenant and cloud still match the session' {
        $warnings = InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { }
            $script:MsecSession = @{ TenantId = 't1'; Endpoints = [pscustomobject]@{ EnvironmentName = 'AzureCloud' } }

            Select-MsecAzureContext -Subscription 'dev' -WarningVariable w -WarningAction SilentlyContinue | Out-Null
            $w
        }
        $warnings | Should -BeNullOrEmpty
    }

    It 'does NOT warn when .Environment is an object equal to the session cloud (regression)' {
        # A real saved context exposes .Environment as a PSAzureEnvironment OBJECT, not a
        # string. Comparing the object directly to the session cloud string is always "not
        # equal", which fired a false drift warning even when tenant + cloud matched.
        $cloud = [pscustomobject]@{ Name = 'AzureCloud' }
        $cloud | Add-Member -MemberType ScriptMethod -Name ToString -Value { 'AzureCloud' } -Force
        $ctx = [pscustomobject]@{
            Name = 'ctx-x'; Subscription = [pscustomobject]@{ Id = 'sub-x'; Name = 'prod' }
            Tenant = [pscustomobject]@{ Id = 't1' }; Environment = $cloud
            Account = [pscustomobject]@{ Id = 'admin@contoso.com' }
        }

        $warnings = InModuleScope Msec -Parameters @{ Ctx = $ctx } {
            param($Ctx)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { @($Ctx) }
            Mock Select-AzContext -RemoveParameterType 'InputObject' -MockWith { }
            $script:MsecSession = @{ TenantId = 't1'; Endpoints = [pscustomobject]@{ EnvironmentName = 'AzureCloud' } }

            Select-MsecAzureContext -Subscription 'prod' -WarningVariable w -WarningAction SilentlyContinue | Out-Null
            $w
        }
        $warnings | Should -BeNullOrEmpty
    }

    It 'throws when there are no saved contexts at all' {
        InModuleScope Msec {
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { @() }
            { Select-MsecAzureContext -Subscription 'dev' } |
                Should -Throw -ExpectedMessage '*No saved Azure contexts*'
        }
    }

    It '-Subscription tab-completes DISTINCT subscription names, with accounts in the tooltip' {
        InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }

            $sb = (Get-Command Select-MsecAzureContext).Parameters['Subscription'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ArgumentCompleterAttribute] } |
                Select-Object -ExpandProperty ScriptBlock

            $completions = & $sb $null 'Subscription' '' $null @{}

            $completions.ListItemText | Should -Contain 'dev'
            $completions.ListItemText | Should -Contain 'prod app'
            # 'shared' is two contexts but appears once (distinct names).
            @($completions | Where-Object ListItemText -eq 'shared').Count | Should -Be 1
            # Its tooltip lists both accounts so the user knows -User will be needed.
            (($completions | Where-Object ListItemText -eq 'shared').ToolTip) | Should -Match 'cn@partner.cn'
            (($completions | Where-Object ListItemText -eq 'shared').ToolTip) | Should -Match 'admin@contoso.com'
        }
    }

    It '-User tab-completes only the accounts available for the chosen -Subscription' {
        InModuleScope Msec -Parameters @{ Ctxs = $script:Ctxs } {
            param($Ctxs)
            Mock Get-AzContext -ParameterFilter { $ListAvailable } -MockWith { $Ctxs }

            $sb = (Get-Command Select-MsecAzureContext).Parameters['User'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ArgumentCompleterAttribute] } |
                Select-Object -ExpandProperty ScriptBlock

            # Dependent on -Subscription 'shared': should offer both of its accounts...
            $shared = & $sb $null 'User' '' $null @{ Subscription = 'shared' }
            $shared.CompletionText | Should -Contain 'cn@partner.cn'
            $shared.CompletionText | Should -Contain 'admin@contoso.com'

            # ...but for 'dev' only that subscription's single account.
            $dev = & $sb $null 'User' '' $null @{ Subscription = 'dev' }
            $dev.CompletionText | Should -Be 'admin@contoso.com'
        }
    }
}