tests/Unit/Public/ConfigChecks.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer die Konfigurationspruefungen und zwei kleine Hilfsfunktionen:
    Test-sqmMaxDop, Test-sqmMaxMemory, Test-sqmCostThreshold, Test-sqmTempDbFileCount,
    New-sqmRandomSaPassword, Get-sqmSupportedCollations.
    dbatools, SMO und WMI werden gemockt; getestet wird die Auswertung (Schwellen, Status, Fehlerpfad).
#>


BeforeAll {
    . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1"
    Import-sqmTestModule

    # Get-WmiObject gibt es in PowerShell 7 nicht; fuer den Mock braucht es ein Kommando im Modul.
    InModuleScope sqmSQLTool {
        if (-not (Get-Command Get-WmiObject -ErrorAction SilentlyContinue))
        {
            ${function:script:Get-WmiObject} = { param($Class, $ComputerName, $ErrorAction) }
        }
    }
    $script:TestDir = New-TempTestDirectory
}

AfterAll {
    if ($script:TestDir -and (Test-Path $script:TestDir)) { Remove-Item $script:TestDir -Recurse -Force }
    if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force }
    $env:MSSQLTOOLS_SKIP_AUTO_UPDATE = $null
}

Describe 'Test-sqmMaxDop' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
    }

    It 'OK wenn MAXDOP = min(8, CPUs)' {
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ NumberOfLogicalProcessors = 16 } }
        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { [pscustomobject]@{ RunningValue = 8 } }
        $r = Test-sqmMaxDop -SqlInstance 'SQL01'
        $r.Status | Should -Be 'OK'
        $r.RecommendedDop | Should -Be 8
    }

    It 'bei weniger als 8 CPUs ist die CPU-Zahl die Empfehlung' {
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ NumberOfLogicalProcessors = 4 } }
        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { [pscustomobject]@{ RunningValue = 4 } }
        (Test-sqmMaxDop -SqlInstance 'SQL01').Status | Should -Be 'OK'
    }

    It 'Unconfigured bei MAXDOP 0' {
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ NumberOfLogicalProcessors = 8 } }
        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { [pscustomobject]@{ RunningValue = 0 } }
        (Test-sqmMaxDop -SqlInstance 'SQL01').Status | Should -Be 'Unconfigured'
    }

    It 'Suboptimal und Richtung der Abweichung' {
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ NumberOfLogicalProcessors = 16 } }
        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { [pscustomobject]@{ RunningValue = 2 } }
        $low = Test-sqmMaxDop -SqlInstance 'SQL01'
        $low.Status | Should -Be 'Suboptimal'
        $low.Message | Should -Match 'zu niedrig'

        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { [pscustomobject]@{ RunningValue = 16 } }
        (Test-sqmMaxDop -SqlInstance 'SQL01').Message | Should -Match 'zu hoch'
    }

    It 'Status Error, wenn dbatools fehlschlaegt' {
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ NumberOfLogicalProcessors = 8 } }
        Mock -ModuleName sqmSQLTool Get-DbaSpConfigure { throw 'Verbindung verweigert' }
        $r = Test-sqmMaxDop -SqlInstance 'SQL01' -ErrorAction SilentlyContinue
        $r.Status | Should -Be 'Error'
        $r.Message | Should -Match 'Verbindung verweigert'
    }
}

Describe 'Test-sqmMaxMemory' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        # 64 GB RAM, eine Instanz: Empfehlung 90 % = 58982 MB, Toleranz 85-95 % = 55706 - 62259 MB
        Mock -ModuleName sqmSQLTool Get-WmiObject { [pscustomobject]@{ TotalPhysicalMemory = [int64]64GB } }
    }

    It '<Name>: <Value> MB ergibt Status <Expected>' -ForEach @(
        @{ Name = 'Default-Wert';  Value = 2147483647; Expected = 'Unconfigured' }
        @{ Name = 'zu hoch';       Value = 65536;      Expected = 'TooHigh' }
        @{ Name = 'zu niedrig';    Value = 32768;      Expected = 'TooLow' }
        @{ Name = 'Empfehlung';    Value = 58982;      Expected = 'OK' }
        @{ Name = 'untere Grenze'; Value = 55706;      Expected = 'OK' }
        @{ Name = 'obere Grenze';  Value = 62259;      Expected = 'OK' }
    ) {
        Mock -ModuleName sqmSQLTool Get-DbaMaxMemory { [pscustomobject]@{ MaxValue = $Value } } -ParameterFilter { $true }
        $r = Test-sqmMaxMemory -SqlInstance 'SQL01' -InstanceCount 1
        $r.Status | Should -Be $Expected
        $r.RecommendedMB | Should -Be 58982
    }

    It 'teilt das Budget auf mehrere Instanzen auf' {
        Mock -ModuleName sqmSQLTool Get-DbaMaxMemory { [pscustomobject]@{ MaxValue = 29491 } }
        $r = Test-sqmMaxMemory -SqlInstance 'SQL01' -InstanceCount 2
        $r.RecommendedMB | Should -Be 29491
        $r.Status | Should -Be 'OK'
    }

    It 'ermittelt die Instanzzahl selbst, wenn sie nicht angegeben ist' {
        Mock -ModuleName sqmSQLTool Get-sqmHostEngineInstanceCount { 2 }
        Mock -ModuleName sqmSQLTool Get-DbaMaxMemory { [pscustomobject]@{ MaxValue = 29491 } }
        $r = Test-sqmMaxMemory -SqlInstance 'SQL01'
        $r.InstanceCount | Should -Be 2
        Should -Invoke -ModuleName sqmSQLTool Get-sqmHostEngineInstanceCount -Times 1 -Exactly
    }

    It 'lehnt RecommendedPct ausserhalb 70-99 ab' {
        { Test-sqmMaxMemory -SqlInstance 'SQL01' -RecommendedPct 50 -InstanceCount 1 } | Should -Throw
    }
}

Describe 'Test-sqmCostThreshold' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Get-sqmConfig { $null }
        Mock -ModuleName sqmSQLTool Write-Host { }
    }

    It '<Value> bei Minimum <Min> ergibt <Expected>' -ForEach @(
        @{ Value = 5;  Min = 50; Expected = 'Warning' }
        @{ Value = 50; Min = 50; Expected = 'OK' }
        @{ Value = 80; Min = 50; Expected = 'OK' }
        @{ Value = 25; Min = 20; Expected = 'OK' }
    ) {
        $fake = [pscustomobject]@{ Configuration = [pscustomobject]@{ CostThresholdForParallelism = [pscustomobject]@{ RunValue = $Value } } }
        Mock -ModuleName sqmSQLTool New-Object { $fake } -ParameterFilter { $TypeName -like '*Smo.Server' }
        $r = Test-sqmCostThreshold -SqlInstance 'SQL01' -MinRecommendedValue $Min
        $r.Status | Should -Be $Expected
        $r.CurrentValue | Should -Be $Value
    }

    It 'nimmt das Minimum aus der Modulkonfiguration, wenn kein Parameter gesetzt ist' {
        Mock -ModuleName sqmSQLTool Get-sqmConfig { 30 } -ParameterFilter { $Key -eq 'CheckCostThresholdMin' }
        $fake = [pscustomobject]@{ Configuration = [pscustomobject]@{ CostThresholdForParallelism = [pscustomobject]@{ RunValue = 35 } } }
        Mock -ModuleName sqmSQLTool New-Object { $fake } -ParameterFilter { $TypeName -like '*Smo.Server' }
        $r = Test-sqmCostThreshold -SqlInstance 'SQL01'
        $r.RecommendedMinValue | Should -Be 30
        $r.Status | Should -Be 'OK'
    }

    It 'liefert Status Error bzw. wirft mit -EnableException' {
        Mock -ModuleName sqmSQLTool New-Object { throw 'kein Zugriff' } -ParameterFilter { $TypeName -like '*Smo.Server' }
        (Test-sqmCostThreshold -SqlInstance 'SQL01' -ErrorAction SilentlyContinue).Status | Should -Be 'Error'
        { Test-sqmCostThreshold -SqlInstance 'SQL01' -EnableException } | Should -Throw '*kein Zugriff*'
    }
}

Describe 'Test-sqmTempDbFileCount' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Get-sqmConfig { $null }
        Mock -ModuleName sqmSQLTool Write-Host { }
    }

    BeforeAll {
        function script:New-FakeSmoServer {
            param([int]$Cores, [int]$DataFiles)
            $files = 1..$DataFiles | ForEach-Object { [pscustomobject]@{ UsedSpace = 1024 } }
            [pscustomobject]@{
                Processors = @(1..$Cores | ForEach-Object { [pscustomobject]@{} })
                Databases  = @{ tempdb = [pscustomobject]@{ FileGroups = @([pscustomobject]@{ Files = $files }) } }
            }
        }
    }

    It '<Files> Dateien bei <Cores> Kernen (max <Max>) ergibt <Expected>' -ForEach @(
        @{ Cores = 4;  Files = 4; Max = 8; Expected = 'OK' }
        @{ Cores = 16; Files = 8; Max = 8; Expected = 'OK' }
        @{ Cores = 16; Files = 4; Max = 8; Expected = 'Warning' }
        @{ Cores = 2;  Files = 8; Max = 8; Expected = 'Warning' }
        @{ Cores = 16; Files = 12; Max = 12; Expected = 'OK' }
    ) {
        $fake = New-FakeSmoServer -Cores $Cores -DataFiles $Files
        Mock -ModuleName sqmSQLTool New-Object { $fake } -ParameterFilter { $TypeName -like '*Smo.Server' }
        $r = Test-sqmTempDbFileCount -SqlInstance 'SQL01' -MaxFiles $Max
        $r.Status | Should -Be $Expected
        $r.CurrentFileCount | Should -Be $Files
        $r.RecommendedCount | Should -Be ([Math]::Min($Cores, $Max))
    }

    It 'liefert Status Error bei SMO-Fehler' {
        Mock -ModuleName sqmSQLTool New-Object { throw 'Server nicht erreichbar' } -ParameterFilter { $TypeName -like '*Smo.Server' }
        (Test-sqmTempDbFileCount -SqlInstance 'SQL01' -ErrorAction SilentlyContinue).Status | Should -Be 'Error'
    }
}

Describe 'New-sqmRandomSaPassword' {
    BeforeEach { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } }

    BeforeAll {
        function script:ConvertTo-PlainText([securestring]$s)
        {
            [System.Net.NetworkCredential]::new('', $s).Password
        }
    }

    It 'liefert ein SecureString der geforderten Laenge' {
        $pw = New-sqmRandomSaPassword -Length 24
        $pw | Should -BeOfType [securestring]
        (ConvertTo-PlainText $pw).Length | Should -Be 24
    }

    It 'enthaelt in jedem von 15 Durchlaeufen alle vier Zeichenklassen' {
        1..15 | ForEach-Object {
            $p = ConvertTo-PlainText (New-sqmRandomSaPassword -Length 12)
            $p | Should -MatchExactly '[A-Z]'
            $p | Should -MatchExactly '[a-z]'
            $p | Should -MatchExactly '[0-9]'
            $p | Should -MatchExactly '[^A-Za-z0-9]'
        }
    }

    It 'nutzt keine verwechselbaren Zeichen (I O l i o 0 1)' {
        1..15 | ForEach-Object {
            ConvertTo-PlainText (New-sqmRandomSaPassword -Length 40) | Should -Not -MatchExactly '[IOlio01]'
        }
    }

    It 'erzeugt unterschiedliche Passwoerter' {
        $set = 1..20 | ForEach-Object { ConvertTo-PlainText (New-sqmRandomSaPassword) }
        ($set | Select-Object -Unique).Count | Should -Be 20
    }

    It 'lehnt Laengen unter 12 ab' {
        { New-sqmRandomSaPassword -Length 8 } | Should -Throw
    }

    It '-ExportPath legt auch fehlende Ordner an und schreibt keinen Klartext' {
        $path = Join-Path $script:TestDir 'neu\ordner\sa.pwd'
        $pw = New-sqmRandomSaPassword -ExportPath $path
        Test-Path $path | Should -BeTrue
        (Get-Content $path -Raw) | Should -Not -Match ([regex]::Escape((ConvertTo-PlainText $pw)))
    }
}

Describe 'Get-sqmSupportedCollations' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            @(
                [pscustomobject]@{ name = 'Latin1_General_CI_AS'; description = 'Latin1-General, case-insensitive' }
                [pscustomobject]@{ name = 'Latin1_General_BIN2';  description = 'Latin1-General, binary code point' }
                [pscustomobject]@{ name = 'SQL_Latin1_General_CP1_CI_AS'; description = 'Latin1-General, SQL' }
            )
        }
    }

    It 'liefert alle Collations ohne Filter' {
        @(Get-sqmSupportedCollations -SqlInstance 'SQL01').Count | Should -Be 3
    }

    It 'filtert per Wildcard' {
        $r = @(Get-sqmSupportedCollations -SqlInstance 'SQL01' -Filter 'Latin1_General_*')
        $r.Count | Should -Be 2
        $r.Name | Should -Not -Contain 'SQL_Latin1_General_CP1_CI_AS'
        $r[0].SqlInstance | Should -Be 'SQL01'
    }

    It 'fragt mit -EnableException ab und reicht Fehler weiter' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery { throw 'Login failed' }
        { Get-sqmSupportedCollations -SqlInstance 'SQL01' -EnableException } | Should -Throw '*Login failed*'
    }
}