Tests/ntp/Get-NTPConfiguration.Tests.ps1
|
<#
.SYNOPSIS Pester tests for Get-NTPConfiguration function. .DESCRIPTION Tests covering: - Output structure and property types - Service availability checks - w32tm output parsing (NtpServer, status, peers) - IncludePeerDetails switch behavior - Error handling (service not found, w32tm failure) .NOTES Author: K9FR4N Pester: v5.x Run with: Invoke-Pester -Path .\Get-NTPConfiguration.Tests.ps1 -Output Detailed #> BeforeAll { #region Stubs for Windows-only commands # Pester cannot Mock a command that does not exist on the system. # On Linux CI runners, Get-Service and w32tm are absent. # We declare global stub functions BEFORE dot-sourcing the script # so that Mock can intercept them in every test. if (-not (Get-Command 'Get-Service' -ErrorAction SilentlyContinue)) { function global:Get-Service { param([string]$Name, $ErrorAction) } } function global:w32tm { param() } #endregion . (Join-Path $PSScriptRoot '..\..\Public\ntp\Get-NTPConfiguration.ps1') #region Mock data $Script:MockConfigOutput = @( 'NtpServer: ntp1.example.com,0x9 ntp2.example.com,0x9 (Local)' 'Type: NTP (Local)' 'SpecialPollInterval: 3600 (Local)' 'MinPollInterval: 6 (Local)' 'MaxPollInterval: 10 (Local)' ) $Script:MockStatusOutput = @( 'Leap Indicator: 0(no warning)' 'Stratum: 3 (secondary reference)' 'Source: ntp1.example.com' 'Last Successful Sync Time: 2/20/2026 8:00:00 AM' ) $Script:MockPeersOutput = @( '#Peers: 2' 'Peer: ntp1.example.com,0x9' 'State: Active' 'Peer: ntp2.example.com,0x9' 'State: Active' ) #endregion } Describe 'Get-NTPConfiguration' { # --------------------------------------------------------------- # Context 1 : Nominal - service running # --------------------------------------------------------------- Context 'Nominal - service running, w32tm outputs valid data' { BeforeEach { Mock Get-Service { [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' } } -ParameterFilter { $Name -eq 'w32time' } Mock w32tm { switch ($args -join ' ') { '/query /configuration' { return $Script:MockConfigOutput } '/query /status /verbose' { return $Script:MockStatusOutput } '/query /peers' { return $Script:MockPeersOutput } } } } It 'Should return a PSCustomObject' { Get-NTPConfiguration | Should -BeOfType [PSCustomObject] } It 'Should expose all expected properties' { $r = Get-NTPConfiguration $expected = @( 'ServiceName', 'ServiceStatus', 'SyncType', 'ConfiguredServers', 'CurrentSource', 'LastSuccessfulSync', 'Stratum', 'LeapIndicator', 'SpecialPollInterval', 'MinPollInterval', 'MaxPollInterval', 'MinPollIntervalSec', 'MaxPollIntervalSec', 'QueryTimestamp' ) foreach ($p in $expected) { $r.PSObject.Properties.Name | Should -Contain $p } } It 'ServiceName should be w32time' { (Get-NTPConfiguration).ServiceName | Should -Be 'w32time' } It 'ServiceStatus should be Running' { (Get-NTPConfiguration).ServiceStatus | Should -Be 'Running' } It 'SyncType should contain NTP' { # w32tm returns 'NTP (Local)' - use -Match to tolerate the suffix (Get-NTPConfiguration).SyncType | Should -Match 'NTP' } It 'ConfiguredServers should contain 2 entries' { $r = Get-NTPConfiguration $r.ConfiguredServers | Should -HaveCount 2 $r.ConfiguredServers | Should -Contain 'ntp1.example.com,0x9' $r.ConfiguredServers | Should -Contain 'ntp2.example.com,0x9' } It 'CurrentSource should match mock' { (Get-NTPConfiguration).CurrentSource | Should -Be 'ntp1.example.com' } It 'LastSuccessfulSync should not be Never' { (Get-NTPConfiguration).LastSuccessfulSync | Should -Not -Be 'Never' } It 'Stratum should be [int] equal to 3' { $r = Get-NTPConfiguration $r.Stratum | Should -BeOfType [int] $r.Stratum | Should -Be 3 } It 'SpecialPollInterval should be [int] equal to 3600' { $r = Get-NTPConfiguration $r.SpecialPollInterval | Should -BeOfType [int] $r.SpecialPollInterval | Should -Be 3600 } It 'MinPollInterval should be 6' { (Get-NTPConfiguration).MinPollInterval | Should -Be 6 } It 'MaxPollInterval should be 10' { (Get-NTPConfiguration).MaxPollInterval | Should -Be 10 } It 'MinPollIntervalSec should equal 2^6 = 64' { (Get-NTPConfiguration).MinPollIntervalSec | Should -Be 64 } It 'MaxPollIntervalSec should equal 2^10 = 1024' { (Get-NTPConfiguration).MaxPollIntervalSec | Should -Be 1024 } It 'QueryTimestamp should be parseable as ISO 8601' { { [datetime]::Parse((Get-NTPConfiguration).QueryTimestamp) } | Should -Not -Throw } It 'LeapIndicator should not be Unknown' { (Get-NTPConfiguration).LeapIndicator | Should -Not -Be 'Unknown' } } # --------------------------------------------------------------- # Context 2 : -IncludePeerDetails # --------------------------------------------------------------- Context '-IncludePeerDetails switch' { BeforeEach { Mock Get-Service { [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' } } -ParameterFilter { $Name -eq 'w32time' } Mock w32tm { switch ($args -join ' ') { '/query /configuration' { return $Script:MockConfigOutput } '/query /status /verbose' { return $Script:MockStatusOutput } '/query /peers' { return $Script:MockPeersOutput } } } } It 'Should NOT expose PeerDetails without the switch' { $r = Get-NTPConfiguration $r.PSObject.Properties.Name | Should -Not -Contain 'PeerDetails' } It 'Should expose PeerDetails with -IncludePeerDetails' { $r = Get-NTPConfiguration -IncludePeerDetails $r.PSObject.Properties.Name | Should -Contain 'PeerDetails' } It 'PeerDetails should reference both peers' { $r = Get-NTPConfiguration -IncludePeerDetails $r.PeerDetails | Should -Match 'ntp1.example.com' $r.PeerDetails | Should -Match 'ntp2.example.com' } } # --------------------------------------------------------------- # Context 3 : Degraded - empty w32tm output # --------------------------------------------------------------- Context 'Degraded - w32tm returns empty output' { BeforeEach { Mock Get-Service { [PSCustomObject]@{ Name = 'w32time'; Status = 'Stopped' } } -ParameterFilter { $Name -eq 'w32time' } Mock w32tm { return @() } } It 'SyncType should default to Unknown' { (Get-NTPConfiguration).SyncType | Should -Be 'Unknown' } It 'ConfiguredServers should be empty' { (Get-NTPConfiguration).ConfiguredServers | Should -HaveCount 0 } It 'CurrentSource should default to Unknown' { (Get-NTPConfiguration).CurrentSource | Should -Be 'Unknown' } It 'LastSuccessfulSync should default to Never' { (Get-NTPConfiguration).LastSuccessfulSync | Should -Be 'Never' } It 'Stratum should be null' { (Get-NTPConfiguration).Stratum | Should -BeNullOrEmpty } It 'SpecialPollInterval should be null' { (Get-NTPConfiguration).SpecialPollInterval | Should -BeNullOrEmpty } It 'MinPollIntervalSec should be null when MinPollInterval is null' { (Get-NTPConfiguration).MinPollIntervalSec | Should -BeNullOrEmpty } It 'MaxPollIntervalSec should be null when MaxPollInterval is null' { (Get-NTPConfiguration).MaxPollIntervalSec | Should -BeNullOrEmpty } } # --------------------------------------------------------------- # Context 4 : Error - service absent # --------------------------------------------------------------- Context 'Error handling - w32time service absent' { BeforeEach { Mock Get-Service { throw "Cannot find any service with service name 'w32time'." } -ParameterFilter { $Name -eq 'w32time' } } It 'Should throw when service is not found' { { Get-NTPConfiguration } | Should -Throw } It 'Should throw an error mentioning w32time' -Skip:(-not $IsWindows) { { Get-NTPConfiguration } | Should -Throw -ExpectedMessage '*w32time*' } } # --------------------------------------------------------------- # Context 5 : Error - unexpected w32tm failure # --------------------------------------------------------------- Context 'Error handling - unexpected w32tm failure' { BeforeEach { Mock Get-Service { [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' } } -ParameterFilter { $Name -eq 'w32time' } Mock w32tm { throw 'Simulated w32tm failure' } } It 'Should propagate unexpected w32tm errors' { { Get-NTPConfiguration } | Should -Throw } } } |