tests/Unit/Public/Find-sqmAgentJobReference.Tests.ps1
|
#Requires -Modules Pester <# .SYNOPSIS Unit Tests fuer Find-sqmAgentJobReference. Invoke-DbaQuery wird gemockt und liefert eine feste Menge von msdb-Jobstep-Zeilen; getestet wird die Trefferlogik (Wortgrenze, Unterstrich, Kommentar, EXEC vs. Erwaehnung, Datenbankaufloesung, Filter). #> BeforeAll { . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1" Import-sqmTestModule function New-JobStepRow { param( [string]$JobName = 'Nightly Load', [int]$JobEnabled = 1, [int]$StepId = 1, [string]$StepName = 'Step 1', [string]$Subsystem = 'TSQL', $StepDatabase = 'Sales', [string]$Command = 'EXEC dbo.usp_LoadSales;', [int]$ScheduleCount = 1, [int]$ActiveScheduleCount = 1, $NextRunKey = 20260908220000, $JobLastRunStatus = 1, $JobLastRunDate = 20260907, $JobLastRunTime = 220000, $StepLastRunOutcome = 1, $StepLastRunDate = 20260907, $StepLastRunTime = 220000 ) [PSCustomObject]@{ JobName = $JobName JobEnabled = $JobEnabled JobOwner = 'sa' JobCategory = 'Database Maintenance' ScheduleCount = $ScheduleCount ActiveScheduleCount = $ActiveScheduleCount NextRunKey = $NextRunKey JobLastRunStatus = $JobLastRunStatus JobLastRunDate = $JobLastRunDate JobLastRunTime = $JobLastRunTime StepId = $StepId StepName = $StepName Subsystem = $Subsystem StepDatabase = $StepDatabase StepLastRunOutcome = $StepLastRunOutcome StepLastRunDate = $StepLastRunDate StepLastRunTime = $StepLastRunTime Command = $Command } } # Ein Satz Jobsteps, der die Faelle abdeckt, an denen eine reine LIKE-Suche scheitert. function New-StandardJobStepSet { @( New-JobStepRow -JobName 'Nightly Load' -StepId 1 -StepName 'Load' ` -Command 'EXEC dbo.usp_LoadSales;' New-JobStepRow -JobName 'Nightly Load' -StepId 2 -StepName 'Bracketed' ` -Command "DECLARE @rc INT;`r`nEXECUTE @rc = [dbo].[usp_LoadSales];" New-JobStepRow -JobName 'Cross DB' -StepId 1 -StepName 'Archive' -StepDatabase 'master' ` -Command 'EXEC Archive.dbo.usp_LoadSales;' New-JobStepRow -JobName 'Only Mentioned' -StepId 1 -StepName 'Check' ` -Command "SELECT name FROM sys.objects WHERE name = 'usp_LoadSales';" New-JobStepRow -JobName 'Commented Out' -StepId 1 -StepName 'Old' ` -Command "/* EXEC dbo.usp_LoadSales */`r`nSELECT 1;" # Unterstrich-Falle: LIKE '%usp_LoadSales%' wuerde das hier finden. New-JobStepRow -JobName 'Underscore Trap' -StepId 1 -StepName 'Trap' ` -Command 'EXEC dbo.uspXLoadYSales;' # Praefix-Falle: eine laengere Prozedur mit demselben Namensanfang. New-JobStepRow -JobName 'Prefix Trap' -StepId 1 -StepName 'Trap' ` -Command 'EXEC dbo.usp_LoadSalesArchive;' New-JobStepRow -JobName 'Disabled Job' -JobEnabled 0 -StepId 1 -StepName 'Load' ` -Command 'EXEC dbo.usp_LoadSales;' New-JobStepRow -JobName 'Via sqlcmd' -StepId 1 -StepName 'Shell' -Subsystem 'CmdExec' ` -StepDatabase $null ` -Command 'sqlcmd -S SQL01 -d Warehouse -Q "EXEC dbo.usp_LoadSales"' New-JobStepRow -JobName 'Via PowerShell' -StepId 1 -StepName 'PS' -Subsystem 'PowerShell' ` -StepDatabase $null ` -Command 'Invoke-DbaQuery -SqlInstance SQL01 -Database Reporting -Query "EXEC dbo.usp_Refresh"' New-JobStepRow -JobName 'Use Statement' -StepId 1 -StepName 'Use' -StepDatabase 'master' ` -Command "USE [Reporting];`r`nGO`r`nEXEC dbo.usp_Refresh;" New-JobStepRow -JobName 'Package' -StepId 1 -StepName 'SSIS' -Subsystem 'SSIS' ` -StepDatabase $null ` -Command '/ISSERVER "\SSISDB\Sales\usp_LoadSales.dtsx" /SERVER SQL01' New-JobStepRow -JobName 'Truncate Job' -StepId 1 -StepName 'Clean' -StepDatabase 'Staging' ` -Command "TRUNCATE TABLE dbo.Import;`r`nEXEC dbo.usp_LoadSales;" New-JobStepRow -JobName 'Other Schema' -StepId 1 -StepName 'Load' ` -Command 'EXEC etl.usp_LoadSales;' ) } } AfterAll { if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force } $env:MSSQLTOOLS_SKIP_AUTO_UPDATE = $null } Describe 'Find-sqmAgentJobReference' { Context 'Parameter-Validierung' { It 'Funktion existiert' { Get-Command Find-sqmAgentJobReference | Should -Not -BeNullOrEmpty } It '<_> Parameter existiert' -ForEach @( 'SqlInstance', 'SqlCredential', 'ObjectName', 'SearchText', 'RegexSearch', 'Database', 'JobName', 'Subsystem', 'ExcludeDisabledJobs', 'IncludeCommand', 'VerifyObject', 'EnableException' ) { (Get-Command Find-sqmAgentJobReference).Parameters.ContainsKey($_) | Should -Be $true } It 'Lehnt ein unbekanntes Subsystem ab' { { Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -Subsystem 'Nonsense' } | Should -Throw } It 'Lehnt einen ungueltigen regulaeren Ausdruck ab' { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } { Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -SearchText '[unclosed' -RegexSearch } | Should -Throw } } Context 'Objektsuche' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { New-StandardJobStepSet } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Findet den EXEC-Aufruf und meldet ihn als Execute' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Nightly Load' -and $_.StepId -eq 1 } $hit | Should -Not -BeNullOrEmpty $hit.CallType | Should -Be 'Execute' $hit.InComment | Should -Be $false $hit.MatchedObject | Should -Be 'usp_LoadSales' } It 'Findet den Aufruf auch in eckigen Klammern und mit Rueckgabevariable' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.StepName -eq 'Bracketed' } $hit | Should -Not -BeNullOrEmpty $hit.CallType | Should -Be 'Execute' $hit.MatchedObject | Should -Be 'usp_LoadSales' } It 'Behandelt den Unterstrich als Zeichen, nicht als LIKE-Platzhalter' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) ($r | Where-Object { $_.JobName -eq 'Underscore Trap' }) | Should -BeNullOrEmpty } It 'Trifft nicht auf einen laengeren Namen mit gleichem Anfang' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) ($r | Where-Object { $_.JobName -eq 'Prefix Trap' }) | Should -BeNullOrEmpty } It 'Meldet eine blosse Erwaehnung als Reference, nicht als Execute' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Only Mentioned' } $hit.CallType | Should -Be 'Reference' $hit.InComment | Should -Be $false } It 'Kennzeichnet eine auskommentierte Fundstelle' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Commented Out' } $hit | Should -Not -BeNullOrEmpty $hit.InComment | Should -Be $true } It 'Unterstuetzt Wildcards im Objektnamen' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_Load*' -EnableException) ($r | Where-Object { $_.JobName -eq 'Prefix Trap' }) | Should -Not -BeNullOrEmpty ($r | Where-Object { $_.JobName -eq 'Underscore Trap' }) | Should -BeNullOrEmpty } It 'Beschraenkt auf das angegebene Schema' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'etl.usp_LoadSales' -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Other Schema' } It 'Meldet Zeilennummer und Fundzeile' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.StepName -eq 'Bracketed' } $hit.MatchLine | Should -Be 2 $hit.LineText | Should -Match 'EXECUTE @rc' } } Context 'Datenbankaufloesung' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { New-StandardJobStepSet } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Nimmt die Datenbank des Steps, wenn der Aufruf nicht qualifiziert ist' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Nightly Load' -and $_.StepId -eq 1 } $hit.ResolvedDatabase | Should -Be 'Sales' } It 'Nimmt bei dreiteiligem Namen die Datenbank aus dem Aufruf, nicht die des Steps' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Cross DB' } $hit.StepDatabase | Should -Be 'master' $hit.ResolvedDatabase | Should -Be 'Archive' } It 'Liest die Datenbank aus einem sqlcmd -d Argument' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -Database 'Warehouse' -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Via sqlcmd' } It 'Liest die Datenbank aus einem -Database Argument eines PowerShell-Steps' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -Database 'Reporting' -EnableException) @($r).JobName | Should -Contain 'Via PowerShell' } It 'Liest die Datenbank aus einer USE-Anweisung' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -Database 'Reporting' -EnableException) @($r).JobName | Should -Contain 'Use Statement' } It 'Der Datenbankteil eines dreiteiligen -ObjectName wirkt als Datenbankfilter' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'Archive.dbo.usp_LoadSales' -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Cross DB' } } Context 'Filter' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { New-StandardJobStepSet } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Deaktivierte Jobs sind standardmaessig enthalten' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) ($r | Where-Object { $_.JobName -eq 'Disabled Job' }) | Should -Not -BeNullOrEmpty } It 'ExcludeDisabledJobs entfernt sie' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -ExcludeDisabledJobs -EnableException) ($r | Where-Object { $_.JobName -eq 'Disabled Job' }) | Should -BeNullOrEmpty } It 'SSIS-Steps werden standardmaessig nicht durchsucht' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) ($r | Where-Object { $_.JobName -eq 'Package' }) | Should -BeNullOrEmpty } It 'Subsystem All schliesst SSIS-Steps ein' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -Subsystem 'All' -EnableException) ($r | Where-Object { $_.JobName -eq 'Package' }) | Should -Not -BeNullOrEmpty } It 'JobName filtert auf den Job' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -JobName 'Nightly*' -EnableException) @($r).Count | Should -Be 2 (@($r).JobName | Select-Object -Unique) | Should -Be 'Nightly Load' } It 'SearchText findet freien Text' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -SearchText 'TRUNCATE TABLE' -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Truncate Job' $r[0].CallType | Should -Be 'Text' } It 'SearchText und ObjectName wirken zusammen als UND' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -SearchText 'TRUNCATE TABLE' -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Truncate Job' $r[0].MatchType | Should -Be 'Object+Text' } It 'RegexSearch behandelt SearchText als regulaeren Ausdruck' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -SearchText 'TRUNCATE\s+TABLE\s+dbo\.\w+' ` -RegexSearch -EnableException) @($r).Count | Should -Be 1 $r[0].JobName | Should -Be 'Truncate Job' } } Context 'Ausgabe' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { New-StandardJobStepSet } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Rechnet die msdb-Ganzzahlen in echte Zeitstempel um' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $hit = $r | Where-Object { $_.JobName -eq 'Nightly Load' -and $_.StepId -eq 1 } $hit.JobLastRunDate | Should -Be ([datetime]'2026-09-07 22:00:00') $hit.NextRunDate | Should -Be ([datetime]'2026-09-08 22:00:00') $hit.JobLastRunOutcome | Should -Be 'Succeeded' $hit.IsScheduled | Should -Be $true } It 'Ein nie gelaufener Step wird nicht als Failed gemeldet' { # msdb speichert "nie gelaufen" als last_run_outcome = 0 / last_run_date = 0, und 0 # ist zugleich der Code fuer Failed (auf DEV01 mit acht Ola-Jobs ohne Historie belegt). Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @(New-JobStepRow -JobName 'Never Ran' -StepLastRunOutcome 0 -StepLastRunDate 0 ` -StepLastRunTime 0 -JobLastRunStatus $null -JobLastRunDate 0 -JobLastRunTime 0 ` -NextRunKey $null) } $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $r[0].StepLastRunOutcome | Should -Be 'NeverRun' $r[0].StepLastRunDate | Should -BeNullOrEmpty $r[0].JobLastRunOutcome | Should -Be 'NeverRun' $r[0].NextRunDate | Should -BeNullOrEmpty } It 'Ein wirklich fehlgeschlagener Lauf bleibt Failed' { Mock -ModuleName sqmSQLTool Invoke-DbaQuery { @(New-JobStepRow -JobName 'Failed Job' -StepLastRunOutcome 0 -StepLastRunDate 20260907 ` -StepLastRunTime 220000 -JobLastRunStatus 0) } $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $r[0].StepLastRunOutcome | Should -Be 'Failed' $r[0].JobLastRunOutcome | Should -Be 'Failed' } It 'Liefert ohne IncludeCommand nur die Vorschau' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException) $r[0].PSObject.Properties.Name | Should -Not -Contain 'Command' $r[0].CommandPreview | Should -Not -BeNullOrEmpty } It 'IncludeCommand haengt das vollstaendige Kommando an' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -IncludeCommand -EnableException) $r[0].PSObject.Properties.Name | Should -Contain 'Command' $r[0].Command | Should -Be 'EXEC dbo.usp_LoadSales;' } It 'Fragt jede Instanz einzeln ab und markiert die Herkunft' { $r = @(Find-sqmAgentJobReference -SqlInstance 'SQL01', 'SQL02' -ObjectName 'usp_LoadSales' -EnableException) (@($r).SqlInstance | Select-Object -Unique | Sort-Object) | Should -Be @('SQL01', 'SQL02') Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -Exactly 2 } } Context 'VerifyObject' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { if ($Query -match 'sysjobsteps') { @( New-JobStepRow -JobName 'Nightly Load' -StepId 1 -Command 'EXEC dbo.usp_LoadSales;' New-JobStepRow -JobName 'Second Job' -StepId 1 -Command 'EXEC dbo.usp_LoadSales;' ) } else { @([PSCustomObject]@{ ObjectType = 'SQL_STORED_PROCEDURE' }) } } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Meldet ein vorhandenes Objekt mit Typ' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -VerifyObject -EnableException) $r[0].ObjectExists | Should -Be $true $r[0].ObjectType | Should -Be 'SQL_STORED_PROCEDURE' } It 'Schlaegt dieselbe Datenbank/Objekt-Kombination nur einmal nach' { Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' ` -VerifyObject -EnableException | Out-Null # 1x sysjobsteps + 1x sys.objects, obwohl zwei Jobs dasselbe Objekt aufrufen Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -Exactly 2 } It 'Ohne VerifyObject wird sys.objects nicht abgefragt' { Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException | Out-Null Should -Invoke -ModuleName sqmSQLTool Invoke-DbaQuery -Exactly 1 } } Context 'VerifyObject bei fehlendem Objekt' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { if ($Query -match 'sysjobsteps') { @(New-JobStepRow -JobName 'Broken Job' -StepId 1 -Command 'EXEC dbo.usp_Gone;') } else { @() } } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Meldet einen Aufruf eines nicht mehr vorhandenen Objekts' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_Gone' ` -VerifyObject -EnableException) @($r).Count | Should -Be 1 $r[0].CallType | Should -Be 'Execute' $r[0].ObjectExists | Should -Be $false } } Context 'Fehlerbehandlung' { BeforeAll { Mock -ModuleName sqmSQLTool Invoke-sqmLogging { } Mock -ModuleName sqmSQLTool Invoke-DbaQuery { throw 'Login failed' } InModuleScope sqmSQLTool { $script:dbatoolsAvailable = $true } } It 'Wirft mit EnableException' { { Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales' -EnableException } | Should -Throw } It 'Ohne EnableException wird protokolliert und leer zurueckgegeben' { $r = @(Find-sqmAgentJobReference -SqlInstance 'TESTSERVER' -ObjectName 'usp_LoadSales') @($r).Count | Should -Be 0 Should -Invoke -ModuleName sqmSQLTool Invoke-sqmLogging -ParameterFilter { $Level -eq 'ERROR' } } } } |