diff --git a/build.ps1 b/build.ps1 index 0afe46305..5cb6b3922 100755 --- a/build.ps1 +++ b/build.ps1 @@ -26,6 +26,11 @@ param( $env:RUSTC_LOG=$null $env:RUSTFLAGS='-Dwarnings' +trap { + Write-Error "An error occurred: $($_ | Out-String)" + exit 1 +} + if ($Verbose) { $env:RUSTC_LOG='rustc_codegen_ssa::back::link=info' } @@ -569,11 +574,7 @@ if ($Test) { (Get-Module -Name Pester -ListAvailable).Path } - try { - Invoke-Pester -ErrorAction Stop - } catch { - throw "Pester had unexpected error: $($_.Exception.Message)" - } + Invoke-Pester -Output Detailed -ErrorAction Stop } function Find-MakeAppx() { diff --git a/dsc/tests/dsc_extension_discover.tests.ps1 b/dsc/tests/dsc_extension_discover.tests.ps1 index b935ffeea..d37455465 100644 --- a/dsc/tests/dsc_extension_discover.tests.ps1 +++ b/dsc/tests/dsc_extension_discover.tests.ps1 @@ -1,6 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +BeforeDiscovery { + try { + $windowWidth = [Console]::WindowWidth + } catch { + $consoleUnavailable = $true + } +} + Describe 'Discover extension tests' { BeforeAll { $oldPath = $env:PATH @@ -15,15 +23,31 @@ Describe 'Discover extension tests' { It 'Discover extensions' { $out = dsc extension list | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 - $out.Count | Should -Be 2 -Because ($out | Out-String) - $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' - $out[0].version | Should -Be '0.1.0' - $out[0].capabilities | Should -BeExactly @('import') - $out[0].manifest | Should -Not -BeNullOrEmpty - $out[1].type | Should -BeExactly 'Test/Discover' - $out[1].version | Should -BeExactly '0.1.0' - $out[1].capabilities | Should -BeExactly @('discover') - $out[1].manifest | Should -Not -BeNullOrEmpty + if ($IsWindows) { + $out.Count | Should -Be 3 -Because ($out | Out-String) + $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' + $out[0].version | Should -Be '0.1.0' + $out[0].capabilities | Should -BeExactly @('import') + $out[0].manifest | Should -Not -BeNullOrEmpty + $out[1].type | Should -Be 'Microsoft.Windows.Appx/Discover' + $out[1].version | Should -Be '0.1.0' + $out[1].capabilities | Should -BeExactly @('discover') + $out[1].manifest | Should -Not -BeNullOrEmpty + $out[2].type | Should -BeExactly 'Test/Discover' + $out[2].version | Should -BeExactly '0.1.0' + $out[2].capabilities | Should -BeExactly @('discover') + $out[2].manifest | Should -Not -BeNullOrEmpty + } else { + $out.Count | Should -Be 2 -Because ($out | Out-String) + $out[0].type | Should -Be 'Microsoft.DSC.Extension/Bicep' + $out[0].version | Should -Be '0.1.0' + $out[0].capabilities | Should -BeExactly @('import') + $out[0].manifest | Should -Not -BeNullOrEmpty + $out[1].type | Should -BeExactly 'Test/Discover' + $out[1].version | Should -BeExactly '0.1.0' + $out[1].capabilities | Should -BeExactly @('discover') + $out[1].manifest | Should -Not -BeNullOrEmpty + } } It 'Filtering works for extension discovered resources' { @@ -94,12 +118,12 @@ Describe 'Discover extension tests' { } } - It 'Table can be not truncated' { + It 'Table can be not truncated' -Skip:($consoleUnavailable) { $output = dsc extension list --output-format table-no-truncate $LASTEXITCODE | Should -Be 0 $foundWideLine = $false foreach ($line in $output) { - if ($line.Length -gt [Console]::WindowWidth) { + if ($line.Length -gt $windowWidth) { $foundWideLine = $true } } diff --git a/dsc/tests/dsc_resource_list.tests.ps1 b/dsc/tests/dsc_resource_list.tests.ps1 index db90fdc45..8ee75845a 100644 --- a/dsc/tests/dsc_resource_list.tests.ps1 +++ b/dsc/tests/dsc_resource_list.tests.ps1 @@ -1,6 +1,14 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +BeforeDiscovery { + try { + $windowWidth = [Console]::WindowWidth + } catch { + $consoleUnavailable = $true + } +} + Describe 'Tests for listing resources' { It 'dsc resource list' { $resources = dsc resource list | ConvertFrom-Json -Depth 10 @@ -89,12 +97,12 @@ Describe 'Tests for listing resources' { $out | Should -BeLike "*ERROR*Adapter not found: foo`*" } - It 'Table is not truncated' { + It 'Table is not truncated' -Skip:($consoleUnavailable) { $output = dsc resource list --output-format table-no-truncate $LASTEXITCODE | Should -Be 0 $foundWideLine = $false foreach ($line in $output) { - if ($line.Length -gt [Console]::WindowWidth) { + if ($line.Length -gt $windowWidth) { $foundWideLine = $true break } diff --git a/dsc_lib/locales/en-us.toml b/dsc_lib/locales/en-us.toml index 956b74047..6a757c2ee 100644 --- a/dsc_lib/locales/en-us.toml +++ b/dsc_lib/locales/en-us.toml @@ -135,6 +135,7 @@ processChildExit = "Process '%{executable}' id %{id} exited with code %{code}" processChildTerminated = "Process '%{executable}' id %{id} terminated by signal" processTerminated = "Process terminated by signal" commandInvoke = "Invoking command '%{executable}' with args %{args}" +commandCwd = "Current working directory: %{cwd}" noArgs = "No args to process" parseAsEnvVars = "Parsing input as environment variables" parseAsStdin = "Parsing input as stdin" diff --git a/dsc_lib/src/discovery/command_discovery.rs b/dsc_lib/src/discovery/command_discovery.rs index 51ec01069..f63668721 100644 --- a/dsc_lib/src/discovery/command_discovery.rs +++ b/dsc_lib/src/discovery/command_discovery.rs @@ -286,10 +286,10 @@ impl ResourceDiscovery for CommandDiscovery { if manifest.kind == Some(Kind::Adapter) { trace!("{}", t!("discovery.commandDiscovery.adapterFound", adapter = resource.type_name)); insert_resource(&mut adapters, &resource, true); - } else { - trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name)); - insert_resource(&mut resources, &resource, true); } + // also make sure to add adapters as a resource as well + trace!("{}", t!("discovery.commandDiscovery.resourceFound", resource = resource.type_name)); + insert_resource(&mut resources, &resource, true); } } } @@ -563,27 +563,20 @@ impl ResourceDiscovery for CommandDiscovery { // TODO: This should be a BTreeMap of the resource name and a BTreeMap of the version and DscResource, this keeps it version sorted more efficiently fn insert_resource(resources: &mut BTreeMap>, resource: &DscResource, skip_duplicate_version: bool) { - if resources.contains_key(&resource.type_name) { - let Some(resource_versions) = resources.get_mut(&resource.type_name) else { - resources.insert(resource.type_name.clone(), vec![resource.clone()]); - return; - }; + if let Some(resource_versions) = resources.get_mut(&resource.type_name) { + debug!("Resource '{}' already exists, checking versions", resource.type_name); // compare the resource versions and insert newest to oldest using semver let mut insert_index = resource_versions.len(); for (index, resource_instance) in resource_versions.iter().enumerate() { let resource_instance_version = match Version::parse(&resource_instance.version) { Ok(v) => v, - Err(err) => { - // write as info since PowerShell resources tend to have invalid semver - info!("Resource '{}' has invalid version: {err}", resource_instance.type_name); + Err(_err) => { continue; }, }; let resource_version = match Version::parse(&resource.version) { Ok(v) => v, - Err(err) => { - // write as info since PowerShell resources tend to have invalid semver - info!("Resource '{}' has invalid version: {err}", resource.type_name); + Err(_err) => { continue; }, }; diff --git a/dsc_lib/src/dscresources/command_resource.rs b/dsc_lib/src/dscresources/command_resource.rs index 41b69e15b..cf4d57156 100644 --- a/dsc_lib/src/dscresources/command_resource.rs +++ b/dsc_lib/src/dscresources/command_resource.rs @@ -697,6 +697,9 @@ async fn run_process_async(executable: &str, args: Option>, input: O #[allow(clippy::implicit_hasher)] pub fn invoke_command(executable: &str, args: Option>, input: Option<&str>, cwd: Option<&str>, env: Option>, exit_codes: Option<&HashMap>) -> Result<(i32, String, String), DscError> { debug!("{}", t!("dscresources.commandResource.commandInvoke", executable = executable, args = args : {:?})); + if let Some(cwd) = cwd { + debug!("{}", t!("dscresources.commandResource.commandCwd", cwd = cwd)); + } tokio::runtime::Builder::new_multi_thread() .enable_all() diff --git a/extensions/appx/appx.tests.ps1 b/extensions/appx/appx.tests.ps1 index 7e3f0bd51..5d505a8a3 100644 --- a/extensions/appx/appx.tests.ps1 +++ b/extensions/appx/appx.tests.ps1 @@ -1,13 +1,17 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'Tests for Appx resource discovery' -Skip:(!$IsWindows){ +BeforeDiscovery { + $runningInCI = $null -ne $env:GITHUB_RUN_ID +} + +Describe 'Tests for Appx resource discovery' -Skip:(!$IsWindows -or $runningInCI) { It 'Should find DSC appx resources' { $out = dsc resource list | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $found = $false foreach ($resource in $out) { - if ($resource.directory.StartsWith("$env:ProgramFiles\WindowsApps\Microsoft.DesiredStateConfiguration-Private")) { + if ($resource.directory.StartsWith("$env:ProgramFiles\WindowsApps")) { $found = $true break } diff --git a/extensions/bicep/bicep.tests.ps1 b/extensions/bicep/bicep.tests.ps1 index a60e65cd7..b6ccd6d79 100644 --- a/extensions/bicep/bicep.tests.ps1 +++ b/extensions/bicep/bicep.tests.ps1 @@ -15,6 +15,7 @@ Describe 'Bicep extension tests' -Skip:(!$foundBicep) { $out = dsc -l trace config get -f $bicepFile 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path $TestDrive/error.log -Raw | Out-String) $out.results[0].result.actualState.output | Should -BeExactly 'Hello, world!' + $bicepFile = $bicepFile.ToString().Replace('\', '\\') (Get-Content -Path $TestDrive/error.log -Raw) | Should -Match "Importing file '$bicepFile' with extension 'Microsoft.DSC.Extension/Bicep'" } @@ -28,9 +29,10 @@ resource invalid 'Microsoft.DSC.Extension/Bicep:1.0' = { properties: { output: 'This is invalid' "@ - $out = dsc -l trace config get -f $bicepFile 2>$TestDrive/error.log | ConvertFrom-Json + dsc -l trace config get -f $bicepFile 2>$TestDrive/error.log | ConvertFrom-Json $LASTEXITCODE | Should -Be 4 -Because (Get-Content -Path $TestDrive/error.log -Raw | Out-String) $content = (Get-Content -Path $TestDrive/error.log -Raw) + $bicepFile = $bicepFile.ToString().Replace('\', '\\') $content | Should -Match "Importing file '$bicepFile' with extension 'Microsoft.DSC.Extension/Bicep'" $content | Should -Match "BCP033" } diff --git a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 index 568789549..cc01c4460 100644 --- a/powershell-adapter/Tests/powershellgroup.config.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.config.tests.ps1 @@ -15,12 +15,13 @@ Describe 'PowerShell adapter resource tests' { $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache.json" } } + AfterAll { $env:PSModulePath = $OldPSModulePath } BeforeEach { - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath + Remove-Item -Force -ErrorAction Ignore -Path $cacheFilePath } It 'Get works on config with class-based resources' { diff --git a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 index ffe97c7e3..da0d20f0c 100644 --- a/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 +++ b/powershell-adapter/Tests/powershellgroup.resource.tests.ps1 @@ -14,12 +14,13 @@ Describe 'PowerShell adapter resource tests' { $cacheFilePath = Join-Path $env:LocalAppData "dsc" "PSAdapterCache.json" } } + AfterAll { $env:PSModulePath = $OldPSModulePath } BeforeEach { - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath + Remove-Item -Force -ErrorAction Ignore -Path $cacheFilePath } It 'Discovery includes class-based resources' { @@ -173,7 +174,7 @@ Describe 'PowerShell adapter resource tests' { # remove the module files Remove-Item -Recurse -Force -Path "$TestDrive/TestClassResource" # verify that cache rebuid happened - dsc -l trace resource list '*' -a Microsoft.DSC/PowerShell 2> $TestDrive/tracing.txt + $null = dsc -l trace resource list '*' -a Microsoft.DSC/PowerShell 2> $TestDrive/tracing.txt $LASTEXITCODE | Should -Be 0 "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Detected non-existent cache entry' diff --git a/powershell-adapter/Tests/win_powershell_cache.tests.ps1 b/powershell-adapter/Tests/win_powershell_cache.tests.ps1 new file mode 100644 index 000000000..8f2ca4b58 --- /dev/null +++ b/powershell-adapter/Tests/win_powershell_cache.tests.ps1 @@ -0,0 +1,183 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +BeforeDiscovery { + if ($IsWindows) { + $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [System.Security.Principal.WindowsPrincipal]::new($identity) + $isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} + +Describe 'WindowsPowerShell adapter resource tests - requires elevated permissions' -Skip:(!$IsWindows -or !$isElevated) { + + BeforeAll { + $OldPSModulePath = $env:PSModulePath + $dscHome = Split-Path (Get-Command dsc -ErrorAction Stop).Source -Parent + $psexeHome = Split-Path (Get-Command powershell -ErrorAction Stop).Source -Parent + $ps7exeHome = Split-Path (Get-Command pwsh -ErrorAction Stop).Source -Parent + $env:DSC_RESOURCE_PATH = $dscHome + [System.IO.Path]::PathSeparator + $psexeHome + [System.IO.Path]::PathSeparator + $ps7exeHome + $null = winrm quickconfig -quiet -force 2>&1 + $env:PSModulePath = $PSScriptRoot + [System.IO.Path]::PathSeparator + $env:PSModulePath + + $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" + $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" + + $script:winPSModule = Resolve-Path -Path (Join-Path $PSScriptRoot '..' 'psDscAdapter' 'win_psDscAdapter.psm1') | Select-Object -ExpandProperty Path + Import-Module $winPSModule -Force -ErrorAction Stop + } + + AfterAll { + $env:PSModulePath = $OldPSModulePath + $env:DSC_RESOURCE_PATH = $null + + # Remove after all the tests are done + Remove-Module $script:winPSModule -Force -ErrorAction Ignore + } + + BeforeEach { + Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 + } + + It 'Windows PowerShell adapter supports File resource' { + + $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell + $LASTEXITCODE | Should -Be 0 + $resources = $r | ConvertFrom-Json + ($resources | Where-Object { $_.Type -eq 'PSDesiredStateConfiguration/File' }).Count | Should -Be 1 + } + + It 'Get works on Binary "File" resource' { + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.DestinationPath | Should -Be "$testFile" + } + + It 'Set works on Binary "File" resource' { + + $testFile = "$testdrive\test.txt" + $null = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '", type: File, contents: HelloWorld, Ensure: present}' | dsc resource set -r 'PSDesiredStateConfiguration/File' -f - + $LASTEXITCODE | Should -Be 0 + Get-Content -Raw -Path $testFile | Should -Be "HelloWorld" + } + + It 'Get works on traditional "Script" resource' { + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\', '\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.actualState.result | Should -Be 'test' + } + + It 'Get works on config with File resource for WinPS' { + + $testFile = "$testdrive\test.txt" + 'test' | Set-Content -Path $testFile -Force + $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt', "$testFile") | dsc config get -f - + $LASTEXITCODE | Should -Be 0 + $res = $r | ConvertFrom-Json + $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" + } + + It 'Verify that there are no cache rebuilds for several sequential executions' { + # first execution should build the cache + $null = dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' + + # next executions following shortly after should Not rebuild the cache + 1..3 | ForEach-Object { + $null = dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt + "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' + } + } + + It 'Verify if assertion is used that no module is cleared in the cache' { + # create a test file in the test drive + $testFile = "$testdrive\test.txt" + New-Item -Path $testFile -ItemType File -Force | Out-Null + + # build the cache + dsc resource list --adapter Microsoft.Windows/WindowsPowerShell | Out-Null + + # Create a test module in the test drive + $testModuleDir = "$testdrive\TestModule\1.0.0" + New-Item -Path $testModuleDir -ItemType Directory -Force | Out-Null + + $manifestContent = @" + @{ + RootModule = 'TestModule.psm1' + ModuleVersion = '1.0.0' + GUID = '$([guid]::NewGuid().Guid)' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) Microsoft Corporation. All rights reserved.' + Description = 'Test module for DSC tests' + PowerShellVersion = '5.1' + DscResourcesToExport = @() + FunctionsToExport = @() + CmdletsToExport = @() + VariablesToExport = @() + AliasesToExport = @() + } +"@ + Set-Content -Path "$testModuleDir\TestModule.psd1" -Value $manifestContent + + $scriptContent = @" +Write-Host 'The DSC world!' +"@ + Set-Content -Path "$testModuleDir\TestModule.psm1" -Value $scriptContent + + # Add the test module directory to PSModulePath + $env:PSModulePath = $testdrive + [System.IO.Path]::PathSeparator + $env:PSModulePath + + $yaml = @" +`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: File + type: Microsoft.Windows/WindowsPowerShell + properties: + resources: + - name: File + type: PSDesiredStateConfiguration/File + properties: + DestinationPath: $testfile + - name: File present + type: Microsoft.DSC/Assertion + properties: + `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Use powershell adapter + type: Microsoft.Windows/WindowsPowerShell + properties: + resources: + - name: File present + type: PSDesiredStateConfiguration/File + properties: + DestinationPath: $testFile + dependsOn: + - "[resourceId('Microsoft.Windows/WindowsPowerShell', 'File')]" + - name: TestPSRepository + type: PSTestModule/TestPSRepository + properties: + Name: NuGet + dependsOn: + - "[resourceId('Microsoft.Windows/WindowsPowerShell', 'File')]" + - "[resourceId('Microsoft.DSC/Assertion', 'File present')]" +"@ + # output to file for Windows PowerShell 5.1 + $filePath = "$testdrive\test.assertion.dsc.resource.yaml" + $yaml | Set-Content -Path $filePath -Force + dsc config test -f $filePath 2> "$TestDrive/error.txt" + $LASTEXITCODE | Should -Be 2 + + $cache = Get-Content -Path $cacheFilePath_v5 -Raw | ConvertFrom-Json + $cache.ResourceCache.Type | Should -Contain 'PSTestModule/TestPSRepository' + $cache.ResourceCache.Type | Should -Contain 'PSDesiredStateConfiguration/File' + } +} diff --git a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 index 077170815..7f99b136b 100644 --- a/powershell-adapter/Tests/win_powershellgroup.tests.ps1 +++ b/powershell-adapter/Tests/win_powershellgroup.tests.ps1 @@ -1,282 +1,24 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'WindowsPowerShell adapter resource tests - requires elevated permissions' { - - BeforeAll { - if ($isWindows) { - winrm quickconfig -quiet -force - $OldPSModulePath = $env:PSModulePath - $env:PSModulePath += [System.IO.Path]::PathSeparator + $PSScriptRoot - - $winpsConfigPath = Join-path $PSScriptRoot "winps_resource.dsc.yaml" - if ($isWindows) { - $cacheFilePath_v5 = Join-Path $env:LocalAppData "dsc" "WindowsPSAdapterCache.json" - } - } - } - AfterAll { - if ($isWindows) { - $env:PSModulePath = $OldPSModulePath - - # Remove after all the tests are done - Remove-Module $script:winPSModule -Force -ErrorAction Ignore - } - } - - BeforeEach { - if ($isWindows) { - Remove-Item -Force -ea SilentlyContinue -Path $cacheFilePath_v5 +BeforeDiscovery { + if ($IsWindows) { + $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [System.Security.Principal.WindowsPrincipal]::new($identity) + $isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } - } - - It 'Windows PowerShell adapter supports File resource' -Skip:(!$IsWindows) { - - $r = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell - $LASTEXITCODE | Should -Be 0 - $resources = $r | ConvertFrom-Json - ($resources | Where-Object { $_.Type -eq 'PSDesiredStateConfiguration/File' }).Count | Should -Be 1 - } - - It 'Get works on Binary "File" resource' -Skip:(!$IsWindows) { - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '"}' | dsc resource get -r 'PSDesiredStateConfiguration/File' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.DestinationPath | Should -Be "$testFile" - } - - It 'Set works on Binary "File" resource' -Skip:(!$IsWindows) { - - $testFile = "$testdrive\test.txt" - $null = '{"DestinationPath":"' + $testFile.replace('\', '\\') + '", type: File, contents: HelloWorld, Ensure: present}' | dsc resource set -r 'PSDesiredStateConfiguration/File' -f - - $LASTEXITCODE | Should -Be 0 - Get-Content -Raw -Path $testFile | Should -Be "HelloWorld" - } - - It 'Get works on traditional "Script" resource' -Skip:(!$IsWindows) { - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = '{"GetScript": "@{result = $(Get-Content ' + $testFile.replace('\', '\\') + ')}", "SetScript": "throw", "TestScript": "throw"}' | dsc resource get -r 'PSDesiredStateConfiguration/Script' -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.actualState.result | Should -Be 'test' - } - - It 'Get works on config with File resource for WinPS' -Skip:(!$IsWindows) { - - $testFile = "$testdrive\test.txt" - 'test' | Set-Content -Path $testFile -Force - $r = (Get-Content -Raw $winpsConfigPath).Replace('c:\test.txt', "$testFile") | dsc config get -f - - $LASTEXITCODE | Should -Be 0 - $res = $r | ConvertFrom-Json - $res.results[0].result.actualState.result[0].properties.DestinationPath | Should -Be "$testFile" - } - - It 'Verify that there are no cache rebuilds for several sequential executions' -Skip:(!$IsWindows) { - # remove cache file - $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore - - # first execution should build the cache - dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -FileContentMatchExactly 'Constructing Get-DscResource cache' - - # next executions following shortly after should Not rebuild the cache - 1..3 | ForEach-Object { - dsc -l trace resource list -a Microsoft.Windows/WindowsPowerShell 2> $TestDrive/tracing.txt - "$TestDrive/tracing.txt" | Should -Not -FileContentMatchExactly 'Constructing Get-DscResource cache' - } - } - - It 'Verify if assertion is used that no module is cleared in the cache' -Skip:(!$IsWindows) { - # create a test file in the test drive - $testFile = "$testdrive\test.txt" - New-Item -Path $testFile -ItemType File -Force | Out-Null - - # remove cache file - $cacheFilePath = Join-Path $env:LocalAppData "dsc\WindowsPSAdapterCache.json" - Remove-Item -Force -Path $cacheFilePath -ErrorAction Ignore - - # build the cache - dsc resource list --adapter Microsoft.Windows/WindowsPowerShell | Out-Null - - # Create a test module in the test drive - $testModuleDir = "$testdrive\TestModule\1.0.0" - New-Item -Path $testModuleDir -ItemType Directory -Force | Out-Null - - $manifestContent = @" - @{ - RootModule = 'TestModule.psm1' - ModuleVersion = '1.0.0' - GUID = $([guid]::NewGuid().Guid) - Author = 'Microsoft Corporation' - CompanyName = 'Microsoft Corporation' - Copyright = '(c) Microsoft Corporation. All rights reserved.' - Description = 'Test module for DSC tests' - PowerShellVersion = '5.1' - DscResourcesToExport = @() - FunctionsToExport = @() - CmdletsToExport = @() - VariablesToExport = @() - AliasesToExport = @() - } -"@ - Set-Content -Path "$testModuleDir\TestModule.psd1" -Value $manifestContent - - $scriptContent = @" -Write-Host 'The DSC world!' -"@ - Set-Content -Path "$testModuleDir\TestModule.psm1" -Value $scriptContent - - # Add the test module directory to PSModulePath - $env:PSModulePath += [System.IO.Path]::PathSeparator + $testdrive - - $yaml = @" -`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -resources: - - name: File - type: Microsoft.Windows/WindowsPowerShell - properties: - resources: - - name: File - type: PSDesiredStateConfiguration/File - properties: - DestinationPath: $testfile - - name: File present - type: Microsoft.DSC/Assertion - properties: - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Use powershell adapter - type: Microsoft.Windows/WindowsPowerShell - properties: - resources: - - name: File present - type: PSDesiredStateConfiguration/File - properties: - DestinationPath: $testFile - dependsOn: - - "[resourceId('Microsoft.Windows/WindowsPowerShell', 'File')]" - - name: TestPSRepository - type: PSTestModule/TestPSRepository - properties: - Name: NuGet - dependsOn: - - "[resourceId('Microsoft.Windows/WindowsPowerShell', 'File')]" - - "[resourceId('Microsoft.DSC/Assertion', 'File present')]" -"@ - # output to file for Windows PowerShell 5.1 - $filePath = "$testdrive\test.assertion.dsc.resource.yaml" - $yaml | Set-Content -Path $filePath -Force - dsc config test -f $filePath 2> "$TestDrive/error.txt" - $LASTEXITCODE | Should -Be 2 - - $cache = Get-Content -Path $cacheFilePath -Raw | ConvertFrom-Json - $cache.ResourceCache.Type | Should -Contain 'PSTestModule/TestPSRepository' - $cache.ResourceCache.Type | Should -Contain 'PSDesiredStateConfiguration/File' - } - - It '_inDesiredState is returned correction: ' -Skip:(!$IsWindows) -TestCases @( - @{ Context = 'Both running'; FirstState = 'Running'; SecondState = 'Running' } - @{ Context = 'Both stopped'; FirstState = 'Stopped'; SecondState = 'Stopped' } - @{ Context = 'First Stopped'; FirstState = 'Stopped'; SecondState = 'Running' } - @{ Context = 'First Running'; FirstState = 'Running'; SecondState = 'Stopped' } - ) { - param($Context, $FirstState, $SecondState) - $yaml = @" -`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json -resources: - - name: Use Windows PowerShell resources - type: Microsoft.Windows/WindowsPowerShell - properties: - resources: - - name: Check Spooler service 1 - type: PsDesiredStateConfiguration/Service - properties: - Name: Spooler - State: $FirstState - - name: Check Spooler service 2 - type: PsDesiredStateConfiguration/Service - properties: - Name: Spooler - State: $SecondState -"@ - - $inDesiredState = if ($FirstState -eq $SecondState) { - $FirstState -eq (Get-Service Spooler).Status - } - else { - $false - } - - $out = dsc config test -i $yaml | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.results[0].result.inDesiredState | Should -Be $inDesiredState - } - - It 'Config works with credential object' -Skip:(!$IsWindows) { - BeforeDiscovery { - $script:winPSModule = Resolve-Path -Path (Join-Path $PSScriptRoot '..' 'psDscAdapter' 'win_psDscAdapter.psm1') | Select-Object -ExpandProperty Path - Import-Module $winPSModule -Force -ErrorAction Stop - - # Mock the command to work on GitHub runners because Microsoft.PowerShell.Security is not available - Mock -CommandName ConvertTo-SecureString -MockWith { [System.Security.SecureString]::new() } - } - - $jsonInput = @{ - resources = @{ - name = 'Service info' - type = 'PSDesiredStateConfiguration/Service' - properties = @{ - Name = 'Spooler' - Credential = @{ - UserName = 'User' - Password = 'Password' - } - } - } - } | ConvertTo-Json -Depth 10 - - # Instead of calling dsc.exe we call the cmdlet directly to be able to test the output and mocks - $resourceObject = Get-DscResourceObject -jsonInput $jsonInput - $cacheEntry = Invoke-DscCacheRefresh -Module PSDesiredStateConfiguration - - $out = Invoke-DscOperation -Operation Test -DesiredState $resourceObject -dscResourceCache $cacheEntry - $LASTEXITCODE | Should -Be 0 - $out.properties.InDesiredState.InDesiredState | Should -Be $false - - Should -Invoke -CommandName ConvertTo-SecureString -Exactly -Times 1 -Scope It - } - - It 'Config does not work when credential properties are missing required fields' -Skip:(!$IsWindows) { - $yaml = @" - `$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json - resources: - - name: Service info - type: PsDesiredStateConfiguration/Service - properties: - Name: Spooler - Credential: - UserName: 'User' - OtherProperty: 'Password' -"@ - # Compared to PowerShell we use test here as it filters out the properties - $out = dsc config test -i $yaml 2>&1 | Out-String - $LASTEXITCODE | Should -Be 2 - $out | Should -Not -BeNullOrEmpty - $out | Should -BeLike "*ERROR*Credential object 'Credential' requires both 'username' and 'password' properties*" - } +} - It 'List works with class-based PS DSC resources' -Skip:(!$IsWindows) { - BeforeDiscovery { - $windowsPowerShellPath = Join-Path $testDrive 'WindowsPowerShell' 'Modules' - $env:PSModulePath += [System.IO.Path]::PathSeparator + $windowsPowerShellPath +Describe 'WindowsPowerShell adapter resource tests - requires elevated permissions' -Skip:(!$IsWindows -or !$isElevated) { - $moduleFile = @" + BeforeAll { + $OldPSModulePath = $env:PSModulePath + $dscHome = Split-Path (Get-Command dsc -ErrorAction Stop).Source -Parent + $psexeHome = Split-Path (Get-Command powershell -ErrorAction Stop).Source -Parent + $ps7exeHome = Split-Path (Get-Command pwsh -ErrorAction Stop).Source -Parent + $env:DSC_RESOURCE_PATH = $dscHome + [System.IO.Path]::PathSeparator + $psexeHome + [System.IO.Path]::PathSeparator + $ps7exeHome + $windowsPowerShellPath = Join-Path $testDrive 'WindowsPowerShell' 'Modules' + $moduleFile = @" @{ RootModule = 'PSClassResource.psm1' ModuleVersion = '0.1.0' @@ -304,13 +46,12 @@ resources: } } "@ - $moduleFilePath = Join-Path $windowsPowerShellPath 'PSClassResource' '0.1.0' 'PSClassResource.psd1' - if (-not (Test-Path -Path $moduleFilePath)) { - New-Item -Path $moduleFilePath -ItemType File -Value $moduleFile -Force | Out-Null - } - + $moduleFilePath = Join-Path $windowsPowerShellPath 'PSClassResource' '0.1.0' 'PSClassResource.psd1' + if (-not (Test-Path -Path $moduleFilePath)) { + New-Item -Path $moduleFilePath -ItemType File -Value $moduleFile -Force | Out-Null + } - $module = @' + $module = @' enum Ensure { Present Absent @@ -329,6 +70,9 @@ class PSClassResource { [DscProperty()] [Ensure] $Ensure = [Ensure]::Present + [DscProperty()] + [PSCredential] $Credential + PSClassResource() { } @@ -341,7 +85,13 @@ class PSClassResource { } [void] Set() { + if ($null -eq $this.Credential) { + throw 'Credential property is required' + } + if ($this.Credential.UserName -ne 'MyUser') { + throw 'Invalid user name' + } } static [PSClassResource[]] Export() @@ -363,39 +113,119 @@ class PSClassResource { } '@ - $modulePath = Join-Path $windowsPowerShellPath 'PSClassResource' '0.1.0' 'PSClassResource.psm1' - if (-not (Test-Path -Path $modulePath)) { - New-Item -Path $modulePath -ItemType File -Value $module -Force | Out-Null - } + $modulePath = Join-Path $windowsPowerShellPath 'PSClassResource' '0.1.0' 'PSClassResource.psm1' + if (-not (Test-Path -Path $modulePath)) { + New-Item -Path $modulePath -ItemType File -Value $module -Force | Out-Null } - $out = dsc -l trace resource list --adapter Microsoft.Windows/WindowsPowerShell | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.type | Should -Contain 'PSClassResource/PSClassResource' - $out | Where-Object -Property type -EQ PSClassResource/PSClassResource | Select-Object -ExpandProperty implementedAs | Should -Be 1 # Class-based - ($out | Where-Object -Property type -EQ 'PSClassResource/PSClassResource').capabilities | Should -BeIn @('get', 'test', 'set', 'export') + $env:PSModulePath = $windowsPowerShellPath + [System.IO.Path]::PathSeparator + $env:PSModulePath + } + + AfterAll { + $env:PSModulePath = $OldPSModulePath + $env:DSC_RESOURCE_PATH = $null + } + + It '_inDesiredState is returned correction: ' -TestCases @( + @{ Context = 'Both running'; FirstState = 'Running'; SecondState = 'Running' } + @{ Context = 'Both stopped'; FirstState = 'Stopped'; SecondState = 'Stopped' } + @{ Context = 'First Stopped'; FirstState = 'Stopped'; SecondState = 'Running' } + @{ Context = 'First Running'; FirstState = 'Running'; SecondState = 'Stopped' } + ) { + param($Context, $FirstState, $SecondState) + $yaml = @" +`$schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json +resources: + - name: Use Windows PowerShell resources + type: Microsoft.Windows/WindowsPowerShell + properties: + resources: + - name: Check Spooler service 1 + type: PsDesiredStateConfiguration/Service + properties: + Name: Spooler + State: $FirstState + - name: Check Spooler service 2 + type: PsDesiredStateConfiguration/Service + properties: + Name: Spooler + State: $SecondState +"@ + + $inDesiredState = if ($FirstState -eq $SecondState) { + $FirstState -eq (Get-Service Spooler).Status + } + else { + $false + } + + $out = dsc -l trace config test -i $yaml 2>"$testdrive/error.log" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) + $out.results[0].result.inDesiredState | Should -Be $inDesiredState } - It 'Get works with class-based PS DSC resources' -Skip:(!$IsWindows) { + It 'Config works with credential object' { + $yaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Cred test + type: PSClassResource/PSClassResource + properties: + Name: Test + Credential: + UserName: 'MyUser' + Password: 'MyPassword' +'@ + + $out = dsc -l debug config set -i $yaml 2> "$testdrive/error.log" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) + } - $out = dsc resource get -r PSClassResource/PSClassResource --input (@{Name = 'TestName' } | ConvertTo-Json) | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + It 'Config does not work when credential properties are missing required fields' { + $yaml = @' + $schema: https://aka.ms/dsc/schemas/v3/bundled/config/document.json + resources: + - name: Service info + type: PsDesiredStateConfiguration/Service + properties: + Name: Spooler + Credential: + UserName: 'User' + OtherProperty: 'Password' +'@ + # Compared to PowerShell we use test here as it filters out the properties + $out = dsc -l debug config test -i $yaml 2> "$testdrive/error.log" | Out-String + $LASTEXITCODE | Should -Be 2 + $out | Should -BeNullOrEmpty + (Get-Content -Path "$testdrive/error.log" -Raw) | Should -BeLike "*ERROR*Credential object 'Credential' requires both 'username' and 'password' properties*" -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) + } + + It 'List works with class-based PS DSC resources' { + $out = dsc resource list --adapter Microsoft.Windows/WindowsPowerShell 2> "$testdrive/error.log" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) + $out.type | Should -Contain 'PSClassResource/PSClassResource' -Because ($out.type | Out-String) + $out | Where-Object -Property type -EQ PSClassResource/PSClassResource | Select-Object -ExpandProperty implementedAs | Should -Be 1 + ($out | Where-Object -Property type -EQ 'PSClassResource/PSClassResource').capabilities | Should -BeIn @('get', 'test', 'set', 'export') + } + + It 'Get works with class-based PS DSC resources' { + $out = dsc resource get -r PSClassResource/PSClassResource --input (@{Name = 'TestName' } 2> "$testdrive/error.log" | ConvertTo-Json) | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) $out.actualState.Name | Should -Be 'TestName' + $out.actualState.Ensure | Should -Be 'Present' $propCount = $out.actualState | Get-Member -MemberType NoteProperty - $propCount.Count | Should -Be 1 # Only the DscProperty should be returned + $propCount.Count | Should -Be 3 -Because ($out | Out-String) } - It 'Set works with class-based PS DSC resources' -Skip:(!$IsWindows) { - - $out = dsc resource set -r PSClassResource/PSClassResource --input (@{Name = 'TestName' } | ConvertTo-Json) | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + It 'Set works with class-based PS DSC resources' { + $out = dsc resource set -r PSClassResource/PSClassResource --input (@{Name = 'TestName' } 2> "$testdrive/error.log" | ConvertTo-Json) | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) $out.afterstate.InDesiredState | Should -Be $true } - It 'Export works with class-based PS DSC resources' -Skip:(!$IsWindows) { - - $out = dsc resource export -r PSClassResource/PSClassResource | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 + It 'Export works with class-based PS DSC resources' { + $out = dsc resource export -r PSClassResource/PSClassResource 2> "$testdrive/error.log" | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 -Because (Get-Content -Path "$testdrive/error.log" -Raw | Out-String) $out | Should -Not -BeNullOrEmpty $out.resources.count | Should -Be 5 $out.resources[0].properties.Ensure | Should -Be 'Present' # Check for enum property diff --git a/powershell-adapter/psDscAdapter/powershell.resource.ps1 b/powershell-adapter/psDscAdapter/powershell.resource.ps1 index ccee164ae..73717a900 100644 --- a/powershell-adapter/psDscAdapter/powershell.resource.ps1 +++ b/powershell-adapter/psDscAdapter/powershell.resource.ps1 @@ -22,6 +22,10 @@ function Write-DscTrace { $host.ui.WriteErrorLine($trace) } +trap { + Write-DscTrace -Operation Debug -Message ($_ | Format-List -Force | Out-String) +} + # Adding some debug info to STDERR 'PSVersion=' + $PSVersionTable.PSVersion.ToString() | Write-DscTrace 'PSPath=' + $PSHome | Write-DscTrace diff --git a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 index ba302ac87..47ea29bee 100644 --- a/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 +++ b/powershell-adapter/psDscAdapter/win_psDscAdapter.psm1 @@ -4,6 +4,10 @@ $global:ProgressPreference = 'SilentlyContinue' $script:CurrentCacheSchemaVersion = 1 +trap { + Write-DscTrace -Operation Debug -Message ($_ | Format-List -Force | Out-String) +} + function Write-DscTrace { param( [Parameter(Mandatory = $false)] @@ -133,11 +137,14 @@ function Invoke-DscCacheRefresh { # improve by performance by having the option to only get details for named modules # workaround for File and SignatureValidation resources that ship in Windows + Write-DscTrace -Operation Debug "Named module count: $($namedModules.Count)" if ($namedModules.Count -gt 0) { + Write-DscTrace -Operation Debug "Modules specified, getting DSC resources from modules: $($namedModules -join ', ')" $DscResources = [System.Collections.Generic.List[Object]]::new() $Modules = [System.Collections.Generic.List[Object]]::new() $filteredResources = @() foreach ($m in $namedModules) { + Write-DscTrace -Operation Debug "Getting DSC resources for module '$($m | Out-String)'" $DscResources.AddRange(@(Get-DscResource -Module $m)) $Modules.AddRange(@(Get-Module -Name $m -ListAvailable)) } @@ -156,7 +163,7 @@ function Invoke-DscCacheRefresh { # Exclude the one module that was passed in as a parameter $existingDscResourceCacheEntries = @($cache.ResourceCache | Where-Object -Property Type -NotIn $filteredResources) } else { - # if no module is specified, get all resources + Write-DscTrace -Operation Debug "No modules specified, getting all DSC resources" $DscResources = Get-DscResource $Modules = Get-Module -ListAvailable } @@ -352,7 +359,7 @@ function Invoke-DscOperation { $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - Write-DscTrace -Operation Debug -Message ($validateProperty | Out-String) + Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" if ($validateProperty -and $validateProperty.PropertyType -eq '[PSCredential]') { if (-not $_.Value.Username -or -not $_.Value.Password) { "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error @@ -406,7 +413,8 @@ function Invoke-DscOperation { # handle input objects by converting them to a hash table if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name - if ($validateProperty.PropertyType -eq '[PSCredential]') { + Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" + if ($validateProperty.PropertyType -eq 'PSCredential') { if (-not $_.Value.Username -or -not $_.Value.Password) { "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error exit 1 @@ -483,6 +491,7 @@ function Invoke-DscOperation { $DesiredState.properties.psobject.properties | ForEach-Object -Begin { $property = @{} } -Process { if ($_.Value -is [System.Management.Automation.PSCustomObject]) { $validateProperty = $cachedDscResourceInfo.Properties | Where-Object -Property Name -EQ $_.Name + Write-DscTrace -Operation Debug -Message "Property type: $($validateProperty.PropertyType)" if ($validateProperty.PropertyType -eq '[PSCredential]') { if (-not $_.Value.Username -or -not $_.Value.Password) { "Credential object '$($_.Name)' requires both 'username' and 'password' properties" | Write-DscTrace -Operation Error diff --git a/reboot_pending/tests/reboot_pending.tests.ps1 b/reboot_pending/tests/reboot_pending.tests.ps1 index d6bf5a80c..81658ac14 100644 --- a/reboot_pending/tests/reboot_pending.tests.ps1 +++ b/reboot_pending/tests/reboot_pending.tests.ps1 @@ -1,33 +1,47 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -Describe 'reboot_pending resource tests' { - It 'should get reboot_pending' -Skip:(!$IsWindows) { +BeforeDiscovery { + if ($IsWindows) { + $identity = [System.Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [System.Security.Principal.WindowsPrincipal]::new($identity) + $isElevated = $principal.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) + } +} + +Describe 'reboot_pending resource tests' -Skip:(!$IsWindows -or !$isElevated) { + BeforeAll { + $keyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" + $keyName = "RebootRequired" + if (-not (Get-ItemProperty "$keyPath\$keyName" -ErrorAction Ignore)) { + New-ItemProperty -Path $keyPath -Name $keyName -Value 1 -PropertyType DWord -Force | Out-Null + } + } + + AfterAll { + Remove-ItemProperty -Path $keyPath -Name $keyName -ErrorAction Ignore + } + + It 'should get reboot_pending' { $out = dsc resource get -r Microsoft.Windows/RebootPending | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.actualState.rebootPending | Should -Not -BeNullOrEmpty } - It 'reboot_pending works in a config' -Skip:(!$IsWindows) { + It 'reboot_pending works in a config' { $ConfigPath = Resolve-Path "$PSScriptRoot/reboot_pending.dsc.yaml" $out = dsc config get --file $ConfigPath | ConvertFrom-Json $LASTEXITCODE | Should -Be 0 $out.results.result.actualState.rebootPending | Should -Not -BeNullOrEmpty } - It 'reboot_pending should have a reason' -Skip:(!$IsWindows) { - $keyPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update" - $keyName = "RebootRequired" - try { - if (-not (Get-ItemProperty "$keyPath\$keyName" -ErrorAction SilentlyContinue)) { - New-ItemProperty -Path $keyPath -Name $keyName -Value 1 -PropertyType DWord -Force | Out-Null - } - - $out | dsc resource get -r Microsoft.Windows/RebootPending | ConvertFrom-Json - $LASTEXITCODE | Should -Be 0 - $out.actualState.reason | Should -Not -BeNullOrEmpty - } finally { - Remove-ItemProperty -Path $keyPath -Name $keyName -ErrorAction Ignore + It 'reboot_pending should have a reason' { + $out = dsc resource get -r Microsoft.Windows/RebootPending | ConvertFrom-Json + $LASTEXITCODE | Should -Be 0 + if ($out.actualState.rebootPending) { + $out.actualState.reason.count | Should -BeGreaterThan 0 -Because ($out | ConvertTo-Json -Depth 10 |Out-String) + } else { + $out.actualState.reason | Should -BeNullOrEmpty -Because ($out | ConvertTo-Json -Depth 10 |Out-String) } } }