Tests/ntp/Get-NTPConfiguration.Tests.ps1

#Requires -Version 5.1
#Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '5.0.0' }

<#
.SYNOPSIS
    Pester tests for Get-NTPConfiguration function
 
.DESCRIPTION
    Comprehensive test coverage for Get-NTPConfiguration, including:
    - Output structure and property types validation
    - Service availability checks and error handling
    - w32tm command output parsing (configuration, status, peers)
    - IncludePeerDetails switch behavior
    - Degraded state handling (empty output, missing data)
    - Error scenarios (service not found, w32tm command failure)
 
.EXAMPLE
    Invoke-Pester -Path .\Get-NTPConfiguration.Tests.ps1 -Output Detailed
 
    Runs all tests with detailed output showing each assertion result.
 
.EXAMPLE
    Invoke-Pester -Path .\Get-NTPConfiguration.Tests.ps1 -Output Detailed -Tag 'Nominal'
 
    Runs only tests tagged as nominal/happy path scenarios.
 
.NOTES
    Author: K9FR4N
    Version: 1.1.0
    Last Modified: 2026-02-26
    Requires: Pester 5.x, PowerShell 5.1+
    Permissions: None required (all external commands are mocked)
#>


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 -Name 'Get-Service' -ErrorAction SilentlyContinue)) {
        function global:Get-Service {
            param([string]$Name, $ErrorAction)
        }
    }

    # w32tm.exe stub -- must exist for Mock to intercept
    if (-not (Get-Command -Name 'w32tm' -ErrorAction SilentlyContinue)) {
        function global:w32tm {
            param()
        }
    }
    #endregion

    # Import module -- two levels above Tests\ntp\
    $script:modulePath = Join-Path -Path $PSScriptRoot -ChildPath '..\..\PSWinOps.psd1'
    Import-Module -Name $script:modulePath -Force -ErrorAction Stop

    #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 -Name 'Get-NTPConfiguration' -Fixture {

    # ---------------------------------------------------------------
    # Context 1: Nominal - service running
    # ---------------------------------------------------------------
    Context -Name 'Nominal - service running, w32tm outputs valid data' -Fixture {

        BeforeEach {
            Mock -CommandName 'Get-Service' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' }
            } -ParameterFilter { $Name -eq 'w32time' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockConfigOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/configuration' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockStatusOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/status' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockPeersOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/peers' }
        }

        It -Name 'Should return a PSCustomObject' -Test {
            $result = Get-NTPConfiguration
            $result | Should -BeOfType ([PSCustomObject])
        }

        It -Name 'Should expose all expected properties' -Test {
            $result = Get-NTPConfiguration
            $expected = @(
                'ServiceName', 'ServiceStatus', 'SyncType', 'ConfiguredServers',
                'CurrentSource', 'LastSuccessfulSync', 'Stratum', 'LeapIndicator',
                'SpecialPollInterval', 'MinPollInterval', 'MaxPollInterval',
                'MinPollIntervalSec', 'MaxPollIntervalSec', 'QueryTimestamp'
            )
            foreach ($prop in $expected) {
                $result.PSObject.Properties.Name | Should -Contain $prop
            }
        }

        It -Name 'ServiceName should be w32time' -Test {
            $result = Get-NTPConfiguration
            $result.ServiceName | Should -Be 'w32time'
        }

        It -Name 'ServiceStatus should be Running' -Test {
            $result = Get-NTPConfiguration
            $result.ServiceStatus | Should -Be 'Running'
        }

        It -Name 'SyncType should contain NTP' -Test {
            $result = Get-NTPConfiguration
            $result.SyncType | Should -Match 'NTP'
        }

        It -Name 'ConfiguredServers should contain 2 entries' -Test {
            $result = Get-NTPConfiguration
            $result.ConfiguredServers | Should -HaveCount 2
            $result.ConfiguredServers | Should -Contain 'ntp1.example.com,0x9'
            $result.ConfiguredServers | Should -Contain 'ntp2.example.com,0x9'
        }

        It -Name 'CurrentSource should match mock' -Test {
            $result = Get-NTPConfiguration
            $result.CurrentSource | Should -Be 'ntp1.example.com'
        }

        It -Name 'LastSuccessfulSync should not be Never' -Test {
            $result = Get-NTPConfiguration
            $result.LastSuccessfulSync | Should -Not -Be 'Never'
        }

        It -Name 'Stratum should be [int] equal to 3' -Test {
            $result = Get-NTPConfiguration
            $result.Stratum | Should -BeOfType ([int])
            $result.Stratum | Should -Be 3
        }

        It -Name 'SpecialPollInterval should be [int] equal to 3600' -Test {
            $result = Get-NTPConfiguration
            $result.SpecialPollInterval | Should -BeOfType ([int])
            $result.SpecialPollInterval | Should -Be 3600
        }

        It -Name 'MinPollInterval should be 6' -Test {
            $result = Get-NTPConfiguration
            $result.MinPollInterval | Should -Be 6
        }

        It -Name 'MaxPollInterval should be 10' -Test {
            $result = Get-NTPConfiguration
            $result.MaxPollInterval | Should -Be 10
        }

        It -Name 'MinPollIntervalSec should equal 2^6 = 64' -Test {
            $result = Get-NTPConfiguration
            $result.MinPollIntervalSec | Should -Be 64
        }

        It -Name 'MaxPollIntervalSec should equal 2^10 = 1024' -Test {
            $result = Get-NTPConfiguration
            $result.MaxPollIntervalSec | Should -Be 1024
        }

        It -Name 'QueryTimestamp should be parseable as ISO 8601' -Test {
            $result = Get-NTPConfiguration
            { [datetime]::Parse($result.QueryTimestamp) } | Should -Not -Throw
        }

        It -Name 'LeapIndicator should not be Unknown' -Test {
            $result = Get-NTPConfiguration
            $result.LeapIndicator | Should -Not -Be 'Unknown'
        }
    }

    # ---------------------------------------------------------------
    # Context 2: -IncludePeerDetails
    # ---------------------------------------------------------------
    Context -Name '-IncludePeerDetails switch' -Fixture {

        BeforeEach {
            Mock -CommandName 'Get-Service' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' }
            } -ParameterFilter { $Name -eq 'w32time' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockConfigOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/configuration' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockStatusOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/status' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                $script:mockPeersOutput
            } -ParameterFilter { $args -contains '/query' -and $args -contains '/peers' }
        }

        It -Name 'Should NOT expose PeerDetails without the switch' -Test {
            $result = Get-NTPConfiguration
            $result.PSObject.Properties.Name | Should -Not -Contain 'PeerDetails'
        }

        It -Name 'Should expose PeerDetails with -IncludePeerDetails' -Test {
            $result = Get-NTPConfiguration -IncludePeerDetails
            $result.PSObject.Properties.Name | Should -Contain 'PeerDetails'
        }

        It -Name 'PeerDetails should reference both peers' -Test {
            $result = Get-NTPConfiguration -IncludePeerDetails
            $result.PeerDetails | Should -Match 'ntp1.example.com'
            $result.PeerDetails | Should -Match 'ntp2.example.com'
        }
    }

    # ---------------------------------------------------------------
    # Context 3: Degraded - empty w32tm output
    # ---------------------------------------------------------------
    Context -Name 'Degraded - w32tm returns empty output' -Fixture {

        BeforeEach {
            Mock -CommandName 'Get-Service' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{ Name = 'w32time'; Status = 'Stopped' }
            } -ParameterFilter { $Name -eq 'w32time' }

            # Mock without ParameterFilter = catch-all for all w32tm calls
            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith { @() }
        }

        It -Name 'SyncType should default to Unknown' -Test {
            $result = Get-NTPConfiguration
            $result.SyncType | Should -Be 'Unknown'
        }

        It -Name 'ConfiguredServers should be empty' -Test {
            $result = Get-NTPConfiguration
            $result.ConfiguredServers | Should -HaveCount 0
        }

        It -Name 'CurrentSource should default to Unknown' -Test {
            $result = Get-NTPConfiguration
            $result.CurrentSource | Should -Be 'Unknown'
        }

        It -Name 'LastSuccessfulSync should default to Never' -Test {
            $result = Get-NTPConfiguration
            $result.LastSuccessfulSync | Should -Be 'Never'
        }

        It -Name 'Stratum should be null' -Test {
            $result = Get-NTPConfiguration
            $result.Stratum | Should -BeNullOrEmpty
        }

        It -Name 'SpecialPollInterval should be null' -Test {
            $result = Get-NTPConfiguration
            $result.SpecialPollInterval | Should -BeNullOrEmpty
        }

        It -Name 'MinPollIntervalSec should be null when MinPollInterval is null' -Test {
            $result = Get-NTPConfiguration
            $result.MinPollIntervalSec | Should -BeNullOrEmpty
        }

        It -Name 'MaxPollIntervalSec should be null when MaxPollInterval is null' -Test {
            $result = Get-NTPConfiguration
            $result.MaxPollIntervalSec | Should -BeNullOrEmpty
        }
    }

    # ---------------------------------------------------------------
    # Context 4: Error - service absent
    # ---------------------------------------------------------------
    Context -Name 'Error handling - w32time service absent' -Fixture {

        BeforeEach {
            Mock -CommandName 'Get-Service' -ModuleName 'PSWinOps' -MockWith {
                # Throw a well-formed ErrorRecord directly
                throw [System.Management.Automation.ErrorRecord]::new(
                    [System.InvalidOperationException]::new(
                        "Cannot find any service with service name 'w32time'."
                    ),
                    'ServiceNotFound',
                    [System.Management.Automation.ErrorCategory]::ObjectNotFound,
                    'w32time'
                )
            } -ParameterFilter { $Name -eq 'w32time' }
        }

        It -Name 'Should throw when service is not found' -Test {
            { Get-NTPConfiguration -ErrorAction Stop } | Should -Throw
        }

        It -Name 'Should throw an error mentioning w32time' -Test {
            { Get-NTPConfiguration -ErrorAction Stop } | Should -Throw -ExpectedMessage '*w32time*'
        }
    }

    # ---------------------------------------------------------------
    # Context 5: Error - unexpected w32tm failure
    # ---------------------------------------------------------------
    Context -Name 'Error handling - unexpected w32tm failure' -Fixture {

        BeforeEach {
            Mock -CommandName 'Get-Service' -ModuleName 'PSWinOps' -MockWith {
                [PSCustomObject]@{ Name = 'w32time'; Status = 'Running' }
            } -ParameterFilter { $Name -eq 'w32time' }

            Mock -CommandName 'w32tm' -ModuleName 'PSWinOps' -MockWith {
                throw 'Simulated w32tm failure'
            }
        }

        It -Name 'Should propagate unexpected w32tm errors' -Test {
            { Get-NTPConfiguration -ErrorAction Stop } | Should -Throw
        }
    }
}