tests/Unit/Public/Invoke-sqmDatabaseStandardization.Tests.ps1
|
#Requires -Modules Pester <# .SYNOPSIS Unit Tests fuer Invoke-sqmDatabaseStandardization und den Aufruf aus Invoke-sqmRestoreDatabase. .DESCRIPTION Geprueft wird vor allem, was NICHT passieren darf: kein ALTER an Secondary-, Offline- oder Read-only-Datenbanken, kein Absenken der Kompatibilitaetsstufe, kein DROP eines Windows-Users, der ueber einen Gruppen-Login Zugriff hat, und kein stilles Verschlucken eines gescheiterten DROP USER (der alte Restore-Schritt lief mit -ErrorAction SilentlyContinue). Die SQL-Haelfte ist zusaetzlich live gegen DEV01 (SQL 2022) geprueft. #> BeforeAll { . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1" Import-sqmTestModule } AfterAll { if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force } $env:MSSQLTOOLS_SKIP_AUTO_UPDATE = $null } Describe 'Invoke-sqmDatabaseStandardization - Vertrag' { It 'Funktion existiert und ist exportiert' { Get-Command Invoke-sqmDatabaseStandardization -Module sqmSQLTool | Should -Not -BeNullOrEmpty } It 'Unterstuetzt -WhatIf' { (Get-Command Invoke-sqmDatabaseStandardization).Parameters.ContainsKey('WhatIf') | Should -Be $true } It 'Jeder Schritt laesst sich einzeln abschalten' { $p = (Get-Command Invoke-sqmDatabaseStandardization).Parameters foreach ($name in 'SkipOrphanRepair', 'SkipUserRemoval', 'SkipCompatibilityLevel', 'SkipTargetRecoveryTime', 'SkipOwner') { $p.ContainsKey($name) | Should -Be $true -Because $name } } It 'TargetRecoveryTimeSeconds ist standardmaessig 60' { $source = Get-Content "$PSScriptRoot\..\..\..\Public\Invoke-sqmDatabaseStandardization.ps1" -Raw $source | Should -Match '\[int\]\$TargetRecoveryTimeSeconds = 60' } It 'DROP USER laeuft nicht mit SilentlyContinue' { $source = Get-Content "$PSScriptRoot\..\..\..\Public\Invoke-sqmDatabaseStandardization.ps1" -Raw $source | Should -Not -Match 'SilentlyContinue' } } Describe 'Invoke-sqmDatabaseStandardization - Verhalten' { BeforeEach { InModuleScope sqmSQLTool { $script:stdQueries = [System.Collections.Generic.List[string]]::new() $script:stdDbRow = [PSCustomObject]@{ name = 'AppDb'; state_desc = 'ONLINE'; is_read_only = 0; compatibility_level = 130 target_recovery_time_in_seconds = 0; owner_is_sa = 0; owner_name = 'olduser'; is_ag_secondary = 0 } $script:stdCandidates = @() $script:stdOrphans = @() $script:stdLoginInfo = @() $script:stdDropFails = $false } } BeforeAll { InModuleScope sqmSQLTool { function script:Set-StdMocks { Mock Invoke-sqmLogging { } Mock Copy-sqmToCentralPath { } Mock Invoke-DbaQuery { $script:stdQueries.Add($Query) if ($Query -match 'MICROSOFTVERSION') { return [PSCustomObject]@{ ServerCompatibilityLevel = 160; SaName = 'sa' } } if ($Query -match 'FROM sys\.databases d') { return $script:stdDbRow } if ($Query -match 'AS MappedUser') { return $script:stdOrphans } if ($Query -match 'AS WindowsAccount') { return $script:stdCandidates } if ($Query -match 'xp_logininfo') { return $script:stdLoginInfo } if ($Query -match "SELECT 'Schema' AS Kind") { return @() } if ($Query -match 'DROP USER' -and $script:stdDropFails) { throw 'The database principal owns objects in the database and cannot be dropped.' } return $null } } } } It 'fuehrt an einer Standard-Datenbank alle fuenf Schritte aus' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdOrphans = @([PSCustomObject]@{ UserName = 'app'; LoginName = 'app'; MappedUser = $null }) $script:stdCandidates = @([PSCustomObject]@{ UserName = 'gone'; UserType = 'S'; AuthType = 1; WindowsAccount = $null }) $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false ($r | Where-Object Status -eq 'OK').Step | Should -Be @('FixOrphanUser', 'RemoveUserWithoutLogin', 'CompatibilityLevel', 'TargetRecoveryTime', 'DatabaseOwner') $script:stdQueries | Where-Object { $_ -match 'ALTER USER \[app\] WITH LOGIN = \[app\]' } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match 'DROP USER \[gone\]' } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match 'SET COMPATIBILITY_LEVEL = 160' } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match 'SET TARGET_RECOVERY_TIME = 60 SECONDS' } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match 'ALTER AUTHORIZATION ON DATABASE::\[AppDb\] TO \[sa\]' } | Should -Not -BeNullOrEmpty } } It 'fasst Secondary-, Offline- und Read-only-Datenbanken nicht an' -TestCases @( @{ Prop = 'is_ag_secondary'; Value = 1 } @{ Prop = 'state_desc'; Value = 'RESTORING' } @{ Prop = 'is_read_only'; Value = 1 } ) { param ($Prop, $Value) InModuleScope sqmSQLTool -Parameters @{ Prop = $Prop; Value = $Value } { param ($Prop, $Value) Set-StdMocks $script:stdDbRow.$Prop = $Value $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false @($r).Count | Should -Be 1 $r.Status | Should -Be 'Skipped' $script:stdQueries | Where-Object { $_ -match 'ALTER |DROP ' } | Should -BeNullOrEmpty } } It 'senkt eine hoehere Kompatibilitaetsstufe nicht ab' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdDbRow.compatibility_level = 170 $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false ($r | Where-Object Step -eq 'CompatibilityLevel').Status | Should -Be 'Skipped' $script:stdQueries | Where-Object { $_ -match 'COMPATIBILITY_LEVEL =' } | Should -BeNullOrEmpty } } It 'laesst WITHOUT-LOGIN-User standardmaessig stehen' { InModuleScope sqmSQLTool { Set-StdMocks Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false | Out-Null $script:stdQueries | Where-Object { $_ -match 'AS WindowsAccount' } | Should -Match '\(0 = 1 AND dp\.authentication_type = 0' } } It 'bezieht WITHOUT-LOGIN-User mit -IncludeUsersWithoutLogin ein' { InModuleScope sqmSQLTool { Set-StdMocks Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -IncludeUsersWithoutLogin -Confirm:$false | Out-Null $script:stdQueries | Where-Object { $_ -match 'AS WindowsAccount' } | Should -Match '\(1 = 1 AND dp\.authentication_type = 0' } } It 'entfernt keinen Windows-User, der ueber einen Gruppen-Login Zugriff hat' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdCandidates = @([PSCustomObject]@{ UserName = 'DOM\anna'; UserType = 'U'; AuthType = 3; WindowsAccount = 'DOM\anna' }) $script:stdLoginInfo = @([PSCustomObject]@{ 'account name' = 'DOM\anna'; 'permission path' = 'DOM\SQL-Readers' }) $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false $row = $r | Where-Object Step -eq 'RemoveUserWithoutLogin' $row.Status | Should -Be 'Skipped' $row.Message | Should -Match 'DOM\\SQL-Readers' $script:stdQueries | Where-Object { $_ -match 'DROP USER' } | Should -BeNullOrEmpty } } It 'entfernt einen Windows-User, den Windows nicht mehr kennt' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdCandidates = @([PSCustomObject]@{ UserName = 'DOM\left'; UserType = 'U'; AuthType = 3; WindowsAccount = $null }) $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false ($r | Where-Object Step -eq 'RemoveUserWithoutLogin').Status | Should -Be 'OK' Should -Invoke Invoke-DbaQuery -Times 0 -ParameterFilter { $Query -match 'xp_logininfo' } } } It 'meldet einen gescheiterten DROP USER als Failed statt ihn zu verschlucken' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdCandidates = @([PSCustomObject]@{ UserName = 'gone'; UserType = 'S'; AuthType = 1; WindowsAccount = $null }) $script:stdDropFails = $true $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false $row = $r | Where-Object Step -eq 'RemoveUserWithoutLogin' $row.Status | Should -Be 'Failed' $row.Message | Should -Match 'owns objects' # Die uebrigen Schritte laufen trotzdem ($r | Where-Object Step -eq 'DatabaseOwner').Status | Should -Be 'OK' } } It 'wirft mit -EnableException bei einem gescheiterten DROP USER' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdCandidates = @([PSCustomObject]@{ UserName = 'gone'; UserType = 'S'; AuthType = 1; WindowsAccount = $null }) $script:stdDropFails = $true { Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -EnableException -Confirm:$false } | Should -Throw -ExpectedMessage '*owns objects*' } } It 'aendert unter -WhatIf nichts und meldet einen per Name reparierbaren User nicht als "wuerde entfernt"' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdOrphans = @([PSCustomObject]@{ UserName = 'app'; LoginName = 'app'; MappedUser = $null }) # Ohne echte Reparatur taucht 'app' auch in der Kandidatenliste von Schritt 2 auf $script:stdCandidates = @([PSCustomObject]@{ UserName = 'app'; UserType = 'S'; AuthType = 1; WindowsAccount = $null }) $r = Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -WhatIf $r | Where-Object Step -eq 'RemoveUserWithoutLogin' | Should -BeNullOrEmpty ($r | Where-Object Step -eq 'FixOrphanUser').Status | Should -Be 'WhatIf' $script:stdQueries | Where-Object { $_ -match 'ALTER |DROP ' } | Should -BeNullOrEmpty } } It 'maskiert Namen mit ] und Hochkomma korrekt' { InModuleScope sqmSQLTool { Set-StdMocks $script:stdDbRow.name = "a]b'c" $script:stdCandidates = @([PSCustomObject]@{ UserName = "x]y'z"; UserType = 'S'; AuthType = 1; WindowsAccount = $null }) Invoke-sqmDatabaseStandardization -SqlInstance 'SQL01' -OutputPath $TestDrive -Confirm:$false | Out-Null $script:stdQueries | Where-Object { $_ -match [regex]::Escape("DROP USER [x]]y'z];") } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match [regex]::Escape("DATABASE_PRINCIPAL_ID(N'x]y''z')") } | Should -Not -BeNullOrEmpty $script:stdQueries | Where-Object { $_ -match [regex]::Escape("ALTER DATABASE [a]]b'c] SET TARGET_RECOVERY_TIME") } | Should -Not -BeNullOrEmpty } } } Describe 'Invoke-sqmRestoreDatabase - Aufruf der Standardisierung' { It 'reicht -KeepCompatibilityLevel als -SkipCompatibilityLevel durch' { InModuleScope sqmSQLTool { $fakeBackup = Join-Path ([System.IO.Path]::GetTempPath()) 'sqmRestoreTest_std.bak' Set-Content -Path $fakeBackup -Value 'dummy' -Encoding Ascii Mock Invoke-sqmLogging { } Mock Connect-DbaInstance { [PSCustomObject]@{ Name = 'SQL01'; Databases = @{} } } Mock Get-sqmDatabaseAgMembership { [PSCustomObject]@{ IsAgDatabase = $false; HadrEnabled = $false } } Mock Invoke-DbaQuery { $null } Mock Restore-DbaDatabase { [PSCustomObject]@{ Database = 'amb'; RestoreComplete = $true } } Mock Invoke-sqmDatabaseStandardization { [PSCustomObject]@{ Step = 'CompatibilityLevel'; Target = 'amb'; OldValue = 130; NewValue = 160; Status = 'OK'; Message = 'Angehoben.' } } $result = Invoke-sqmRestoreDatabase -SqlInstance 'SQL01' -BackupFile $fakeBackup -DatabaseName 'amb' -KeepCompatibilityLevel -Confirm:$false Should -Invoke Invoke-sqmDatabaseStandardization -Times 1 -ParameterFilter { $Database -eq 'amb' -and $SkipCompatibilityLevel } ($result | Where-Object Action -eq 'CompatibilityLevel').Status | Should -Be 'Success' Remove-Item $fakeBackup -ErrorAction SilentlyContinue } } It 'loest den Aufruf nur mit -DatabaseName (Backup aus der Historie) auf den Satz FromHistory auf' { # Regression 1.9.149.0: DefaultParameterSetName stand auf SingleFile, der dokumentierte Aufruf # scheiterte beim Parameter-Binding am fehlenden -BackupFile. $cmd = Get-Command Invoke-sqmRestoreDatabase $cmd.DefaultParameterSet | Should -Be 'FromHistory' } } |