private/Update-OSDCloudCoreCatalogOS.Tests.ps1
|
BeforeAll { $fixtureRoot = Join-Path $TestDrive 'module' $null = New-Item -Path $fixtureRoot -ItemType Directory $modulePath = Join-Path $fixtureRoot 'CatalogRefreshFixture.psm1' @" . '$PSScriptRoot\Update-OSDCloudCoreCatalogOS.ps1' . '$PSScriptRoot\core-license\Get-OSDCoreLicense.ps1' . '$PSScriptRoot\core-license\Test-OSDCoreLicense.ps1' "@ | Set-Content -LiteralPath $modulePath $fixtureModule = Import-Module $modulePath -Force -PassThru $fixtureModule.SessionState.PSVariable.Set('fixtureRoot', $fixtureRoot) $expandScript = Join-Path $fixtureRoot 'expand.ps1' 'Copy-Item -LiteralPath "$PSScriptRoot\products.xml" -Destination $args[1] -WhatIf:$false; $global:LASTEXITCODE = 0' | Set-Content -LiteralPath $expandScript } AfterAll { Remove-Module $fixtureModule } Describe 'Licensed OS catalog refresh and USB publication' { BeforeEach { $catalogRoot = Join-Path $fixtureRoot 'core\operatingsystems' if (Test-Path $catalogRoot) { Remove-Item $catalogRoot -Recurse -Force } $null = New-Item $catalogRoot -ItemType Directory -Force $usbRoot = Join-Path $fixtureRoot 'usb' if (Test-Path $usbRoot) { Remove-Item $usbRoot -Recurse -Force } $fixtureModule.SessionState.PSVariable.Set('licenseResult', [pscustomobject]@{ IsValid = $true; IsRegistered = $false }) $fixtureModule.SessionState.PSVariable.Set('volumes', @()) $fixtureModule.SessionState.PSVariable.Set('downloadDirectory', $null) $fixtureModule.SessionState.PSVariable.Set('disk', [pscustomobject]@{ BusType = 'USB'; IsReadOnly = $false; IsOffline = $false }) $fixtureModule.SessionState.PSVariable.Set('partition', [pscustomobject]@{ DiskNumber = 7; IsReadOnly = $false; IsOffline = $false }) $identity = '26200.9999.261015-1200' $nodes = foreach ($architecture in 'x64', 'ARM64') { "<File><FileName>$identity.CLIENTBUSINESS_VOL.en-us.$architecture.esd</FileName><LanguageCode>en-us</LanguageCode><Edition>Enterprise</Edition><Architecture>$architecture</Architecture><Size>100</Size><Sha256>$('A' * 64)</Sha256><FilePath>https://example.invalid/$architecture.esd</FilePath></File>" } "<MCT><Catalogs><Catalog><PublishedMedia><Files>$($nodes -join '')</Files></PublishedMedia></Catalog></Catalogs></MCT>" | Set-Content (Join-Path $fixtureRoot 'products.xml') Mock Get-OSDCoreLicense -ModuleName CatalogRefreshFixture { & (Get-Module CatalogRefreshFixture) { $script:licenseResult } } Mock Write-Host -ModuleName CatalogRefreshFixture {} Mock Invoke-RestMethod -ModuleName CatalogRefreshFixture { $sha = [System.Security.Cryptography.SHA256]::Create() try { $digest = [Convert]::ToBase64String($sha.ComputeHash([byte[]]@(1, 2, 3))) } finally { $sha.Dispose() } [pscustomobject]@{ FileLocations = @([pscustomobject]@{ FileName = 'products.cab'; Url = 'https://example.invalid/products.cab'; Size = 3; Digest = $digest }) } } Mock Invoke-WebRequest -ModuleName CatalogRefreshFixture { & (Get-Module CatalogRefreshFixture) { param ($Path) $script:downloadDirectory = Split-Path $Path } $OutFile [System.IO.File]::WriteAllBytes($OutFile, [byte[]]@(1, 2, 3)) } Mock Get-Volume -ModuleName CatalogRefreshFixture { & (Get-Module CatalogRefreshFixture) { $script:volumes } } Mock Get-Partition -ModuleName CatalogRefreshFixture { & (Get-Module CatalogRefreshFixture) { $script:partition } } Mock Get-Disk -ModuleName CatalogRefreshFixture { & (Get-Module CatalogRefreshFixture) { $script:disk } } Mock Join-Path -ModuleName CatalogRefreshFixture -ParameterFilter { $ChildPath -eq 'System32\expand.exe' } { Microsoft.PowerShell.Management\Join-Path $fixtureRoot 'expand.ps1' } Mock Join-Path -ModuleName CatalogRefreshFixture -ParameterFilter { $Path -match '^[F-H]:\\$' } { Microsoft.PowerShell.Management\Join-Path "$fixtureRoot\usb\$($Path.Substring(0, 1))" $ChildPath } } AfterEach { $downloadDirectory = $fixtureModule.SessionState.PSVariable.GetValue('downloadDirectory') if ($downloadDirectory) { Test-Path -LiteralPath $downloadDirectory | Should -BeFalse } @(Get-ChildItem $catalogRoot -Filter '*.tmp' -Force).Count | Should -Be 0 if (Test-Path $usbRoot) { @(Get-ChildItem $usbRoot -Filter '*.tmp' -Recurse -Force).Count | Should -Be 0 } } It 'quietly skips <Case> before any update discovery or side effects' -TestCases @( @{ Case = 'missing license'; License = $null } @{ Case = 'invalid registered license'; License = [pscustomobject]@{ IsValid = $false; IsRegistered = $true } } @{ Case = 'registration only'; License = [pscustomobject]@{ IsRegistered = $true } } ) { param ($License) $fixtureModule.SessionState.PSVariable.Set('licenseResult', $License) Mock Test-Path -ModuleName CatalogRefreshFixture { throw 'Unexpected discovery' } Mock New-Item -ModuleName CatalogRefreshFixture { throw 'Unexpected write' } $result = @(Update-OSDCloudCoreCatalogOS -WarningVariable warnings -ErrorVariable errors) $result.Count | Should -Be 0 @($warnings).Count | Should -Be 0 @($errors).Count | Should -Be 0 Should -Invoke Test-Path -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke New-Item -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Invoke-RestMethod -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Invoke-WebRequest -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Get-Volume -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly } It 'fails closed with only a generic verbose diagnostic on lookup failure' { Mock Get-OSDCoreLicense -ModuleName CatalogRefreshFixture { throw 'Sensitive synthetic lookup detail' } $output = @(Update-OSDCloudCoreCatalogOS -Verbose -WarningVariable warnings 4>&1) "$output" | Should -Match 'License lookup failed' "$output" | Should -Not -Match 'Sensitive synthetic' @($warnings).Count | Should -Be 0 Should -Invoke Invoke-RestMethod -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Get-Volume -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly } It 'suppresses license-reader warnings when skipping an update' { Mock Get-OSDCoreLicense -ModuleName CatalogRefreshFixture { Write-Warning 'Synthetic license-reader warning' -WarningAction $WarningAction $null } $result = @(Update-OSDCloudCoreCatalogOS 3>&1) $result.Count | Should -Be 0 Should -Invoke Get-OSDCoreLicense -ModuleName CatalogRefreshFixture -Times 1 -Exactly -ParameterFilter { $ErrorAction -eq 'Stop' -and $WarningAction -eq 'SilentlyContinue' -and $Verbose -eq $false } Should -Invoke Invoke-RestMethod -ModuleName CatalogRefreshFixture -Times 0 -Exactly } It 'publishes validated content independently of registration and preserves older catalogs' { Set-Content (Join-Path $catalogRoot '26100.1.260101-1200.xml') 'older' $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false $result.Published | Should -BeTrue $result.Build | Should -Be '26200.9999' $result.ItemCount | Should -Be 2 $result.Sha256 | Should -Be (Get-FileHash (Join-Path $fixtureRoot 'products.xml')).Hash $result.ModuleCatalogPath | Should -Be (Join-Path $catalogRoot "$identity.xml") Test-Path $result.ModuleCatalogPath | Should -BeTrue Test-Path (Join-Path $catalogRoot '26100.1.260101-1200.xml') | Should -BeTrue Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 1 -Exactly -ParameterFilter { $Object -like '*Saving OS Catalog:*' -and $Object.EndsWith($result.ModuleCatalogPath) } Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 1 -Exactly -ParameterFilter { $ForegroundColor -eq 'DarkGreen' -and $Object -like '*Recast: OSDCloud is updating the OS catalog.*' } } It 'copies only the updated catalog to multiple named USBs even if module content is unchanged' { Copy-Item (Join-Path $fixtureRoot 'products.xml') (Join-Path $catalogRoot "$identity.xml") Set-Content (Join-Path $catalogRoot '26100.1.260101-1200.xml') 'older' $fixtureModule.SessionState.PSVariable.Set('volumes', @( [pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'F' } [pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'G' } [pscustomobject]@{ FileSystemLabel = 'Other'; DriveLetter = 'H' } [pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = $null } )) $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false $result.Published | Should -BeFalse $result.DriveCatalogPaths.Count | Should -Be 2 foreach ($letter in 'F', 'G') { $files = @(Get-ChildItem "$usbRoot\$letter\OSDCloud\catalogs\operatingsystems" -File) $files.Count | Should -Be 1 $files[0].Name | Should -Be "$identity.xml" $savedPath = $files[0].FullName Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 1 -Exactly -ParameterFilter { $Object -like '*Saving USB OS Catalog:*' -and $Object.EndsWith($savedPath) } } Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly -ParameterFilter { $Object -like '*Saving OS Catalog:*' } Test-Path "$usbRoot\H" | Should -BeFalse Should -Invoke Get-Partition -ModuleName CatalogRefreshFixture -Times 2 -Exactly } It 'excludes <Case> from synchronization' -TestCases @( @{ Case = 'non-USB'; Target = 'disk'; Property = 'BusType'; Value = 'NVMe' } @{ Case = 'read-only disk'; Target = 'disk'; Property = 'IsReadOnly'; Value = $true } @{ Case = 'offline disk'; Target = 'disk'; Property = 'IsOffline'; Value = $true } @{ Case = 'read-only partition'; Target = 'partition'; Property = 'IsReadOnly'; Value = $true } @{ Case = 'offline partition'; Target = 'partition'; Property = 'IsOffline'; Value = $true } ) { param ($Target, $Property, $Value) $fixtureModule.SessionState.PSVariable.GetValue($Target).$Property = $Value $fixtureModule.SessionState.PSVariable.Set('volumes', @([pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'F' })) $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false -WarningAction SilentlyContinue $result.Published | Should -BeTrue $result.DriveCatalogPaths.Count | Should -Be 0 Test-Path $usbRoot | Should -BeFalse } It 'retains module publication when USB discovery fails' { Mock Get-Volume -ModuleName CatalogRefreshFixture { throw 'Synthetic discovery failure' } $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false -WarningVariable warnings -WarningAction SilentlyContinue $result.Published | Should -BeTrue "$warnings" | Should -Match 'Unable to discover' } It 'does not copy to a non-USB drive even when it already has an OSDCloud folder' { $existingFolder = New-Item "$usbRoot\F\OSDCloud" -ItemType Directory -Force Set-Content (Join-Path $existingFolder.FullName 'existing.txt') 'preserve' $fixtureModule.SessionState.PSVariable.GetValue('disk').BusType = 'NVMe' $fixtureModule.SessionState.PSVariable.Set('volumes', @([pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'F' })) $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false $result.DriveCatalogPaths.Count | Should -Be 0 Test-Path "$usbRoot\F\OSDCloud\catalogs" | Should -BeFalse Get-Content (Join-Path $existingFolder.FullName 'existing.txt') | Should -Be 'preserve' } It 'warns on module publication failure and removes the staged catalog' { $null = New-Item (Join-Path $catalogRoot "$identity.xml") -ItemType Directory $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false -WarningVariable warnings -WarningAction SilentlyContinue $result | Should -BeNullOrEmpty "$warnings" | Should -Match 'Catalog update was not successful' Should -Invoke Get-Volume -ModuleName CatalogRefreshFixture -Times 0 -Exactly Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly -ParameterFilter { $Object -like '*OS catalog saved to*' } } It 'continues after a USB <Failure> failure' -TestCases @( @{ Failure = 'inspection' } @{ Failure = 'write' } ) { param ($Failure) $fixtureModule.SessionState.PSVariable.Set('volumes', @( [pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'F' } [pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'G' } )) if ($Failure -eq 'inspection') { Mock Get-Partition -ModuleName CatalogRefreshFixture -ParameterFilter { $DriveLetter -eq 'F' } { throw 'Synthetic inspection failure' } } else { Mock New-Item -ModuleName CatalogRefreshFixture -ParameterFilter { $Path -like '*\usb\F\*' } { throw 'Synthetic denied write' } } $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -Confirm:$false -WarningVariable warnings -WarningAction SilentlyContinue $result.Published | Should -BeTrue $result.DriveCatalogPaths.Count | Should -Be 1 $result.DriveCatalogPaths[0] | Should -BeLike '*\usb\G\*' @($warnings).Count | Should -Be 1 Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 1 -Exactly -ParameterFilter { $Object -like '*Saving USB OS Catalog:*' -and $Object.EndsWith($result.DriveCatalogPaths[0]) } Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly -ParameterFilter { $Object -like '*Saving USB OS Catalog:*\usb\F\*' } } It 'rejects <Case> without changing persistent catalogs' -TestCases @( @{ Case = 'conflicting content' } @{ Case = 'newer catalog' } @{ Case = 'insufficient records' } @{ Case = 'download failure' } @{ Case = 'digest mismatch' } @{ Case = 'size mismatch' } ) { param ($Case) switch ($Case) { 'conflicting content' { Set-Content (Join-Path $catalogRoot "$identity.xml") 'existing' } 'newer catalog' { Set-Content (Join-Path $catalogRoot '26200.10000.261015-1200.xml') 'newer' } 'download failure' { Mock Invoke-WebRequest -ModuleName CatalogRefreshFixture { throw 'Synthetic network failure' } } 'digest mismatch' { Mock Invoke-WebRequest -ModuleName CatalogRefreshFixture { [System.IO.File]::WriteAllBytes($OutFile, [byte[]]@(3, 2, 1)) } } 'size mismatch' { Mock Invoke-WebRequest -ModuleName CatalogRefreshFixture { [System.IO.File]::WriteAllBytes($OutFile, [byte[]]@(1, 2)) } } } $before = @(Get-ChildItem $catalogRoot -File | Get-FileHash) $minimum = if ($Case -eq 'insufficient records') { 50 } else { 2 } $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount $minimum -Confirm:$false -WarningVariable warnings -WarningAction SilentlyContinue $result | Should -BeNullOrEmpty @($warnings).Count | Should -Be 1 $after = @(Get-ChildItem $catalogRoot -File | Get-FileHash) ($after.Hash -join ',') | Should -Be ($before.Hash -join ',') @(Get-ChildItem $catalogRoot -Filter '*.tmp').Count | Should -Be 0 Should -Invoke Get-Volume -ModuleName CatalogRefreshFixture -Times 0 -Exactly } It 'validates under WhatIf without publishing or creating USB folders' { $fixtureModule.SessionState.PSVariable.Set('volumes', @([pscustomobject]@{ FileSystemLabel = 'OSDCloud'; DriveLetter = 'F' })) $result = Update-OSDCloudCoreCatalogOS -MinimumItemCount 2 -WhatIf $result.Published | Should -BeFalse $result.ItemCount | Should -Be 2 @(Get-ChildItem $catalogRoot -File).Count | Should -Be 0 Test-Path $usbRoot | Should -BeFalse Should -Invoke Invoke-WebRequest -ModuleName CatalogRefreshFixture -Times 1 -Exactly Should -Invoke Write-Host -ModuleName CatalogRefreshFixture -Times 0 -Exactly -ParameterFilter { $Object -like '*OS catalog saved to*' } } } |