diff --git a/CHANGELOG.md b/CHANGELOG.md index be7d919..7bc6033 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. The format follows [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/): a new feature or capability bumps MINOR, a bug fix or performance change that leaves the public surface alone bumps PATCH, and docs/tests/chore-only commits don't bump. While the module is pre-1.0 a breaking change also bumps MINOR and is called out under a **Breaking Changes** heading — 1.0.0 is reserved for the point the public surface is declared stable, after which breaking changes bump MAJOR. There is intentionally no `[Unreleased]` section — every entry is dated at ship time. +## [0.7.3] - 2026-09-14 + +### Bug Fixes + +- **`Test-InforcerConnection` now returns `$true` / `$false`.** It previously wrote to the host and returned nothing at all, so `if (Test-InforcerConnection) { ... }` was always false — the `Test-*` verb promised a predicate the cmdlet never delivered. It now returns a boolean; the host output is unchanged. + - **Failures are warnings, not errors.** "Not connected" and a failed API call used to call `Write-Error`. That cannot coexist with a predicate: `Test-InforcerConnection -ErrorAction Stop` would throw instead of returning `$false`, as would any call once `$ErrorActionPreference` is `Stop` at global scope. (A script-scope `$ErrorActionPreference` does *not* reach a module cmdlet, so that case was never affected.) Both paths now use `Write-Warning`. + - **Migration.** Anything reading `-ErrorVariable` from this cmdlet should read `-WarningVariable` instead. The sharper edge: a script running under `Stop` that called `Test-InforcerConnection` bare, relying on the throw to abort, now continues past it — check the return value instead, e.g. `if (-not (Test-InforcerConnection)) { throw 'no connection' }`. Nothing inside the module calls this cmdlet and neither does the MCP server, so the blast radius is your own scripts. + ## [0.7.2] - 2026-09-11 ### Bug Fixes diff --git a/README.md b/README.md index c20a79e..7472110 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ Disconnect-Inforcer | ------------------------------ | ----------------------------------------------------------- | | **Connect-Inforcer** | Establishes a secure connection to the Inforcer REST API. Supports `-FetchGraphData` to also connect Microsoft Graph. | | **Disconnect-Inforcer** | Disconnects and clears the session (including Graph if connected). | -| **Test-InforcerConnection** | Tests the API connection. | +| **Test-InforcerConnection** | Tests the API connection; returns `$true` or `$false`. | | **Get-InforcerTenant** | Retrieves tenant information (optional filter by TenantId). | | **Get-InforcerBaseline** | Retrieves baseline groups and members. | | **Get-InforcerTenantPolicies** | Retrieves policies for a specified tenant. | diff --git a/Tests/Consistency.Tests.ps1 b/Tests/Consistency.Tests.ps1 index c96a83f..1294716 100644 --- a/Tests/Consistency.Tests.ps1 +++ b/Tests/Consistency.Tests.ps1 @@ -142,10 +142,73 @@ Describe 'No-silent-failure contract' { $output | Should -Not -BeNullOrEmpty } - It 'Test-InforcerConnection produces an error when not connected' { - $err = $null - Test-InforcerConnection -ErrorVariable err -ErrorAction SilentlyContinue - $err | Should -Not -BeNullOrEmpty -Because 'should report not connected, not return silence' + It 'Test-InforcerConnection warns and returns $false when not connected' { + $warn = $null + $result = Test-InforcerConnection -WarningVariable warn -WarningAction SilentlyContinue + $result | Should -BeOfType [bool] + $result | Should -BeFalse + $warn | Should -Not -BeNullOrEmpty -Because 'should report not connected, not return silence' + } + + It 'Test-InforcerConnection answers instead of throwing under -ErrorAction Stop' { + # -ErrorAction Stop is the form that reaches a module cmdlet: a caller-scope + # $ErrorActionPreference does NOT propagate into module scope, so testing that + # would pass no matter which stream the cmdlet writes to. + $result = Test-InforcerConnection -ErrorAction Stop -WarningAction SilentlyContinue + $result | Should -BeFalse + } + + It 'Test-InforcerConnection returns $true when the API responds' { + Mock Invoke-RestMethod { [PSCustomObject]@{ data = @() } } -ModuleName InforcerCommunity + & (Get-Module InforcerCommunity) { + $script:InforcerSession = @{ + ApiKey = (ConvertTo-SecureString 'test-key' -AsPlainText -Force) + BaseUrl = 'https://api.test.com' + } + } + try { + $result = Test-InforcerConnection 6>$null + $result | Should -BeOfType [bool] + $result | Should -BeTrue + } finally { + & (Get-Module InforcerCommunity) { $script:InforcerSession = $null } + } + } + + It 'Test-InforcerConnection returns $false and warns when the API call fails' { + Mock Invoke-RestMethod { throw 'HTTP 401 Unauthorized' } -ModuleName InforcerCommunity + & (Get-Module InforcerCommunity) { + $script:InforcerSession = @{ + ApiKey = (ConvertTo-SecureString 'test-key' -AsPlainText -Force) + BaseUrl = 'https://api.test.com' + } + } + try { + $warn = $null + $result = Test-InforcerConnection -WarningVariable warn -WarningAction SilentlyContinue 6>$null + $result | Should -BeFalse + "$warn" | Should -Match '401' + } finally { + & (Get-Module InforcerCommunity) { $script:InforcerSession = $null } + } + } + + It 'Test-InforcerConnection answers instead of throwing when the API call fails under -ErrorAction Stop' { + # The failure path is the one that matters: a Write-Error in the catch would throw here + # and the predicate would be unusable exactly when the connection is broken. + Mock Invoke-RestMethod { throw 'HTTP 500 Server Error' } -ModuleName InforcerCommunity + & (Get-Module InforcerCommunity) { + $script:InforcerSession = @{ + ApiKey = (ConvertTo-SecureString 'test-key' -AsPlainText -Force) + BaseUrl = 'https://api.test.com' + } + } + try { + $result = Test-InforcerConnection -ErrorAction Stop -WarningAction SilentlyContinue 6>$null + $result | Should -BeFalse + } finally { + & (Get-Module InforcerCommunity) { $script:InforcerSession = $null } + } } It 'Get-InforcerTenant produces an error when not connected' { diff --git a/docs/CMDLET-REFERENCE.md b/docs/CMDLET-REFERENCE.md index e7c5c5c..6f42c9e 100644 --- a/docs/CMDLET-REFERENCE.md +++ b/docs/CMDLET-REFERENCE.md @@ -77,7 +77,9 @@ No active session to disconnect. ## Test-InforcerConnection -Tests the current API connection by sending a request to the API. Requires an active session (run `Connect-Inforcer` first). +Tests the current API connection by sending a request to the API, and returns `$true` or `$false` so the result can be used as a condition. Requires an active session (run `Connect-Inforcer` first). + +Failures — including "no session" — are reported as **warnings**, not errors, so the cmdlet keeps answering the question instead of throwing in scripts that run under `$ErrorActionPreference = 'Stop'`. **Required API scope(s)**: None (session management only) @@ -90,15 +92,24 @@ Connect-Inforcer -ApiKey $env:INFORCER_API_KEY -Region uk Test-InforcerConnection ``` +```powershell +# Reconnect only when the current session is dead +if (-not (Test-InforcerConnection)) { Connect-Inforcer -ApiKey $key -Region uk } +``` + ### Example output -Success (written to host): +Returns `True`; the following is written to the host: ``` -Connection successful. API is reachable. +SUCCESS! Connection is working. ``` -When not connected, an error is written (e.g. "Not connected. To connect, run: Connect-Inforcer ..."). +With no active session it returns `False` and writes a warning: + +``` +WARNING: Not connected. To connect, run: Connect-Inforcer -ApiKey -Region +``` --- diff --git a/module/InforcerCommunity.psd1 b/module/InforcerCommunity.psd1 index 0c4799a..6e856ec 100644 --- a/module/InforcerCommunity.psd1 +++ b/module/InforcerCommunity.psd1 @@ -1,6 +1,6 @@ @{ RootModule = 'InforcerCommunity.psm1' - ModuleVersion = '0.7.2' + ModuleVersion = '0.7.3' GUID = 'a1b2c3d4-e5f6-7890-abcd-ef1234567890' Author = 'Roy Klooster' Description = 'Community PowerShell module for the Inforcer API. Created by Roy Klooster. Not owned or officially maintained by Inforcer.' diff --git a/module/Public/Test-InforcerConnection.ps1 b/module/Public/Test-InforcerConnection.ps1 index 909c9ff..9916c5a 100644 --- a/module/Public/Test-InforcerConnection.ps1 +++ b/module/Public/Test-InforcerConnection.ps1 @@ -2,13 +2,20 @@ .SYNOPSIS Tests the Inforcer API connection. .DESCRIPTION - Makes a test request to the /beta/baselines endpoint to verify the current session and API key work. - Requires an active session (run Connect-Inforcer first). + Makes a test request to the /beta/baselines endpoint to verify the current session and API key + work, and returns $true or $false so the result can be used as a condition. + + Failures are reported as warnings rather than errors, so `Test-InforcerConnection -ErrorAction Stop` + still answers the question instead of throwing. .EXAMPLE Connect-Inforcer -ApiKey $env:INFORCER_API_KEY -Region uk; Test-InforcerConnection Connects then verifies the connection. +.EXAMPLE + if (-not (Test-InforcerConnection)) { Connect-Inforcer -ApiKey $key -Region uk } + Reconnects only when the current session is dead. .OUTPUTS - None. Writes success or failure to the host. + System.Boolean. $true when the API responded, $false when it did not or there is no session. + Status messages go to the host; failures to the warning stream and details to verbose. .LINK https://github.com/royklo/InforcerCommunity/blob/main/docs/CMDLET-REFERENCE.md#test-inforcerconnection .LINK @@ -16,12 +23,12 @@ #> function Test-InforcerConnection { [CmdletBinding()] +[OutputType([bool])] param() if (-not (Test-InforcerSession)) { - Write-Error -Message "Not connected. To connect, run: Connect-Inforcer -ApiKey -Region " ` - -ErrorId 'NotConnected' -Category ConnectionError - return + Write-Warning 'Not connected. To connect, run: Connect-Inforcer -ApiKey -Region ' + return $false } $uri = $script:InforcerSession.BaseUrl + '/beta/baselines' @@ -36,9 +43,11 @@ try { $headers = @{ 'Inf-Api-Key' = $apiKey; 'Accept' = 'application/json' } $null = Invoke-RestMethod -Uri $uri -Method GET -Headers $headers -UseBasicParsing Write-Host 'SUCCESS! Connection is working.' -ForegroundColor Green + return $true } catch { Write-Host 'FAILED! Connection test failed.' -ForegroundColor Red Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red - Write-Error -Message $_.Exception.Message -ErrorId 'ConnectionTestFailed' -Category ConnectionError + Write-Warning "Inforcer connection test failed: $($_.Exception.Message)" + return $false } }