diff --git a/build/Build-MaesterModule.ps1 b/build/Build-MaesterModule.ps1 index 9370c954b..b1f58db05 100644 --- a/build/Build-MaesterModule.ps1 +++ b/build/Build-MaesterModule.ps1 @@ -476,7 +476,7 @@ $__MtSession = @{ ExoCache = @{} OrcaCache = @{} AIAgentInfo = $null - AzureDevOpsConnection = $null + AzureDevOpsConnectionCache = $null DataverseApiBase = $null # Resolved Dataverse OData API base URL (e.g. https://org123.api.crm.dynamics.com/api/data/v9.2) DataverseResourceUrl = $null # Dataverse resource URL for token acquisition (e.g. https://org123.crm.dynamics.com) DataverseEnvironmentId = $null # Environment identifier for display (e.g. org123.crm.dynamics.com) diff --git a/powershell/Maester.psm1 b/powershell/Maester.psm1 index 87e32974a..581231f38 100644 --- a/powershell/Maester.psm1 +++ b/powershell/Maester.psm1 @@ -22,6 +22,7 @@ $__MtSession = @{ ExoCache = @{} OrcaCache = @{} AIAgentInfo = $null + AzureDevOpsConnectionCache = $null DataverseApiBase = $null # Resolved Dataverse OData API base URL (e.g. https://org123.api.crm.dynamics.com/api/data/v9.2) DataverseResourceUrl = $null # Dataverse resource URL for token acquisition (e.g. https://org123.crm.dynamics.com) DataverseEnvironmentId = $null # Environment identifier for display (e.g. org123.crm.dynamics.com) diff --git a/powershell/internal/Clear-ModuleVariable.ps1 b/powershell/internal/Clear-ModuleVariable.ps1 index 13e4adc14..3c3033ec3 100644 --- a/powershell/internal/Clear-ModuleVariable.ps1 +++ b/powershell/internal/Clear-ModuleVariable.ps1 @@ -20,7 +20,7 @@ Clear-MtDnsCache Clear-MtExoCache $__MtSession.AIAgentInfo = $null - $__MtSession.AzureDevOpsConnection = $null + $__MtSession.AzureDevOpsConnectionCache = $null # Invoke-Maester resets the per-run cache but preserves the existing GitHub session. # Disconnect-MtGitHub, including through Disconnect-Maester, owns clearing GitHubConnection and GitHubAuthHeader. $__MtSession.GitHubCache = @{} diff --git a/powershell/public/core/Test-MtConnection.ps1 b/powershell/public/core/Test-MtConnection.ps1 index dccfab5f9..f48d5c2b9 100644 --- a/powershell/public/core/Test-MtConnection.ps1 +++ b/powershell/public/core/Test-MtConnection.ps1 @@ -174,12 +174,12 @@ #region AzureDevOps if ($Service -contains 'AzureDevOps' -or $Service -contains 'All') { $IsConnected = $false - if ($null -ne $__MtSession.AzureDevOpsConnection) { + if ($null -ne $__MtSession.AzureDevOpsConnectionCache) { # Use cached probe result to avoid re-running the connection check on every test. - if ($__MtSession.AzureDevOpsConnection -is [string] -and $__MtSession.AzureDevOpsConnection -eq 'NotConnected') { + if ($__MtSession.AzureDevOpsConnectionCache -is [string] -and $__MtSession.AzureDevOpsConnectionCache -eq 'NotConnected') { $IsConnected = $false } else { - $MtConnections.AzureDevOps = $__MtSession.AzureDevOpsConnection + $MtConnections.AzureDevOps = $__MtSession.AzureDevOpsConnectionCache $IsConnected = $true } } else { @@ -188,7 +188,7 @@ $connection = Get-ADOPSConnection if ($null -ne $connection -and $null -ne $connection['Organization']) { $MtConnections.AzureDevOps = $connection - $__MtSession.AzureDevOpsConnection = $connection + $__MtSession.AzureDevOpsConnectionCache = $connection $IsConnected = $true } } @@ -197,7 +197,7 @@ } if (-not $IsConnected) { # Cache negative result so subsequent calls skip the probe. - $__MtSession.AzureDevOpsConnection = 'NotConnected' + $__MtSession.AzureDevOpsConnectionCache = 'NotConnected' } } Write-Verbose "AzureDevOps: $IsConnected" diff --git a/powershell/tests/functions/Test-MtConnection.Tests.ps1 b/powershell/tests/functions/Test-MtConnection.Tests.ps1 index f0c450ccc..cf3384c6e 100644 --- a/powershell/tests/functions/Test-MtConnection.Tests.ps1 +++ b/powershell/tests/functions/Test-MtConnection.Tests.ps1 @@ -23,14 +23,14 @@ Describe 'Test-MtConnection — GitHub service' { BeforeEach { InModuleScope Maester { $__MtSession.GitHubConnection = $null - $__MtSession.AzureDevOpsConnection = $null + $__MtSession.AzureDevOpsConnectionCache = $null } } AfterEach { InModuleScope Maester { $__MtSession.GitHubConnection = $null - $__MtSession.AzureDevOpsConnection = $null + $__MtSession.AzureDevOpsConnectionCache = $null } } @@ -112,7 +112,7 @@ Describe 'Test-MtConnection — GitHub service' { It 'Returns AllConnected $true when all included services are connected and GitHub session is absent' { InModuleScope Maester { $__MtSession.GitHubConnection = $null - $__MtSession.AzureDevOpsConnection = [PSCustomObject]@{ Organization = 'ado-org' } + $__MtSession.AzureDevOpsConnectionCache = [PSCustomObject]@{ Organization = 'ado-org' } } Mock Get-AzContext { [PSCustomObject]@{ Account = 'test@contoso.com' } } -ModuleName Maester Mock Invoke-AzRestMethod { [PSCustomObject]@{} } -ModuleName Maester @@ -138,7 +138,7 @@ Describe 'Test-MtConnection — GitHub service' { It 'does not set the GitHub details property' { InModuleScope Maester { $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $false; FailureReason = 'NotCalled' } - $__MtSession.AzureDevOpsConnection = 'NotConnected' + $__MtSession.AzureDevOpsConnectionCache = 'NotConnected' } Mock Get-AzContext { $null } -ModuleName Maester Mock Get-MgContext { $null } -ModuleName Maester @@ -183,7 +183,7 @@ Describe 'Test-MtConnection — GitHub service' { It 'Does not populate the GitHub property' { InModuleScope Maester { $__MtSession.GitHubConnection = [PSCustomObject]@{ Connected = $true; Organization = 'myorg' } - $__MtSession.AzureDevOpsConnection = 'NotConnected' + $__MtSession.AzureDevOpsConnectionCache = 'NotConnected' } Mock Get-AzContext { $null } -ModuleName Maester Mock Get-MgContext { $null } -ModuleName Maester @@ -194,3 +194,72 @@ Describe 'Test-MtConnection — GitHub service' { } } } + +Describe 'Test-MtConnection AzureDevOps cache' { + BeforeEach { + InModuleScope Maester { + $__MtSession.AzureDevOpsConnectionCache = $null + $__MtSession.Remove('AzureDevOpsConnection') + } + } + + AfterEach { + InModuleScope Maester { + Remove-Item -Path function:Get-ADOPSConnection -ErrorAction SilentlyContinue + $__MtSession.AzureDevOpsConnectionCache = $null + $__MtSession.Remove('AzureDevOpsConnection') + } + } + + It 'caches a successful Azure DevOps probe under a cache-specific key' { + $result = InModuleScope Maester { + New-Item -Path function:Get-ADOPSConnection -Value { @{ Organization = 'ado-org' } } -Force | Out-Null + Test-MtConnection -Service AzureDevOps -Details + } + + $result.AllConnected | Should -BeTrue + $result.AzureDevOps['Organization'] | Should -Be 'ado-org' + InModuleScope Maester { + $__MtSession.AzureDevOpsConnectionCache['Organization'] | Should -Be 'ado-org' + $__MtSession.ContainsKey('AzureDevOpsConnection') | Should -BeFalse + } + } + + It 'caches a failed Azure DevOps probe as NotConnected' { + $result = InModuleScope Maester { + New-Item -Path function:Get-ADOPSConnection -Value { $null } -Force | Out-Null + Test-MtConnection -Service AzureDevOps -Details + } + + $result.AllConnected | Should -BeFalse + $result.AzureDevOps | Should -BeNullOrEmpty + InModuleScope Maester { + $__MtSession.AzureDevOpsConnectionCache | Should -Be 'NotConnected' + $__MtSession.ContainsKey('AzureDevOpsConnection') | Should -BeFalse + } + } + + It 'uses the cached Azure DevOps probe result without re-querying the external command' { + InModuleScope Maester { + $__MtSession.AzureDevOpsConnectionCache = @{ Organization = 'cached-org' } + } + $result = InModuleScope Maester { + New-Item -Path function:Get-ADOPSConnection -Value { throw 'Get-ADOPSConnection should not be called when cache exists.' } -Force | Out-Null + Test-MtConnection -Service AzureDevOps -Details + } + + $result.AllConnected | Should -BeTrue + $result.AzureDevOps['Organization'] | Should -Be 'cached-org' + } + + It 'clears the Azure DevOps probe cache during module-variable reset' { + InModuleScope Maester { + $__MtSession.AzureDevOpsConnectionCache = @{ Organization = 'cached-org' } + + Clear-ModuleVariable + + $__MtSession.AzureDevOpsConnectionCache | Should -BeNullOrEmpty + $__MtSession.ContainsKey('AzureDevOpsConnection') | Should -BeFalse + } + } +}