diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b972706cd..4332280dae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- `Save-SqlDscSqlServerMediaFile` + - Fixed the Force parameter to work correctly when the target ISO file already + exists. The command now properly overwrites the target file when Force is + specified. Removed the safety check that was incorrectly blocking execution + when other ISO files existed in the destination directory + ([issue #2280](https://github.com/dsccommunity/SqlServerDsc/issues/2280)). - `DSC_SqlRS` - Fixed intermittent initialization failures on resource-constrained systems (particularly Windows Server 2025 in CI) by adding an optional `RestartTimeout` diff --git a/source/Public/Save-SqlDscSqlServerMediaFile.ps1 b/source/Public/Save-SqlDscSqlServerMediaFile.ps1 index 8db9dcda58..b5b1ceaad1 100644 --- a/source/Public/Save-SqlDscSqlServerMediaFile.ps1 +++ b/source/Public/Save-SqlDscSqlServerMediaFile.ps1 @@ -107,22 +107,9 @@ function Save-SqlDscSqlServerMediaFile $ConfirmPreference = 'None' } - if ((Get-Item -Path "$DestinationPath/*.iso" -Force).Count -gt 0 -and -not $SkipExecution) - { - $auditAlreadyPresentMessage = $script:localizedData.SqlServerMediaFile_Save_InvalidDestinationFolder - - $PSCmdlet.ThrowTerminatingError( - [System.Management.Automation.ErrorRecord]::new( - $auditAlreadyPresentMessage, - 'SSDSSM0001', # cspell: disable-line - [System.Management.Automation.ErrorCategory]::InvalidOperation, - $DestinationPath - ) - ) - } - $destinationFilePath = Join-Path -Path $DestinationPath -ChildPath $FileName + # Handle target file if it exists - Force parameter controls overwrite behavior if ((Test-Path -Path $destinationFilePath)) { $verboseDescriptionMessage = $script:localizedData.SqlServerMediaFile_Save_ShouldProcessVerboseDescription -f $destinationFilePath @@ -226,10 +213,11 @@ function Save-SqlDscSqlServerMediaFile Message = $script:localizedData.SqlServerMediaFile_Save_MultipleFilesFoundAfterDownload Category = 'InvalidOperation' ErrorId = 'SSDSSM0002' # CSpell: disable-line - TargetObject = $ServiceType + TargetObject = $DestinationPath } Write-Error @writeErrorParameters + return } Write-Verbose -Message ($script:localizedData.SqlServerMediaFile_Save_RenamingFile -f $isoFile.Name, $FileName) diff --git a/source/en-US/SqlServerDsc.strings.psd1 b/source/en-US/SqlServerDsc.strings.psd1 index e59f9f27f4..e246006182 100644 --- a/source/en-US/SqlServerDsc.strings.psd1 +++ b/source/en-US/SqlServerDsc.strings.psd1 @@ -274,7 +274,6 @@ ConvertFrom-StringData @' SqlServerMediaFile_Save_ShouldProcessVerboseWarning = Are you sure you want to replace existing file '{0}'? # This string shall not end with full stop (.) since it is used as a title of ShouldProcess messages. SqlServerMediaFile_Save_ShouldProcessCaption = Replace existing file - SqlServerMediaFile_Save_InvalidDestinationFolder = Multiple files with the .iso extension was found in the destination path. Please choose another destination folder. SqlServerMediaFile_Save_MultipleFilesFoundAfterDownload = Multiple files with the .iso extension was found in the destination path. Cannot determine which one of the files that was downloaded. SqlServerMediaFile_Save_DownloadingInformation = Downloading the SQL Server media from '{0}'. SqlServerMediaFile_Save_IsExecutable = Downloaded an executable file. Using the executable to download the media file. diff --git a/tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1 b/tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1 index 773643d565..4475a2cc4c 100644 --- a/tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1 +++ b/tests/Integration/Commands/Save-SqlDscSqlServerMediaFile.Integration.Tests.ps1 @@ -34,7 +34,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Create a temporary directory for testing downloads $script:testDownloadPath = Join-Path -Path $env:TEMP -ChildPath "SqlDscTestDownloads_$(Get-Random)" New-Item -Path $script:testDownloadPath -ItemType Directory -Force | Out-Null - + Write-Verbose -Message "Created test download directory: $script:testDownloadPath" -Verbose } @@ -52,7 +52,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Use SQL Server 2017 ISO URL for testing direct ISO download $script:directIsoUrl = 'https://download.microsoft.com/download/E/F/2/EF23C21D-7860-4F05-88CE-39AA114B014B/SQLServer2017-x64-ENU.iso' $script:expectedFileName = 'SQLServer2017-test.iso' - + # Create separate subdirectory for this context to avoid ISO file conflicts $script:directIsoTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'DirectIso' New-Item -Path $script:directIsoTestPath -ItemType Directory -Force | Out-Null @@ -63,12 +63,12 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Verify the result is a FileInfo object $result | Should -BeOfType [System.IO.FileInfo] - + # Verify the file was downloaded $result.Name | Should -Be $script:expectedFileName $result.Exists | Should -BeTrue $result.Length | Should -BeGreaterThan 0 - + # Verify the file is in the expected location $expectedPath = Join-Path -Path $script:directIsoTestPath -ChildPath $script:expectedFileName Test-Path -Path $expectedPath | Should -BeTrue @@ -78,22 +78,22 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Create separate subdirectory for this test to avoid conflicts $overwriteTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'OverwriteTest' New-Item -Path $overwriteTestPath -ItemType Directory -Force | Out-Null - - # Use SkipExecution to avoid the safety check while still testing Force parameter + + # Use SkipExecution to download only the executable without extracting ISO $executableUrl = 'https://download.microsoft.com/download/e/6/4/e6477a2a-9b58-40f7-8ad6-62bb8491ea78/SQLServerReportingServices.exe' $targetFileName = 'overwrite-test.exe' - + # First, create a dummy file with the target name $dummyFilePath = Join-Path -Path $overwriteTestPath -ChildPath $targetFileName 'dummy content for overwrite test' | Out-File -FilePath $dummyFilePath -Encoding UTF8 - + # Verify the dummy file exists and get its original size Test-Path -Path $dummyFilePath | Should -BeTrue $originalSize = (Get-Item -Path $dummyFilePath).Length - + # Download with SkipExecution and Force should overwrite the dummy file $result = Save-SqlDscSqlServerMediaFile -Url $executableUrl -DestinationPath $overwriteTestPath -FileName $targetFileName -SkipExecution -Force -Quiet -ErrorAction 'Stop' - + # Verify the file was overwritten (should be much larger than the dummy content) $result | Should -BeOfType [System.IO.FileInfo] $result.Name | Should -Be $targetFileName @@ -108,7 +108,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Use SQL Server 2022 executable URL for testing executable download and extraction $script:executableUrl = 'https://download.microsoft.com/download/c/c/9/cc9c6797-383c-4b24-8920-dc057c1de9d3/SQL2022-SSEI-Dev.exe' $script:expectedIsoFileName = 'SQL2022-media.iso' - + # Create separate subdirectory for this context to avoid ISO file conflicts $script:executableTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'ExecutableTest' New-Item -Path $script:executableTestPath -ItemType Directory -Force | Out-Null @@ -128,7 +128,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Verify the executable was cleaned up (should not exist) $executablePath = [System.IO.Path]::ChangeExtension($result.FullName, 'exe') Test-Path -Path $executablePath | Should -BeFalse - + # Verify only one ISO file exists in the directory $isoFiles = Get-ChildItem -Path $script:executableTestPath -Filter '*.iso' $isoFiles.Count | Should -Be 1 @@ -140,7 +140,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat # Use SQL Server Reporting Services executable for testing SkipExecution $script:rsExecutableUrl = 'https://download.microsoft.com/download/e/6/4/e6477a2a-9b58-40f7-8ad6-62bb8491ea78/SQLServerReportingServices.exe' $script:expectedExecutableFileName = 'SSRS-Test.exe' - + # Create separate subdirectory for this context to avoid file conflicts $script:skipExecutionTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'SkipExecutionTest' New-Item -Path $script:skipExecutionTestPath -ItemType Directory -Force | Out-Null @@ -168,19 +168,63 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat $script:errorTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'ErrorTest' New-Item -Path $script:errorTestPath -ItemType Directory -Force | Out-Null } - - It 'Should throw error when ISO files already exist in destination and SkipExecution is not used' { - # Create a dummy ISO file to trigger the error condition - $dummyIsoPath = Join-Path -Path $script:errorTestPath -ChildPath 'existing.iso' - 'dummy iso content' | Out-File -FilePath $dummyIsoPath -Encoding UTF8 - # This should throw an error due to existing ISO file - { - Save-SqlDscSqlServerMediaFile -Url $script:directIsoUrl -DestinationPath $script:errorTestPath -FileName 'new-download.iso' -Quiet -ErrorAction 'Stop' - } | Should -Throw + It 'Should download successfully even when multiple different ISO files already exist in destination' { + # Create two different dummy ISO files in the destination directory + $dummyIso1Path = Join-Path -Path $script:errorTestPath -ChildPath 'existing1.iso' + $dummyIso2Path = Join-Path -Path $script:errorTestPath -ChildPath 'existing2.iso' + 'dummy iso content 1' | Out-File -FilePath $dummyIso1Path -Encoding UTF8 + 'dummy iso content 2' | Out-File -FilePath $dummyIso2Path -Encoding UTF8 + + # Verify the dummy ISO files exist + Test-Path -Path $dummyIso1Path | Should -BeTrue + Test-Path -Path $dummyIso2Path | Should -BeTrue + + # This should succeed - the command no longer blocks on non-target ISO files + $targetFileName = 'new-download.iso' + $result = Save-SqlDscSqlServerMediaFile -Url $script:directIsoUrl -DestinationPath $script:errorTestPath -FileName $targetFileName -Force -Quiet -ErrorAction 'Stop' + + # Verify the download succeeded + $result | Should -BeOfType [System.IO.FileInfo] + $result.Name | Should -Be $targetFileName + $result.Exists | Should -BeTrue + + # Verify the other ISO files still exist (they should not be affected) + Test-Path -Path $dummyIso1Path | Should -BeTrue + Test-Path -Path $dummyIso2Path | Should -BeTrue + + # Clean up + Remove-Item -Path $dummyIso1Path -Force -ErrorAction SilentlyContinue + Remove-Item -Path $dummyIso2Path -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path -Path $script:errorTestPath -ChildPath $targetFileName) -Force -ErrorAction SilentlyContinue + } + + It 'Should allow overwriting the same ISO file when Force parameter is used' { + # Create a separate subdirectory for this test + $forceOverwriteTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'ForceOverwriteIso' + New-Item -Path $forceOverwriteTestPath -ItemType Directory -Force | Out-Null + + # First, create a dummy ISO file with the exact same name we'll download + $targetFileName = 'force-overwrite-test.iso' + $dummyIsoPath = Join-Path -Path $forceOverwriteTestPath -ChildPath $targetFileName + 'dummy iso content for force overwrite test' | Out-File -FilePath $dummyIsoPath -Encoding UTF8 + + # Verify the dummy file exists and get its original size + Test-Path -Path $dummyIsoPath | Should -BeTrue + $originalSize = (Get-Item -Path $dummyIsoPath).Length + + # Download with Force parameter should overwrite the existing ISO file + $result = Save-SqlDscSqlServerMediaFile -Url $script:directIsoUrl -DestinationPath $forceOverwriteTestPath -FileName $targetFileName -Force -Quiet -ErrorAction 'Stop' + + # Verify the file was overwritten (should be much larger than the dummy content) + $result | Should -BeOfType [System.IO.FileInfo] + $result.Name | Should -Be $targetFileName + $result.Exists | Should -BeTrue + $result.Length | Should -BeGreaterThan $originalSize + $result.Length | Should -BeGreaterThan 1000000 # Real ISO should be at least 1MB # Clean up - Remove-Item -Path $dummyIsoPath -Force -ErrorAction SilentlyContinue + Remove-Item -Path $forceOverwriteTestPath -Recurse -Force -ErrorAction SilentlyContinue } It 'Should handle invalid URL gracefully' { @@ -195,7 +239,7 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag @('Integration_SQL2017', 'Integrat BeforeAll { # Use SQL Server 2019 executable for language testing $script:sql2019Url = 'https://download.microsoft.com/download/d/a/2/da259851-b941-459d-989c-54a18a5d44dd/SQL2019-SSEI-Dev.exe' - + # Create separate subdirectory for language testing to avoid conflicts $script:languageTestPath = Join-Path -Path $script:testDownloadPath -ChildPath 'LanguageTest' New-Item -Path $script:languageTestPath -ItemType Directory -Force | Out-Null diff --git a/tests/Unit/Public/Save-SqlDscSqlServerMediaFile.Tests.ps1 b/tests/Unit/Public/Save-SqlDscSqlServerMediaFile.Tests.ps1 index b509f3ef5b..6ac465e767 100644 --- a/tests/Unit/Public/Save-SqlDscSqlServerMediaFile.Tests.ps1 +++ b/tests/Unit/Public/Save-SqlDscSqlServerMediaFile.Tests.ps1 @@ -200,21 +200,75 @@ Describe 'Save-SqlDscSqlServerMediaFile' -Tag 'Public' { } } - Context 'When there is already an ISO file in the destination path' { + Context 'When the same ISO file already exists and Force parameter is used' { BeforeAll { Mock -CommandName Get-Item -MockWith { - return @{ - Count = 1 - } + return @( + @{ + FullName = Join-Path -Path $DestinationPath -ChildPath 'media.iso' + Count = 1 + } + ) + } -ParameterFilter { + $Path -eq "$DestinationPath/*.iso" } + + Mock -CommandName Test-Path -MockWith { + return $true + } -ParameterFilter { + $Path -eq (Join-Path -Path $DestinationPath -ChildPath 'media.iso') + } + + Mock -CommandName Invoke-WebRequest + Mock -CommandName Remove-Item } - It 'Should throw the correct error' { - $mockErrorMessage = InModuleScope -ScriptBlock { - $script:localizedData.SqlServerMediaFile_Save_InvalidDestinationFolder + It 'Should allow overwriting the existing file with Force parameter' { + Save-SqlDscSqlServerMediaFile -Url 'https://example.com/media.iso' -DestinationPath $DestinationPath -Force + + Should -Invoke -CommandName Invoke-WebRequest -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Remove-Item -Exactly -Times 1 -Scope It + } + } + + Context 'When URL is an .exe and the same ISO file already exists with Force parameter' { + BeforeAll { + $exeUrl = 'https://example.com/installer.exe' + + Mock -CommandName Get-Item -MockWith { + return @( + @{ + FullName = Join-Path -Path $DestinationPath -ChildPath 'media.iso' + Count = 1 + } + ) + } -ParameterFilter { + $Path -eq "$DestinationPath/*.iso" + } + + Mock -CommandName Test-Path -MockWith { + return $true + } -ParameterFilter { + $Path -eq (Join-Path -Path $DestinationPath -ChildPath 'media.iso') } - { Save-SqlDscSqlServerMediaFile -Url $Url -DestinationPath $DestinationPath -Confirm:$false } | Should -Throw $mockErrorMessage + Mock -CommandName Invoke-WebRequest + Mock -CommandName Start-Process + Mock -CommandName Remove-Item + Mock -CommandName Rename-Item + } + + It 'Should allow overwriting the existing file with Force parameter' { + Save-SqlDscSqlServerMediaFile -Url $exeUrl -DestinationPath $DestinationPath -Force + + Should -Invoke -CommandName Invoke-WebRequest -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Remove-Item -ParameterFilter { + $Path -eq (Join-Path -Path $DestinationPath -ChildPath 'media.iso') + } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Start-Process -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Remove-Item -ParameterFilter { + $Path -eq (Join-Path -Path $DestinationPath -ChildPath 'media.exe') + } -Exactly -Times 1 -Scope It } } }