Tests/Invoke-ADRealtimeHeartbeat.Tests.ps1

Import-Module "$PSScriptRoot\..\ADOpsKit.psd1" -Force -ErrorAction Stop

Describe "Invoke-ADRealtimeHeartbeat" {

    Context "Parameter defaults" {

        It "Should default OutputFolder to C:\ADOpsKit\Reports\Invoke-ADRealtimeHeartbeat" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'OutputFolder' }
            $param.DefaultValue.ToString() | Should -Match 'C:\\ADOpsKit\\Reports\\Invoke-ADRealtimeHeartbeat'
        }

        It "Should default ReportRetentionDays to 30" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'ReportRetentionDays' }
            $param.DefaultValue.ToString() | Should -Be '30'
        }

        It "Should default TcpTimeoutMs to 3000" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'TcpTimeoutMs' }
            $param.DefaultValue.ToString() | Should -Be '3000'
        }

        It "Should default TcpPort to 53, 88, 135, 389, 445" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'TcpPort' }
            $param.DefaultValue.ToString() | Should -Match '53.*88.*135.*389.*445'
        }

        It "Should default DeepCheckTimeoutMs to 5000" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'DeepCheckTimeoutMs' }
            $param.DefaultValue.ToString() | Should -Be '5000'
        }

        It "Should default RequiredService to NTDS, Netlogon, KDC, DNS, DFSR, W32Time" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'RequiredService' }
            $param.DefaultValue.ToString() | Should -Match "NTDS.*Netlogon.*KDC.*DNS.*DFSR.*W32Time"
        }

        It "Should default SuppressRepeatMinutes to 15" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'SuppressRepeatMinutes' }
            $param.DefaultValue.ToString() | Should -Be '15'
        }

        It "Should default SmtpPort to 25" {
            $ast   = (Get-Command Invoke-ADRealtimeHeartbeat).ScriptBlock.Ast
            $param = $ast.Body.ParamBlock.Parameters | Where-Object { $_.Name.VariablePath.UserPath -eq 'SmtpPort' }
            $param.DefaultValue.ToString() | Should -Be '25'
        }
    }

    Context "Parameter validation" {

        # These all fail at Assert-Configuration, before the output folder
        # is created or Active Directory is contacted, so they are safe to
        # run without domain connectivity or any side effects.

        It "Should throw when -SendEmail is supplied without -To" {
            { Invoke-ADRealtimeHeartbeat -SendEmail -From 'a@b.com' -SmtpServer 'smtp.contoso.com' } |
                Should -Throw '*no -To recipient addresses*'
        }

        It "Should throw when -SendEmail is supplied without -From" {
            { Invoke-ADRealtimeHeartbeat -SendEmail -To 'a@b.com' -SmtpServer 'smtp.contoso.com' } |
                Should -Throw '*-From is empty*'
        }

        It "Should throw when -SendEmail is supplied without -SmtpServer" {
            { Invoke-ADRealtimeHeartbeat -SendEmail -To 'a@b.com' -From 'a@b.com' } |
                Should -Throw '*-SmtpServer is empty*'
        }

        It "Should throw when -SendSlack is supplied without a webhook URL" {
            { Invoke-ADRealtimeHeartbeat -SendSlack } |
                Should -Throw '*Slack webhook*'
        }
    }

    Context "-VersionInfo returns metadata without running any checks" {

        It "Should return version metadata" {
            $result = Invoke-ADRealtimeHeartbeat -VersionInfo
            $result.ScriptName | Should -Be 'Invoke-ADRealtimeHeartbeat'
            $result.Version    | Should -Not -BeNullOrEmpty
        }
    }

    Context "Integration - requires domain connectivity" -Tag 'Integration' {

        BeforeAll {
            $script:outputFolder = Join-Path $TestDrive 'heartbeat'
            $script:result = Invoke-ADRealtimeHeartbeat -OutputFolder $script:outputFolder
        }

        It "Should return one result per domain controller" {
            $script:result.Count | Should -BeGreaterThan 0
        }

        It "Should report OverallStatus for each result" {
            $script:result | ForEach-Object { $_.OverallStatus | Should -Not -BeNullOrEmpty }
        }

        It "Should write latest.html and latest.json" {
            Join-Path $script:outputFolder 'latest.html' | Should -Exist
            Join-Path $script:outputFolder 'latest.json' | Should -Exist
        }

        It "Should write a dated report into the history folder" {
            Get-ChildItem (Join-Path $script:outputFolder 'history') -Filter '*.html' | Should -Not -BeNullOrEmpty
        }

        It "Should write the alert state file" {
            Join-Path $script:outputFolder 'ADRealtimeHeartbeat-State.json' | Should -Exist
        }
    }
}