tests/Unit/Public/Move-sqmDatabaseFile.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer Move-sqmDatabaseFile
 
.DESCRIPTION
    Geprueft wird vor allem, was NICHT passieren darf: kein ALTER DATABASE, wenn eine Vorpruefung
    scheitert, kein Offline-Setzen einer AG-Datenbank, kein Entfernen alter Dateien, solange der
    neue Ort nicht bestaetigt ist. Genau dort entscheidet sich, ob ein Dateiumzug eine Instanz
    stehen laesst oder nicht.
#>


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

AfterAll {
    if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force }
    $env:MSSQLTOOLS_SKIP_AUTO_UPDATE = $null
}

Describe 'Move-sqmDatabaseFile - Vertrag' {

    It 'Funktion existiert und ist exportiert' {
        Get-Command Move-sqmDatabaseFile -Module sqmSQLTool | Should -Not -BeNullOrEmpty
    }

    It 'Database ist Mandatory' {
        (Get-Command Move-sqmDatabaseFile).Parameters['Database'].Attributes.Mandatory | Should -Contain $true
    }

    It 'FileDestination ist Mandatory' {
        (Get-Command Move-sqmDatabaseFile).Parameters['FileDestination'].Attributes.Mandatory | Should -Contain $true
    }

    It 'Unterstuetzt -WhatIf' {
        (Get-Command Move-sqmDatabaseFile).Parameters.ContainsKey('WhatIf') | Should -Be $true
    }

    It 'FileType laesst nur All/Data/Log zu' {
        $set = (Get-Command Move-sqmDatabaseFile).Parameters['FileType'].Attributes |
            Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
        $set.ValidValues | Should -Be @('All', 'Data', 'Log')
    }

    It 'Nutzt nicht Move-DbaDbFile (lehnt Systemdatenbanken ab)' {
        $source = Get-Content "$PSScriptRoot\..\..\..\Public\Move-sqmDatabaseFile.ps1" -Raw
        $source | Should -Not -Match 'Move-DbaDbFile\s+-SqlInstance'
    }
}

Describe 'Move-sqmDatabaseFile - Sperren vor jeder Aenderung' {

    It 'lehnt master ab und setzt kein ALTER DATABASE ab' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'master' -FileDestination 'G:\Data' -Confirm:$false

            $r.Status | Should -Contain 'Blocked'
            ($r | Where-Object Status -eq 'Blocked').Message | Should -BeLike '*Startparameter*'
            Should -Not -Invoke Invoke-DbaQuery
        }
    }

    It 'lehnt -NoRestart fuer msdb ab (Pfad umgetragen, Datei noch am alten Ort)' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'msdb' -FileDestination 'G:\Data' -NoRestart -Confirm:$false

            $r.Status | Should -Contain 'Blocked'
            Should -Not -Invoke Invoke-DbaQuery
        }
    }

    It 'bricht bei einer AG-Datenbank ab, ohne sie offline zu setzen' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'SalesDB'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Get-sqmDatabaseAgMembership {
                [PSCustomObject]@{ HadrEnabled = $true; IsAgDatabase = $true; AvailabilityGroupName = 'AG_Prod'; PrimaryReplica = 'SQL01' }
            }
            Mock Set-DbaDbState { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            $r.Status | Should -Contain 'Blocked'
            ($r | Where-Object Status -eq 'Blocked').Message | Should -BeLike '*AG_Prod*'
            Should -Not -Invoke Set-DbaDbState
        }
    }

    It 'bricht ab, wenn die AG-Frage gar nicht beantwortet werden kann' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'SalesDB'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Get-sqmDatabaseAgMembership { throw 'VIEW ANY DEFINITION denied' }
            Mock Set-DbaDbState { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            $r.Status | Should -Contain 'Blocked'
            Should -Not -Invoke Set-DbaDbState
        }
    }

    It 'bricht ab, wenn die Schreibprobe im Ziel scheitert - ohne ALTER DATABASE' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'tempdb'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                [PSCustomObject]@{ FileId = 1; LogicalName = 'tempdev'; PhysicalName = 'C:\Data\tempdb.mdf'; TypeDesc = 'ROWS'; SizeMB = 512 }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'CREATE DATABASE' } -MockWith { throw "Operating system error 5(Access is denied.)" }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'DROP DATABASE' } -MockWith { }
            Mock Test-DbaPath { $true }
            Mock Restart-DbaService { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -Confirm:$false

            ($r | Where-Object { $_.Step -eq 'Precheck' -and $_.Status -eq 'Failed' }).Message |
                Should -BeLike '*Schreibprobe*'
            $r.Status | Should -Contain 'Aborted'
            Should -Not -Invoke Invoke-DbaQuery -ParameterFilter { $Query -match 'MODIFY FILE' }
            Should -Not -Invoke Restart-DbaService
        }
    }

    It 'bricht ab, wenn am Ziel zu wenig Platz frei ist' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'tempdb'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                [PSCustomObject]@{ FileId = 1; LogicalName = 'tempdev'; PhysicalName = 'C:\Data\tempdb.mdf'; TypeDesc = 'ROWS'; SizeMB = 10000 }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'xp_fixeddrives' } -MockWith {
                [PSCustomObject]@{ drive = 'G'; 'MB free' = 500 }
            }
            Mock Invoke-DbaQuery { }
            Mock Get-DbaDiskSpace { throw 'WMI nicht erreichbar' }
            Mock Test-DbaPath { $true }
            Mock Restart-DbaService { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -SkipWriteProbe -Confirm:$false

            ($r | Where-Object { $_.Step -eq 'Precheck' -and $_.Status -eq 'Failed' }).Message |
                Should -BeLike '*Zu wenig Platz*'
            Should -Not -Invoke Invoke-DbaQuery -ParameterFilter { $Query -match 'MODIFY FILE' }
            Should -Not -Invoke Restart-DbaService
        }
    }

    It 'ueberspringt Dateien, die bereits am Ziel liegen' {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'tempdb'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                [PSCustomObject]@{ FileId = 1; LogicalName = 'tempdev'; PhysicalName = 'G:\TempDB\tempdb.mdf'; TypeDesc = 'ROWS'; SizeMB = 512 }
            }
            Mock Invoke-DbaQuery { }
            Mock Test-DbaPath { $true }
            Mock Restart-DbaService { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -Confirm:$false

            $r.Status | Should -Contain 'AlreadyInPlace'
            Should -Not -Invoke Restart-DbaService
        }
    }
}

Describe 'Move-sqmDatabaseFile - tempdb' {

    BeforeEach {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery { }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'tempdb'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                @(
                    [PSCustomObject]@{ FileId = 1; LogicalName = 'tempdev'; PhysicalName = 'C:\Data\tempdb.mdf'; TypeDesc = 'ROWS'; SizeMB = 512 }
                    [PSCustomObject]@{ FileId = 2; LogicalName = 'templog'; PhysicalName = 'C:\Data\templog.ldf'; TypeDesc = 'LOG'; SizeMB = 128 }
                )
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'SELECT 1 AS Up' } -MockWith { [PSCustomObject]@{ Up = 1 } }
            Mock Test-DbaPath { $true }
            Mock Get-DbaDiskSpace {
                [PSCustomObject]@{ Name = 'G:\'; Free = [PSCustomObject]@{ Megabyte = 100000 } }
            }
            Mock Get-DbaService { [PSCustomObject]@{ State = 'Running'; StartName = 'NT Service\MSSQLSERVER' } }
            Mock Restart-DbaService { }
            Mock Start-DbaService { }
            Mock Stop-DbaService { }
            Mock Copy-Item { }
            Mock Remove-Item { }
            Mock Test-Path { $true }
        }
    }

    It 'traegt jede Datei per MODIFY FILE um, startet neu und entfernt die alten Dateien' {
        InModuleScope sqmSQLTool {
            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -Confirm:$false

            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter {
                $Query -match 'MODIFY FILE' -and $Query -match "NAME = N'tempdev'" -and $Query -match 'G:\\TempDB\\tempdb\.mdf'
            }
            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter {
                $Query -match 'MODIFY FILE' -and $Query -match "NAME = N'templog'"
            }
            Should -Invoke Restart-DbaService -Times 1 -Exactly
            # tempdb wird beim Start neu angelegt - es darf nichts kopiert werden
            Should -Not -Invoke Copy-Item
            Should -Invoke Remove-Item -Times 2 -Exactly

            ($r | Where-Object Step -eq 'Cleanup').Status | Should -Be @('Success', 'Success')
        }
    }

    It 'trennt Daten- und Logziel, wenn -LogFileDestination angegeben ist' {
        InModuleScope sqmSQLTool {
            $null = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' `
                -FileDestination 'G:\TempDB' -LogFileDestination 'H:\TempLog' -Confirm:$false

            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter {
                $Query -match 'MODIFY FILE' -and $Query -match 'H:\\TempLog\\templog\.ldf'
            }
        }
    }

    It 'verschiebt mit -LogicalFileName nur die genannte Datei' {
        InModuleScope sqmSQLTool {
            $null = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' `
                -FileDestination 'G:\TempDB' -LogicalFileName 'templog' -Confirm:$false

            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter { $Query -match 'MODIFY FILE' }
            Should -Invoke Invoke-DbaQuery -Times 0 -Exactly -ParameterFilter {
                $Query -match 'MODIFY FILE' -and $Query -match "NAME = N'tempdev'"
            }
        }
    }

    It 'startet mit -NoRestart nicht neu und entfernt nichts' {
        InModuleScope sqmSQLTool {
            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -NoRestart -Confirm:$false

            $r.Status | Should -Contain 'PendingRestart'
            Should -Not -Invoke Restart-DbaService
            Should -Not -Invoke Remove-Item
        }
    }

    It 'laesst die alten Dateien mit -KeepOldFiles liegen' {
        InModuleScope sqmSQLTool {
            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -KeepOldFiles -Confirm:$false

            Should -Invoke Restart-DbaService -Times 1 -Exactly
            Should -Not -Invoke Remove-Item
            ($r | Where-Object Step -eq 'Cleanup').Status | Should -Be @('Skipped', 'Skipped')
        }
    }

    It 'entfernt keine alte Datei, wenn die neue nach dem Neustart fehlt' {
        InModuleScope sqmSQLTool {
            # Zielverzeichnis sichtbar, die neue Datei danach aber nicht
            Mock Test-DbaPath -ParameterFilter { $Path -match '\.(mdf|ldf)$' } -MockWith { $false }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -Confirm:$false

            Should -Not -Invoke Remove-Item
            ($r | Where-Object { $_.Step -eq 'Move' -and $_.Status -eq 'Failed' }).Count | Should -Be 2
        }
    }

    It 'meldet einen fehlgeschlagenen Neustart und raeumt nicht auf' {
        InModuleScope sqmSQLTool {
            Mock Restart-DbaService { throw 'Zugriff verweigert (WMI)' }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -Confirm:$false

            ($r | Where-Object { $_.Step -eq 'Restart' -and $_.Status -eq 'Failed' }).Message |
                Should -BeLike '*sys.master_files*'
            Should -Not -Invoke Remove-Item
        }
    }

    It 'aendert mit -WhatIf nichts' {
        InModuleScope sqmSQLTool {
            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'tempdb' -FileDestination 'G:\TempDB' -WhatIf

            $r.Status | Should -Contain 'WhatIfSkipped'
            Should -Not -Invoke Invoke-DbaQuery -ParameterFilter { $Query -match 'MODIFY FILE' }
            Should -Not -Invoke Restart-DbaService
            Should -Not -Invoke Remove-Item
        }
    }
}

Describe 'Move-sqmDatabaseFile - Benutzerdatenbank' {

    BeforeEach {
        InModuleScope sqmSQLTool {
            Mock Invoke-sqmLogging { }
            Mock Connect-DbaInstance { [PSCustomObject]@{ ComputerName = $env:COMPUTERNAME; IsClustered = $false } }
            Mock Invoke-DbaQuery { }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith {
                [PSCustomObject]@{ DbName = 'SalesDB'; StateDesc = 'ONLINE'; IsReadOnly = $false; SourceDatabaseId = $null }
            }
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                [PSCustomObject]@{ FileId = 1; LogicalName = 'SalesDB'; PhysicalName = 'C:\Data\SalesDB.mdf'; TypeDesc = 'ROWS'; SizeMB = 2048 }
            }
            Mock Get-sqmDatabaseAgMembership {
                [PSCustomObject]@{ HadrEnabled = $false; IsAgDatabase = $false; AvailabilityGroupName = $null; PrimaryReplica = $null }
            }
            Mock Test-DbaPath { $true }
            Mock Get-DbaDiskSpace { [PSCustomObject]@{ Name = 'G:\'; Free = [PSCustomObject]@{ Megabyte = 100000 } } }
            Mock Set-DbaDbState { [PSCustomObject]@{ Status = 'OFFLINE' } }
            Mock Copy-Item { }
            Mock Get-Item { [PSCustomObject]@{ Length = 2147483648 } }
            Mock Remove-Item { }
            Mock Test-Path { $true }
            Mock Restart-DbaService { }
        }
    }

    It 'faehrt OFFLINE -> kopieren -> MODIFY FILE -> ONLINE und raeumt danach auf' {
        InModuleScope sqmSQLTool {
            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            Should -Invoke Set-DbaDbState -Times 1 -Exactly -ParameterFilter { $Offline -eq $true }
            Should -Invoke Copy-Item -Times 1 -Exactly
            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter { $Query -match 'MODIFY FILE' }
            Should -Invoke Set-DbaDbState -Times 1 -Exactly -ParameterFilter { $Online -eq $true }
            Should -Invoke Remove-Item -Times 1 -Exactly
            # Eine Benutzerdatenbank wird nie ueber einen Dienst-Neustart verschoben
            Should -Not -Invoke Restart-DbaService

            ($r | Where-Object { $_.Step -eq 'Move' -and $_.LogicalName -eq 'SalesDB' }).Status | Should -Be 'Success'
        }
    }

    It 'setzt den Pfad zurueck und raeumt nicht auf, wenn das Kopieren scheitert' {
        InModuleScope sqmSQLTool {
            Mock Copy-Item { throw 'Auf dem Datentraeger ist nicht genuegend Speicherplatz.' }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            ($r | Where-Object { $_.Step -eq 'Move' -and $_.Status -eq 'Failed' }).Message | Should -BeLike '*Speicherplatz*'
            Should -Not -Invoke Remove-Item
            # Datenbank muss trotzdem wieder online gehen
            Should -Invoke Set-DbaDbState -Times 1 -Exactly -ParameterFilter { $Online -eq $true }
        }
    }

    It 'setzt ein bereits umgetragenes MODIFY FILE zurueck, wenn eine spaetere Datei scheitert' {
        InModuleScope sqmSQLTool {
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.master_files' } -MockWith {
                @(
                    [PSCustomObject]@{ FileId = 1; LogicalName = 'SalesDB'; PhysicalName = 'C:\Data\SalesDB.mdf'; TypeDesc = 'ROWS'; SizeMB = 2048 }
                    [PSCustomObject]@{ FileId = 2; LogicalName = 'SalesDB_log'; PhysicalName = 'C:\Data\SalesDB_log.ldf'; TypeDesc = 'LOG'; SizeMB = 512 }
                )
            }
            $script:copyCount = 0
            Mock Copy-Item {
                $script:copyCount++
                if ($script:copyCount -ge 2) { throw 'Zugriff verweigert' }
            }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            $r.Status | Should -Contain 'RolledBack'
            Should -Invoke Invoke-DbaQuery -Times 1 -Exactly -ParameterFilter {
                $Query -match 'MODIFY FILE' -and $Query -match 'C:\\Data\\SalesDB\.mdf'
            }
            Should -Not -Invoke Remove-Item
        }
    }

    It 'bricht ab, wenn die Datenbank nicht offline gesetzt werden kann' {
        InModuleScope sqmSQLTool {
            Mock Set-DbaDbState { throw 'Database is in use' }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'SalesDB' -FileDestination 'G:\Data' -Confirm:$false

            ($r | Where-Object Status -eq 'Failed').Message | Should -BeLike '*offline*'
            Should -Not -Invoke Copy-Item
            Should -Not -Invoke Remove-Item
        }
    }

    It 'meldet eine nicht existierende Datenbank als NotFound' {
        InModuleScope sqmSQLTool {
            Mock Invoke-DbaQuery -ParameterFilter { $Query -match 'sys\.databases' } -MockWith { }

            $r = Move-sqmDatabaseFile -SqlInstance 'SQL01' -Database 'GibtsNicht' -FileDestination 'G:\Data' -Confirm:$false

            $r.Status | Should -Contain 'NotFound'
            Should -Not -Invoke Copy-Item
        }
    }
}