tests/MsecTenantProfile.Tests.ps1

#Requires -Module Pester
#
# Tests for the tenant connection profile - what makes switching Azure context also reconnect
# the msec app session.
#
# The point of the feature is that msec runs on TWO identities: the Az context is you, the
# msec session is the app registration. Switching one used to leave the other pointing at the
# tenant you just left, so every Graph call kept answering for the wrong tenant and all you
# got was a warning.
#
# What must hold:
# * a profile stores CONFIGURATION only - vault, client id, cert name. No secret, because
# there isn't one: signing happens inside Key Vault and the private key never leaves it.
# * profiles are per tenant and can never overwrite each other
# * a missing, unreadable or incomplete profile is silently "no profile", never an error in
# the middle of a context switch
# * a failed reconnect must not fail the context switch that was actually asked for

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

    $script:CacheDir = Join-Path ([System.IO.Path]::GetTempPath()) "msec-profile-$([guid]::NewGuid().Guid)"
    $env:MSEC_CACHE_DIR = $script:CacheDir
}

AfterAll {
    Remove-Item env:MSEC_CACHE_DIR -ErrorAction SilentlyContinue
    if ($script:CacheDir -and (Test-Path $script:CacheDir)) {
        Remove-Item $script:CacheDir -Recurse -Force -ErrorAction SilentlyContinue
    }
    Remove-Module Msec -Force -ErrorAction SilentlyContinue
}

Describe 'tenant connection profile' {

    It 'round-trips, and keeps tenants apart' {
        InModuleScope Msec {
            Save-MsecTenantProfile -TenantId 'tenant-a' -KeyVaultName 'kv-a' -ClientId 'client-a'
            Save-MsecTenantProfile -TenantId 'tenant-b' -KeyVaultName 'kv-b' -ClientId 'client-b' -CertificateName 'other-cert'

            $a = Get-MsecTenantProfile -TenantId 'tenant-a'
            $b = Get-MsecTenantProfile -TenantId 'tenant-b'

            $a.KeyVaultName    | Should -Be 'kv-a'
            $a.ClientId        | Should -Be 'client-a'
            $a.CertificateName | Should -Be 'msec-app'      # the default, recorded explicitly

            # Saving B must not have touched A - the whole reason profiles are per-tenant
            # folders rather than one shared file.
            $b.KeyVaultName    | Should -Be 'kv-b'
            $b.CertificateName | Should -Be 'other-cert'
            $a.KeyVaultName    | Should -Be 'kv-a'
        }
    }

    It 'stores no secret' {
        InModuleScope Msec {
            Save-MsecTenantProfile -TenantId 'tenant-nokeys' -KeyVaultName 'kv' -ClientId 'client'
            $raw = Get-Content -LiteralPath (Get-MsecCachePath -Name 'profile' -TenantId 'tenant-nokeys') -Raw

            # Signing happens inside Key Vault, so there is nothing secret to leak here - but
            # a future change that started caching key material would be a real problem, and
            # this is where it would show up.
            $raw | Should -Not -Match '(?i)privatekey|thumbprint|secret|password|-----BEGIN'
        }
    }

    It 'treats a missing, malformed or incomplete profile as no profile' {
        InModuleScope Msec {
            Get-MsecTenantProfile -TenantId 'tenant-never-seen' | Should -BeNullOrEmpty

            # Hand-edited into nonsense.
            $path = Get-MsecCachePath -Name 'profile' -TenantId 'tenant-broken'
            New-Item -Path (Split-Path $path -Parent) -ItemType Directory -Force | Out-Null
            Set-Content -LiteralPath $path -Value '{ not json at all'
            Get-MsecTenantProfile -TenantId 'tenant-broken' | Should -BeNullOrEmpty

            # Valid JSON, but without what Connect-Msec needs - e.g. written by an older
            # version. Ignored rather than turned into a failure mid-switch.
            $path = Get-MsecCachePath -Name 'profile' -TenantId 'tenant-partial'
            New-Item -Path (Split-Path $path -Parent) -ItemType Directory -Force | Out-Null
            Set-Content -LiteralPath $path -Value '{ "TenantId": "tenant-partial" }'
            Get-MsecTenantProfile -TenantId 'tenant-partial' | Should -BeNullOrEmpty
        }
    }

    It 'never fails the caller when it cannot be saved' {
        InModuleScope Msec {
            Mock Set-Content -MockWith { throw 'read-only volume' }
            # Losing auto-reconnect must not cost the session that just connected.
            { Save-MsecTenantProfile -TenantId 'tenant-ro' -KeyVaultName 'kv' -ClientId 'c' } |
                Should -Not -Throw
        }
    }
}

Describe 'Set-MsecContext reconnect' {

    BeforeEach {
        InModuleScope Msec { $script:MsecSession = $null }
    }

    # The four auto-reconnect tests that used to live here went with Set-MsecContext.
    # That command carried the "switch context, app session follows" behaviour; Set-MsecContext
    # replaces it and is covered by MsecContextStore.Tests.ps1. The profile itself remains -
    # Connect-MsecAdmin reads the admin account from it, Save-MsecContext the certificate name.
}