From 3ac924c6400e45f4ff3c8654a1ee89d91d07b803 Mon Sep 17 00:00:00 2001 From: Andreas Jordan Date: Sat, 29 Aug 2026 14:22:33 +0200 Subject: [PATCH] Get-DbaAgBackupHistory - Fix single replica discovery and honor EnableException Two defects, both found while sweeping issue #10621 into this command: The single-server branch assigned the replica names to the server list as-is. With exactly one replica, SMO returns a scalar string, so indexing the list yielded the first character of the replica name and the DbaInstanceParameter binder turned that into null - the command failed with "Cannot bind argument to parameter SqlInstance because it is null" for every single replica availability group queried without -Database. The list is now forced to an array. The -Last branch piped into Select-DbaBackupInformation without passing EnableException on, so a chain without a full backup degraded the stop to a warning and the command returned nothing even when the caller asked for exceptions. EnableException now flows into that call; without the switch nothing changes. The command had no integration tests at all - the old comment said no availability group runs in AppVeyor, which predates the HADR lane. New tests build a single replica ClusterType NONE availability group, verify the healthy chain, and pin both EnableException behaviors via a -Since window that holds only a log backup. We use -Since instead of wiping msdb history because Backup-DbaDatabase refuses a log backup for a database whose history holds no full backup. Verified in the lab on SQL04\SQL2025: 4 integration tests passed, lab left clean; the unfixed code failed the same tests with the null bind. References #10621 (do Get-DbaAgBackupHistory) Co-Authored-By: Claude Fable 5 --- public/Get-DbaAgBackupHistory.ps1 | 6 +- tests/Get-DbaAgBackupHistory.Tests.ps1 | 117 ++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 3 deletions(-) diff --git a/public/Get-DbaAgBackupHistory.ps1 b/public/Get-DbaAgBackupHistory.ps1 index 160868ca74be..ac2559d57ec5 100644 --- a/public/Get-DbaAgBackupHistory.ps1 +++ b/public/Get-DbaAgBackupHistory.ps1 @@ -260,7 +260,9 @@ function Get-DbaAgBackupHistory { $replicaNames = ($server.AvailabilityGroups | Where-Object { $_.Name -in $AvailabilityGroup } ).AvailabilityReplicas.Name Write-Message -Level Verbose -Message "We have found these replicas: $replicaNames" - $serverList = $replicaNames + # With a single replica, Name returns a scalar string and indexing it with [0] below + # would yield its first character instead of the replica name, so force an array. + $serverList = @($replicaNames) } Write-Message -Level Verbose -Message "We have more than one server, so query them all and aggregate" @@ -279,7 +281,7 @@ function Get-DbaAgBackupHistory { if ($Last) { Write-Message -Level Verbose -Message "Filtering Ag backups for Last" - $AgResults | Select-DbaBackupInformation -ServerName $AvailabilityGroup + $AgResults | Select-DbaBackupInformation -ServerName $AvailabilityGroup -EnableException:$EnableException } elseif ($LastFull) { Write-Message -Level Verbose -Message "Filtering Ag backups for LastFull" Foreach ($AgDb in ( $AgResults.Database | Select-Object -Unique)) { diff --git a/tests/Get-DbaAgBackupHistory.Tests.ps1 b/tests/Get-DbaAgBackupHistory.Tests.ps1 index 878e4f2250ca..2dfac8c996d4 100644 --- a/tests/Get-DbaAgBackupHistory.Tests.ps1 +++ b/tests/Get-DbaAgBackupHistory.Tests.ps1 @@ -37,4 +37,119 @@ Describe $CommandName -Tag UnitTests { } } -# No Integration Tests, because we don't have an availability group running in AppVeyor \ No newline at end of file +Describe $CommandName -Tag IntegrationTests { + BeforeAll { + # We want to run all commands in the BeforeAll block with EnableException to ensure that the test fails if the setup fails. + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + # For all the backups that we want to clean up after the test, we create a directory that we can delete at the end. + $backupPath = "$($TestConfig.Temp)\$CommandName-$(Get-Random)" + $null = New-Item -Path $backupPath -ItemType Directory + + # To get availability group backup history we need an availability group and a database with backups. + # A single replica availability group with cluster type NONE is enough. + $agName = "dbatoolsci_agbackuphistory" + $agDbName = "dbatoolsci_agbhdb_$(Get-Random)" + + $splatAg = @{ + Primary = $TestConfig.InstanceHadr + Name = $agName + ClusterType = "None" + FailoverMode = "Manual" + Certificate = "dbatoolsci_AGCert" + } + $null = New-DbaAvailabilityGroup @splatAg + + $null = New-DbaDatabase -SqlInstance $TestConfig.InstanceHadr -Name $agDbName + $splatBackup = @{ + SqlInstance = $TestConfig.InstanceHadr + Database = $agDbName + Path = $backupPath + } + $null = Backup-DbaDatabase @splatBackup -Type Full -FilePath "agbh_full.bak" + $splatAddAgDatabase = @{ + SqlInstance = $TestConfig.InstanceHadr + AvailabilityGroup = $agName + Database = $agDbName + } + $null = Add-DbaAgDatabase @splatAddAgDatabase + $null = Backup-DbaDatabase @splatBackup -Type Log -FilePath "agbh_log.trn" + + # We want to run all commands outside of the BeforeAll block without EnableException to be able to test for specific warnings. + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + AfterAll { + # We want to run all commands in the AfterAll block with EnableException to ensure that the cleanup fails loudly. + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + # Cleanup all created objects. + $null = Remove-DbaAvailabilityGroup -SqlInstance $TestConfig.InstanceHadr -AvailabilityGroup $agName + $null = Get-DbaEndpoint -SqlInstance $TestConfig.InstanceHadr -Type DatabaseMirroring | Remove-DbaEndpoint + $null = Remove-DbaDatabase -SqlInstance $TestConfig.InstanceHadr -Database $agDbName + + # Remove the backup directory. + Remove-Item -Path $backupPath -Recurse + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + Context "Gets the last backup chain of the availability group" { + BeforeAll { + $results = @(Get-DbaAgBackupHistory -SqlInstance $TestConfig.InstanceHadr -AvailabilityGroup $agName -Last) + } + + # Always include this test to be sure that the command runs without warnings. + It "Does not warn" { + $WarnVar | Should -BeNullOrEmpty + } + + It "Returns the full and the log backup tagged with the availability group name" { + $results.Type | Should -Contain "Full" + $results.Type | Should -Contain "Log" + $results.AvailabilityGroupName | Select-Object -Unique | Should -Be $agName + } + } + + Context "Honors EnableException when no full backup anchors the chain #10621" { + BeforeAll { + $PSDefaultParameterValues["*-Dba*:EnableException"] = $true + + # A history window that starts after the full backup and holds only a log backup, so the + # last-chain selection finds a log backup but no full backup to anchor it. We use -Since + # instead of deleting msdb history, because Backup-DbaDatabase refuses a log backup for a + # database whose msdb history holds no full backup. The sleeps keep the timestamp strictly + # between the existing backups and the new log backup. + Start-Sleep -Seconds 1 + $sinceTime = Get-Date + Start-Sleep -Seconds 1 + $null = Backup-DbaDatabase @splatBackup -Type Log -FilePath "agbh_log2.trn" + + $PSDefaultParameterValues.Remove("*-Dba*:EnableException") + } + + It "Throws with EnableException" { + $splatAgHistory = @{ + SqlInstance = $TestConfig.InstanceHadr + AvailabilityGroup = $agName + Last = $true + Since = $sinceTime + EnableException = $true + } + { Get-DbaAgBackupHistory @splatAgHistory } | Should -Throw "*Fullname property not found*" + } + + It "Still warns and returns nothing without EnableException" { + $splatAgHistory = @{ + SqlInstance = $TestConfig.InstanceHadr + AvailabilityGroup = $agName + Last = $true + Since = $sinceTime + WarningAction = "SilentlyContinue" + } + $results = Get-DbaAgBackupHistory @splatAgHistory + $WarnVar | Should -BeLike "*Fullname property not found*" + $results | Should -BeNullOrEmpty + } + } +} \ No newline at end of file