tests/Unit/Public/Add-sqmDatabaseToAG.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer Add-sqmDatabaseToAG, Schwerpunkt TDE-Behandlung.
    dbatools wird vollstaendig gemockt; getestet wird die Entscheidungslogik:
    Versionsschranke SQL 2019, Zertifikatspruefung ueber den Thumbprint, Verteilung
    an die Secondaries und die Zusicherung, dass bei einem Abbruch nichts veraendert wurde.
#>


# Hinweis zu den ParameterFiltern: ein Pester-Mock uebernimmt den Parameterblock des
# Originals, deshalb kommt -SqlInstance als DbaInstanceParameter an und nicht als String.
# Ein blosses $SqlInstance -eq 'SQL02' greift nicht - erst "$SqlInstance" -eq 'SQL02'.
BeforeAll {
    . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1"
    Import-sqmTestModule
    $script:TestDir = New-TempTestDirectory
    $script:Pw = ConvertTo-SecureString 'Cert#Pwd!2026' -AsPlainText -Force
}

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 'Add-sqmDatabaseToAG' {

    Context 'Parameter-Validierung' {
        It '<_> Parameter existiert' -ForEach @(
            'SqlInstance', 'SqlCredential', 'AvailabilityGroup', 'Database', 'All',
            'SyncTdeCertificate', 'TdeCertificateBackupPath', 'TdeCertificatePassword',
            'TdeMasterKeyPassword', 'KeepTdeCertificateBackup', 'EnableException'
        ) {
            (Get-Command Add-sqmDatabaseToAG).Parameters.ContainsKey($_) | Should -Be $true
        }

        It 'Unterstuetzt ShouldProcess (WhatIf/Confirm)' {
            $cmd = Get-Command Add-sqmDatabaseToAG
            $cmd.Parameters.ContainsKey('WhatIf')  | Should -Be $true
            $cmd.Parameters.ContainsKey('Confirm') | Should -Be $true
        }

        It 'Die TDE-Parameter gelten in beiden Parametersaetzen' {
            $cmd = Get-Command Add-sqmDatabaseToAG
            foreach ($setName in 'Specific', 'All')
            {
                $set = $cmd.ParameterSets | Where-Object { $_.Name -eq $setName }
                $set.Parameters.Name | Should -Contain 'SyncTdeCertificate'
                $set.Parameters.Name | Should -Contain 'TdeCertificatePassword'
            }
        }

        It '-SyncTdeCertificate ohne -TdeCertificateBackupPath wird abgelehnt' {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            { Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'DB1' `
                    -SyncTdeCertificate -TdeCertificatePassword $script:Pw -WhatIf } |
                Should -Throw '*TdeCertificateBackupPath*'
        }

        It '-SyncTdeCertificate ohne -TdeCertificatePassword wird abgelehnt' {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            { Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'DB1' `
                    -SyncTdeCertificate -TdeCertificateBackupPath $script:TestDir -WhatIf } |
                Should -Throw '*TdeCertificatePassword*'
        }

        It 'Ein nicht vorhandener Ablagepfad wird abgelehnt' {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            { Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'DB1' `
                    -SyncTdeCertificate -TdeCertificateBackupPath 'C:\gibt\es\nicht' `
                    -TdeCertificatePassword $script:Pw -WhatIf } |
                Should -Throw '*nicht gefunden*'
        }
    }

    Context 'Datenbank ohne TDE (unveraendertes Verhalten)' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Get-DbaAvailabilityGroup { [PSCustomObject]@{ Name = 'AG1' } }
            Mock -ModuleName sqmSQLTool Get-DbaAgReplica {
                @([PSCustomObject]@{ Name = 'SQL01'; Role = 'Primary' },
                    [PSCustomObject]@{ Name = 'SQL02'; Role = 'Secondary' })
            }
            Mock -ModuleName sqmSQLTool Get-DbaDatabase { [PSCustomObject]@{ Name = 'PlainDB'; RecoveryModel = 'Full'; IsAccessible = $true } }
            Mock -ModuleName sqmSQLTool Get-DbaAgDatabase { $null }
            Mock -ModuleName sqmSQLTool Remove-DbaDatabase { }
            Mock -ModuleName sqmSQLTool Add-DbaAgDatabase { }
            Mock -ModuleName sqmSQLTool Connect-DbaInstance { [PSCustomObject]@{ VersionMajor = 16 } }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
        }

        It 'Wird ganz normal zur AG hinzugefuegt' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PlainDB' -Confirm:$false
            $r.Status | Should -Be 'Success'
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 1
        }

        It 'Fragt die Zertifikate gar nicht erst ab' {
            Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PlainDB' -Confirm:$false | Out-Null
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'WHERE CONVERT\(varchar\(100\), thumbprint, 1\)' } -Exactly 0
        }
    }

    Context 'TDE-Datenbank auf einem Replica unterhalb SQL 2019' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Get-DbaAvailabilityGroup { [PSCustomObject]@{ Name = 'AG1' } }
            Mock -ModuleName sqmSQLTool Get-DbaAgReplica {
                @([PSCustomObject]@{ Name = 'SQL01'; Role = 'Primary' },
                    [PSCustomObject]@{ Name = 'SQL02'; Role = 'Secondary' })
            }
            Mock -ModuleName sqmSQLTool Get-DbaDatabase { [PSCustomObject]@{ Name = 'PayrollDB'; RecoveryModel = 'Full'; IsAccessible = $true } }
            Mock -ModuleName sqmSQLTool Get-DbaAgDatabase { $null }
            Mock -ModuleName sqmSQLTool Remove-DbaDatabase { }
            Mock -ModuleName sqmSQLTool Add-DbaAgDatabase { }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'dm_database_encryption_keys' } -MockWith {
                [PSCustomObject]@{ DatabaseName = 'PayrollDB'; EncryptionState = 3; CertificateName = 'TDE_Cert'; EncryptorThumbprint = '0xAA11' }
            }
            # Primary SQL 2022, Secondary noch SQL 2016
            Mock -ModuleName sqmSQLTool Connect-DbaInstance { [PSCustomObject]@{ VersionMajor = 16 } }
            Mock -ModuleName sqmSQLTool Connect-DbaInstance -ParameterFilter { "$SqlInstance" -eq 'SQL02' } -MockWith { [PSCustomObject]@{ VersionMajor = 13 } }
        }

        It 'Meldet TdeUnsupportedVersion und nennt das zu alte Replica' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' -Confirm:$false
            $r.Status | Should -Be 'TdeUnsupportedVersion'
            $r.Message | Should -BeLike '*SQL02*'
        }

        It 'Veraendert dabei nichts: kein Drop, kein Add' {
            Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' -Confirm:$false | Out-Null
            Should -Invoke -ModuleName sqmSQLTool Remove-DbaDatabase -Exactly 0
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 0
        }
    }

    Context 'TDE-Datenbank, Zertifikat fehlt auf dem Secondary' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Get-DbaAvailabilityGroup { [PSCustomObject]@{ Name = 'AG1' } }
            Mock -ModuleName sqmSQLTool Get-DbaAgReplica {
                @([PSCustomObject]@{ Name = 'SQL01'; Role = 'Primary' },
                    [PSCustomObject]@{ Name = 'SQL02'; Role = 'Secondary' })
            }
            Mock -ModuleName sqmSQLTool Get-DbaDatabase { [PSCustomObject]@{ Name = 'PayrollDB'; RecoveryModel = 'Full'; IsAccessible = $true } }
            Mock -ModuleName sqmSQLTool Get-DbaAgDatabase { $null }
            Mock -ModuleName sqmSQLTool Remove-DbaDatabase { }
            Mock -ModuleName sqmSQLTool Add-DbaAgDatabase { }
            Mock -ModuleName sqmSQLTool Connect-DbaInstance { [PSCustomObject]@{ VersionMajor = 16 } }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'dm_database_encryption_keys' } -MockWith {
                [PSCustomObject]@{ DatabaseName = 'PayrollDB'; EncryptionState = 3; CertificateName = 'TDE_Cert'; EncryptorThumbprint = '0xAA11' }
            }
        }

        It 'Ohne -SyncTdeCertificate wird uebersprungen, mit Nennung des Knotens' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' -Confirm:$false
            $r.Status | Should -Be 'TdeCertificateMissing'
            $r.Message | Should -BeLike '*SQL02*'
            Should -Invoke -ModuleName sqmSQLTool Remove-DbaDatabase -Exactly 0
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 0
        }

        It 'Mit -SyncTdeCertificate wird exportiert, der Hauptschluessel angelegt und das Zertifikat erzeugt' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' `
                -SyncTdeCertificate -TdeCertificateBackupPath $script:TestDir -TdeCertificatePassword $script:Pw -Confirm:$false
            $r.Status | Should -Be 'Success'
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'BACKUP CERTIFICATE' -and "$SqlInstance" -eq 'SQL01' } -Exactly 1
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'CREATE MASTER KEY' -and "$SqlInstance" -eq 'SQL02' } -Exactly 1
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'CREATE CERTIFICATE' -and "$SqlInstance" -eq 'SQL02' } -Exactly 1
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 1
        }

        It 'Unter -WhatIf wird nichts exportiert und nichts hinzugefuegt' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' `
                -SyncTdeCertificate -TdeCertificateBackupPath $script:TestDir -TdeCertificatePassword $script:Pw -WhatIf
            $r.Status | Should -Be 'TdeSyncSkipped'
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'BACKUP CERTIFICATE' } -Exactly 0
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 0
        }
    }

    Context 'TDE-Datenbank, Zertifikat auf allen Secondaries vorhanden' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Get-DbaAvailabilityGroup { [PSCustomObject]@{ Name = 'AG1' } }
            Mock -ModuleName sqmSQLTool Get-DbaAgReplica {
                @([PSCustomObject]@{ Name = 'SQL01'; Role = 'Primary' },
                    [PSCustomObject]@{ Name = 'SQL02'; Role = 'Secondary' })
            }
            Mock -ModuleName sqmSQLTool Get-DbaDatabase { [PSCustomObject]@{ Name = 'PayrollDB'; RecoveryModel = 'Full'; IsAccessible = $true } }
            Mock -ModuleName sqmSQLTool Get-DbaAgDatabase { $null }
            Mock -ModuleName sqmSQLTool Remove-DbaDatabase { }
            Mock -ModuleName sqmSQLTool Add-DbaAgDatabase { }
            Mock -ModuleName sqmSQLTool Connect-DbaInstance { [PSCustomObject]@{ VersionMajor = 15 } }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'dm_database_encryption_keys' } -MockWith {
                [PSCustomObject]@{ DatabaseName = 'PayrollDB'; EncryptionState = 3; CertificateName = 'TDE_Cert'; EncryptorThumbprint = '0xAA11' }
            }
            # Thumbprint-Treffer auf dem Secondary, dort unter anderem Namen - das genuegt SQL Server
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'WHERE CONVERT\(varchar\(100\), thumbprint, 1\)' } -MockWith {
                [PSCustomObject]@{ name = 'TDE_Cert_Kopie' }
            }
        }

        It 'Wird hinzugefuegt, ohne etwas zu exportieren' {
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' -Confirm:$false
            $r.Status | Should -Be 'Success'
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'BACKUP CERTIFICATE' } -Exactly 0
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 1
        }
    }

    Context 'Sonderfaelle der Verschluesselung' {
        BeforeAll {
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            Mock -ModuleName sqmSQLTool Get-DbaAvailabilityGroup { [PSCustomObject]@{ Name = 'AG1' } }
            Mock -ModuleName sqmSQLTool Get-DbaAgReplica {
                @([PSCustomObject]@{ Name = 'SQL01'; Role = 'Primary' },
                    [PSCustomObject]@{ Name = 'SQL02'; Role = 'Secondary' })
            }
            Mock -ModuleName sqmSQLTool Get-DbaDatabase { [PSCustomObject]@{ Name = 'PayrollDB'; RecoveryModel = 'Full'; IsAccessible = $true } }
            Mock -ModuleName sqmSQLTool Get-DbaAgDatabase { $null }
            Mock -ModuleName sqmSQLTool Remove-DbaDatabase { }
            Mock -ModuleName sqmSQLTool Add-DbaAgDatabase { }
            Mock -ModuleName sqmSQLTool Connect-DbaInstance { [PSCustomObject]@{ VersionMajor = 16 } }
        }

        It 'Asymmetrischer Schluessel (EKM) wird als solcher gemeldet, nicht als fehlendes Zertifikat' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'dm_database_encryption_keys' } -MockWith {
                [PSCustomObject]@{ DatabaseName = 'PayrollDB'; EncryptionState = 3; CertificateName = [System.DBNull]::Value; EncryptorThumbprint = '0xBB22' }
            }
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' -Confirm:$false
            $r.Status | Should -Be 'TdeEncryptorNotCertificate'
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 0
        }

        It 'Gleicher Name mit abweichendem Thumbprint wird nicht ueberschrieben' {
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @() }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'dm_database_encryption_keys' } -MockWith {
                [PSCustomObject]@{ DatabaseName = 'PayrollDB'; EncryptionState = 3; CertificateName = 'TDE_Cert'; EncryptorThumbprint = '0xAA11' }
            }
            Mock -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'WHERE name = N' } -MockWith {
                [PSCustomObject]@{ Thumb = '0xCC33' }
            }
            $r = Add-sqmDatabaseToAG -SqlInstance 'SQL01' -AvailabilityGroup 'AG1' -Database 'PayrollDB' `
                -SyncTdeCertificate -TdeCertificateBackupPath $script:TestDir -TdeCertificatePassword $script:Pw -Confirm:$false
            $r.Status | Should -Be 'TdeCertificateNameConflict'
            $r.Message | Should -BeLike '*0xCC33*'
            Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -ParameterFilter { $Query -match 'CREATE CERTIFICATE' } -Exactly 0
            Should -Invoke -ModuleName sqmSQLTool Add-DbaAgDatabase -Exactly 0
        }
    }
}