tests/Unit/Public/DbOwnerAndMasterObjects.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer Get-sqmDbOwnerRisk, Repair-sqmDbOwnerRisk, Set-sqmDatabaseOwner und
    Remove-sqmMasterDbCustomObjects.
.DESCRIPTION
    dbatools wird gemockt, alle an Invoke-DbaQuery uebergebenen Abfragen werden mitgeschrieben.
    Schwerpunkt: Bewertung (OK/Warning/Critical), -WhatIf aendert nichts, und Bezeichner mit ']'
    werden als ']]' escaped. Letzteres war in 1.9.182.0 kaputt: die Ersetzung '\]\]' liefert
    woertlich '\]\]' und damit ungueltiges SQL fuer jeden Namen mit ']'.
#>


BeforeAll {
    . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1"
    Import-sqmTestModule
    InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true }
    $script:TestDir = New-TempTestDirectory
}

AfterAll {
    Remove-Variable -Name SqmQueries -Scope Global -ErrorAction SilentlyContinue
    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 'Get-sqmDbOwnerRisk' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Get-sqmDatabaseTrustIsolationMap { @{} }
        Mock -ModuleName sqmSQLTool Get-DbaDatabase {
            @(
                [pscustomobject]@{ Name = 'DbOk';   IsSystemObject = $false }
                [pscustomobject]@{ Name = 'DbCrit'; IsSystemObject = $false }
                [pscustomobject]@{ Name = 'DbWarn'; IsSystemObject = $false }
                [pscustomobject]@{ Name = 'DbSvc';  IsSystemObject = $false }
                [pscustomobject]@{ Name = 'master'; IsSystemObject = $true }
                [pscustomobject]@{ Name = 'tempdb'; IsSystemObject = $true }
            )
        }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            @(
                [pscustomobject]@{ DatabaseName = 'DbOk';   IsTrustworthyOn = $false; DbOwnerLogin = 'sa';   OwnerIsSysAdmin = 1 }
                [pscustomobject]@{ DatabaseName = 'DbCrit'; IsTrustworthyOn = $true;  DbOwnerLogin = 'sa';   OwnerIsSysAdmin = 1 }
                [pscustomobject]@{ DatabaseName = 'DbWarn'; IsTrustworthyOn = $false; DbOwnerLogin = 'app';  OwnerIsSysAdmin = 0 }
                [pscustomobject]@{ DatabaseName = 'DbSvc';  IsTrustworthyOn = $true;  DbOwnerLogin = 'sa';   OwnerIsSysAdmin = 1 }
                [pscustomobject]@{ DatabaseName = 'master'; IsTrustworthyOn = $false; DbOwnerLogin = 'sa';   OwnerIsSysAdmin = 1 }
            )
        } -ParameterFilter { $Query -like '*is_trustworthy_on*' }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            switch ("$Database")
            {
                'DbCrit' { [pscustomobject]@{ MemberName = 'dbo' }, [pscustomobject]@{ MemberName = 'app_user' } }
                'DbWarn' { [pscustomobject]@{ MemberName = 'app_user' } }
                'DbSvc'  { [pscustomobject]@{ MemberName = 'svc_backup' }, [pscustomobject]@{ MemberName = 'dbo' } }
                'master' { [pscustomobject]@{ MemberName = 'x' } }
                default  { [pscustomobject]@{ MemberName = 'dbo' } }
            }
        } -ParameterFilter { $Query -like '*database_role_members*' }
    }

    It 'bewertet OK, Warning und Critical' {
        $r = Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf
        ($r | Where-Object DatabaseName -eq 'DbOk').Severity   | Should -Be 'OK'
        ($r | Where-Object DatabaseName -eq 'DbWarn').Severity | Should -Be 'Warning'
        $crit = $r | Where-Object DatabaseName -eq 'DbCrit'
        $crit.Severity | Should -Be 'Critical'
        $crit.EscalationPossible | Should -BeTrue
        $crit.DbOwnerMembers | Should -Be @('app_user')
    }

    It 'dbo zaehlt nicht als Mitglied' {
        $r = Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf
        ($r | Where-Object DatabaseName -eq 'DbOk').MemberCount | Should -Be 0
    }

    It '-ExcludeLogin nimmt Mitglieder aus der Bewertung' {
        $r = Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -ExcludeLogin 'svc_*' -WhatIf
        ($r | Where-Object DatabaseName -eq 'DbSvc').Severity | Should -Be 'OK'
        $r2 = Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf
        ($r2 | Where-Object DatabaseName -eq 'DbSvc').Severity | Should -Be 'Critical'
    }

    It 'laesst Systemdatenbanken weg, tempdb immer' {
        $names = (Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf).DatabaseName
        $names | Should -Not -Contain 'master'
        $namesAll = (Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -IncludeSystemDatabases -WhatIf).DatabaseName
        $namesAll | Should -Contain 'master'
        $namesAll | Should -Not -Contain 'tempdb'
    }

    It '-Database und -ExcludeDatabase arbeiten mit Wildcards' {
        (Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -Database 'DbC*' -WhatIf).DatabaseName | Should -Be @('DbCrit')
        (Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -ExcludeDatabase 'Db*' -WhatIf) | Should -BeNullOrEmpty
    }

    It 'wirft bei Verbindungsfehler, ausser mit -ContinueOnError' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery { throw 'nicht erreichbar' } -ParameterFilter { $Query -like '*is_trustworthy_on*' }
        { Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf } | Should -Throw '*nicht erreichbar*'
        { Get-sqmDbOwnerRisk -SqlInstance 'SQL01' -WhatIf -ContinueOnError } | Should -Not -Throw
    }
}

Describe 'Repair-sqmDbOwnerRisk' {
    BeforeEach {
        $global:SqmQueries = [System.Collections.Generic.List[string]]::new()
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Copy-sqmToCentralPath { }
        Mock -ModuleName sqmSQLTool Get-DbaDatabase {
            @([pscustomobject]@{ Name = 'Sales'; IsSystemObject = $false }, [pscustomobject]@{ Name = 'Clean'; IsSystemObject = $false })
        }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*database_role_members*')
            {
                if ("$Database" -eq 'Sales') { [pscustomobject]@{ MemberName = 'dbo' }, [pscustomobject]@{ MemberName = 'app]user' } }
                else { [pscustomobject]@{ MemberName = 'dbo' } }
            }
            elseif ($Query -like '*database_principals WHERE name*') { [pscustomobject]@{ Found = 1 } }
            elseif ($Query -like '*sys.procedures*') { [pscustomobject]@{ SchemaName = 'sch]ema'; ProcName = 'p]roc' } }
        }
    }

    It 'escaped ] in Bezeichnern als ]] und nicht als \]\]' {
        $r = Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false
        $all = $global:SqmQueries -join "`n"
        $all | Should -Match ([regex]::Escape('ALTER ROLE db_owner DROP MEMBER [app]]user];'))
        $all | Should -Match ([regex]::Escape('GRANT EXECUTE ON OBJECT::[sch]]ema].[p]]roc] TO [db_execute];'))
        $all | Should -Not -Match '\\\]'
        ($r | Where-Object LoginName -eq 'app]user').Status | Should -Be 'OK'
    }

    It 'escaped auch einen Rollennamen mit ]' {
        Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -ExecuteRoleName 'exec]role' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        ($global:SqmQueries -join "`n") | Should -Match ([regex]::Escape('ALTER ROLE [exec]]role] ADD MEMBER [app]]user];'))
    }

    It 'fasst Datenbanken ohne zusaetzliche db_owner-Mitglieder nicht an' {
        $r = Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -Database 'Clean' -OutputPath $script:TestDir -Confirm:$false
        $r | Should -BeNullOrEmpty
        ($global:SqmQueries | Where-Object { $_ -like '*ALTER ROLE*' }) | Should -BeNullOrEmpty
    }

    It '-WhatIf fuehrt keine aendernde Abfrage aus' {
        Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -OutputPath $script:TestDir -WhatIf | Out-Null
        ($global:SqmQueries | Where-Object { $_ -match 'ALTER ROLE|GRANT |CREATE ROLE' }) | Should -BeNullOrEmpty
    }

    It 'legt die Ausfuehrungsrolle an, wenn sie fehlt' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*database_role_members*') { [pscustomobject]@{ MemberName = 'app_user' } }
        }
        Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -Database 'Sales' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        ($global:SqmQueries | Where-Object { $_ -like 'CREATE ROLE [[]db_execute] AUTHORIZATION [[]dbo];' }) | Should -Not -BeNullOrEmpty
    }

    It 'meldet Failed pro Mitglied, wenn die Zuweisung scheitert' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*database_role_members*') { [pscustomobject]@{ MemberName = 'app_user' } }
            elseif ($Query -like '*database_principals WHERE name*') { [pscustomobject]@{ Found = 1 } }
            elseif ($Query -like 'ALTER ROLE*') { throw 'Berechtigung fehlt' }
        }
        $r = Repair-sqmDbOwnerRisk -SqlInstance 'SQL01' -Database 'Sales' -OutputPath $script:TestDir -Confirm:$false
        $r.Status | Should -Be 'Failed'
        $r.Message | Should -Match 'Berechtigung fehlt'
    }
}

Describe 'Set-sqmDatabaseOwner' {
    BeforeEach {
        $global:SqmQueries = [System.Collections.Generic.List[string]]::new()
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Copy-sqmToCentralPath { }
        Mock -ModuleName sqmSQLTool Get-DbaDatabase {
            @(
                [pscustomobject]@{ Name = 'Sales';    Owner = 'DOM\alice'; IsSystemObject = $false }
                [pscustomobject]@{ Name = 'Already';  Owner = 'sa';        IsSystemObject = $false }
                [pscustomobject]@{ Name = 'We]ird';   Owner = 'DOM\bob';   IsSystemObject = $false }
                [pscustomobject]@{ Name = 'master';   Owner = 'sa';        IsSystemObject = $true }
            )
        }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*sid = 0x01*') { [pscustomobject]@{ name = 'sa' } }
            elseif ($Query -like '*FROM sys.server_principals WHERE name = N*') { [pscustomobject]@{ name = 'found' } }
        }
    }

    It 'setzt den Owner auf den sa-Account (SID 0x01), wenn kein Login angegeben ist' {
        $r = Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false
        ($r | Where-Object DatabaseName -eq 'Sales').NewOwner | Should -Be 'sa'
        ($r | Where-Object DatabaseName -eq 'Sales').Status | Should -Be 'OK'
        $global:SqmQueries | Should -Contain 'ALTER AUTHORIZATION ON DATABASE::[Sales] TO [sa]'
    }

    It 'ueberspringt Datenbanken, die schon dem Ziel gehoeren, ausser mit -Force' {
        $r = Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false
        ($r | Where-Object DatabaseName -eq 'Already').Status | Should -Be 'Skipped'
        $rForce = Set-sqmDatabaseOwner -SqlInstance 'SQL01' -Force -OutputPath $script:TestDir -Confirm:$false
        ($rForce | Where-Object DatabaseName -eq 'Already').Status | Should -Be 'OK'
    }

    It 'laesst Systemdatenbanken aus' {
        (Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false).DatabaseName |
            Should -Not -Contain 'master'
    }

    It 'escaped ] im Datenbanknamen und im Ziel-Login' {
        Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OwnerLogin 'DOM\a]b' -Database 'We]ird' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        $global:SqmQueries | Should -Contain 'ALTER AUTHORIZATION ON DATABASE::[We]]ird] TO [DOM\a]]b]'
    }

    It '-WhatIf aendert nichts und meldet WhatIf' {
        $r = Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OutputPath $script:TestDir -WhatIf
        ($r | Where-Object DatabaseName -eq 'Sales').Status | Should -Be 'WhatIf'
        ($global:SqmQueries | Where-Object { $_ -like 'ALTER AUTHORIZATION*' }) | Should -BeNullOrEmpty
    }

    It 'bricht ab, wenn der Ziel-Login nicht existiert' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*sid = 0x01*') { [pscustomobject]@{ name = 'sa' } }
        }
        { Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OwnerLogin 'gibt\es\nicht' -OutputPath $script:TestDir -Confirm:$false -EnableException } |
            Should -Throw '*existiert nicht*'
        ($global:SqmQueries | Where-Object { $_ -like 'ALTER AUTHORIZATION*' }) | Should -BeNullOrEmpty
    }

    It 'meldet Failed und macht mit der naechsten Datenbank weiter' {
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*sid = 0x01*') { [pscustomobject]@{ name = 'sa' } }
            elseif ($Query -like '*FROM sys.server_principals WHERE name = N*') { [pscustomobject]@{ name = 'found' } }
            elseif ($Query -like '*[[]Sales]*') { throw 'Zugriff verweigert' }
        }
        $r = Set-sqmDatabaseOwner -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false
        ($r | Where-Object DatabaseName -eq 'Sales').Status   | Should -Be 'Failed'
        ($r | Where-Object DatabaseName -eq 'We]ird').Status  | Should -Be 'OK'
    }
}

Describe 'Remove-sqmMasterDbCustomObjects' {
    BeforeEach {
        $global:SqmQueries = [System.Collections.Generic.List[string]]::new()
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        Mock -ModuleName sqmSQLTool Copy-sqmToCentralPath { }
        Mock -ModuleName sqmSQLTool Get-sqmConfig { @('sqm_BackupExclude') } -ParameterFilter { $Key -eq 'MasterDbObjectWhitelist' }
        Mock -ModuleName sqmSQLTool Invoke-DbaQuery {
            $global:SqmQueries.Add($Query)
            if ($Query -like '*FROM sys.objects*')
            {
                @(
                    [pscustomobject]@{ SchemaName = 'dbo';    ObjectName = 'sqm_BackupExclude'; ObjectTypeCode = 'U '; ObjectTypeDesc = 'USER_TABLE' }
                    [pscustomobject]@{ SchemaName = 'dbo';    ObjectName = 'my]tbl';            ObjectTypeCode = 'U ';  ObjectTypeDesc = 'USER_TABLE' }
                    [pscustomobject]@{ SchemaName = 'we]ird'; ObjectName = 'proc1';             ObjectTypeCode = 'P ';  ObjectTypeDesc = 'SQL_STORED_PROCEDURE' }
                    [pscustomobject]@{ SchemaName = 'dbo';    ObjectName = 'v1';                ObjectTypeCode = 'V ';  ObjectTypeDesc = 'VIEW' }
                    [pscustomobject]@{ SchemaName = 'dbo';    ObjectName = 'f1';                ObjectTypeCode = 'FN';  ObjectTypeDesc = 'SQL_SCALAR_FUNCTION' }
                )
            }
        }
    }

    It 'baut je Objekttyp das passende DROP und escaped ]' {
        Remove-sqmMasterDbCustomObjects -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        $global:SqmQueries | Should -Contain 'DROP TABLE [dbo].[my]]tbl];'
        $global:SqmQueries | Should -Contain 'DROP PROCEDURE [we]]ird].[proc1];'
        $global:SqmQueries | Should -Contain 'DROP VIEW [dbo].[v1];'
        $global:SqmQueries | Should -Contain 'DROP FUNCTION [dbo].[f1];'
    }

    It 'loescht Objekte der Whitelist nie' {
        Remove-sqmMasterDbCustomObjects -SqlInstance 'SQL01' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        ($global:SqmQueries | Where-Object { $_ -like '*sqm_BackupExclude*' -and $_ -like 'DROP*' }) | Should -BeNullOrEmpty
    }

    It '-ExcludeObject und -ObjectName begrenzen die Auswahl' {
        Remove-sqmMasterDbCustomObjects -SqlInstance 'SQL01' -ObjectName 'v*', 'f*' -ExcludeObject 'f1' -OutputPath $script:TestDir -Confirm:$false | Out-Null
        $drops = @($global:SqmQueries | Where-Object { $_ -like 'DROP*' })
        $drops | Should -Be @('DROP VIEW [dbo].[v1];')
    }

    It '-WhatIf loescht nichts' {
        Remove-sqmMasterDbCustomObjects -SqlInstance 'SQL01' -OutputPath $script:TestDir -WhatIf | Out-Null
        ($global:SqmQueries | Where-Object { $_ -like 'DROP*' }) | Should -BeNullOrEmpty
    }
}