From 96ccc014d5948c6238bff656c60be34762ad506f Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:31:41 +0200 Subject: [PATCH 1/7] feat: Add --what-if for Microsoft.Windows/UpdateList --- resources/WindowsUpdate/locales/en-us.toml | 1 + resources/WindowsUpdate/src/main.rs | 7 +- .../src/windows_update/export.rs | 1 + .../WindowsUpdate/src/windows_update/set.rs | 13 ++- .../WindowsUpdate/src/windows_update/types.rs | 9 ++ .../tests/windowsupdate_whatif.tests.ps1 | 93 +++++++++++++++++++ .../windowsupdate.dsc.resource.json | 24 ++++- 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 diff --git a/resources/WindowsUpdate/locales/en-us.toml b/resources/WindowsUpdate/locales/en-us.toml index 4c2129bd7..45639e018 100644 --- a/resources/WindowsUpdate/locales/en-us.toml +++ b/resources/WindowsUpdate/locales/en-us.toml @@ -47,6 +47,7 @@ noMatchingUpdateForCriteria = "No matching update found for criteria: %{criteria failedDownloadUpdate = "Failed to download update. Result code: %{code}" failedInstallUpdate = "Failed to install update. Result code: %{code}" failedSerializeOutput = "Failed to serialize output: %{err}" +whatIfInstallUpdate = "Would install update '%{title}'" criteriaTitle = "title '%{value}'" criteriaId = "id '%{value}'" criteriaIsInstalled = "is_installed %{value}" diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index be51553e1..8eb477526 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -81,7 +81,7 @@ fn main() { } #[cfg(windows)] - match windows_update::handle_set(&buffer) { + match windows_update::handle_set(&buffer, parse_what_if_arg(&args)) { Ok(output) => { println!("{}", output); std::process::exit(0); @@ -105,3 +105,8 @@ fn main() { } } } + +#[cfg(windows)] +fn parse_what_if_arg(args: &[String]) -> bool { + args.iter().skip(2).any(|arg| arg == "-w" || arg == "--what-if") +} diff --git a/resources/WindowsUpdate/src/windows_update/export.rs b/resources/WindowsUpdate/src/windows_update/export.rs index 81ab9b6ba..514331bdc 100644 --- a/resources/WindowsUpdate/src/windows_update/export.rs +++ b/resources/WindowsUpdate/src/windows_update/export.rs @@ -18,6 +18,7 @@ pub fn handle_export(input: &str) -> Result { UpdateList { restart_required: None, updates: vec![UpdateInfo { + metadata: None, description: None, id: None, installation_behavior: None, diff --git a/resources/WindowsUpdate/src/windows_update/set.rs b/resources/WindowsUpdate/src/windows_update/set.rs index 5528b826c..6452ad95e 100644 --- a/resources/WindowsUpdate/src/windows_update/set.rs +++ b/resources/WindowsUpdate/src/windows_update/set.rs @@ -10,14 +10,14 @@ use windows::{ Win32::System::UpdateAgent::*, }; -use crate::windows_update::types::{UpdateList, UpdateInfo, extract_update_info}; +use crate::windows_update::types::{UpdateList, UpdateInfo, Metadata, extract_update_info}; /// Gets the computer name using the COMPUTERNAME environment variable fn get_computer_name() -> String { std::env::var("COMPUTERNAME").unwrap_or_else(|_| "localhost".to_string()) } -pub fn handle_set(input: &str) -> Result { +pub fn handle_set(input: &str, what_if: bool) -> Result { // Parse input as UpdateList let update_list: UpdateList = serde_json::from_str(input) .map_err(|e| Error::new(E_INVALIDARG, t!("set.failedParseInput", err = e.to_string())))?; @@ -217,6 +217,15 @@ pub fn handle_set(input: &str) -> Result { let update_info = if is_installed { // Already installed, just return current state extract_update_info(&update)? + } else if what_if { + // What-if: project the state after installation without making changes + let mut projected = extract_update_info(&update)?; + let title = projected.title.clone().unwrap_or_default(); + projected.is_installed = Some(true); + projected.metadata = Some(Metadata { + what_if: Some(vec![t!("set.whatIfInstallUpdate", title = title).to_string()]), + }); + projected } else { // Not installed - proceed with installation // Create update collection for download/install diff --git a/resources/WindowsUpdate/src/windows_update/types.rs b/resources/WindowsUpdate/src/windows_update/types.rs index 99408dfbd..51f94cb22 100644 --- a/resources/WindowsUpdate/src/windows_update/types.rs +++ b/resources/WindowsUpdate/src/windows_update/types.rs @@ -12,9 +12,17 @@ pub struct UpdateList { pub updates: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Metadata { + #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] + pub what_if: Option>, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct UpdateInfo { + #[serde(rename = "_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -175,6 +183,7 @@ pub fn extract_update_info(update: &IUpdate) -> Result { }; Ok(UpdateInfo { + metadata: None, title: Some(title), is_installed: Some(is_installed), description: Some(description), diff --git a/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 b/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 new file mode 100644 index 000000000..4ba35b2e7 --- /dev/null +++ b/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Windows Update WhatIf operation tests' -Skip:(!$IsWindows) { + BeforeDiscovery { + $isAdmin = if ($IsWindows) { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]$identity + $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + else { + $false + } + } + + BeforeAll { + $resourceType = 'Microsoft.Windows/UpdateList' + } + + Context 'Set what-if operation' -Skip:(!$isAdmin -or !$IsWindows) { + It 'Can whatif installing an update without installing it' { + # Find an update that is not installed + $exportOut = '{"updates": [{"isInstalled": false}]}' | dsc resource export -r $resourceType -f - 2>&1 + + if ($LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'export operation failed' + return + } + + $exported = $exportOut | ConvertFrom-Json + if ($exported.updates.Count -eq 0) { + Set-ItResult -Skipped -Because 'no uninstalled updates are available' + return + } + + $testUpdate = $exported.updates[0] + $json = @{ + updates = @( + @{ + id = $testUpdate.id + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + $out = $json | dsc resource set -r $resourceType -f - -w 2>&1 + $LASTEXITCODE | Should -Be 0 + + $result = $out | ConvertFrom-Json + $result.afterState.updates[0].id | Should -Be $testUpdate.id + $result.afterState.updates[0].isInstalled | Should -Be $true + $result.afterState.updates[0]._metadata.whatIf | Should -Not -BeNullOrEmpty + $result.afterState.updates[0]._metadata.whatIf | Should -Contain "Would install update '$($testUpdate.title)'" + + # Assert no mutation happened + $getOut = $json | dsc resource get -r $resourceType -f - 2>&1 + $LASTEXITCODE | Should -Be 0 + ($getOut | ConvertFrom-Json).actualState.updates[0].isInstalled | Should -Be $false + } + + It 'Returns current state without whatIf messages for an already installed update' { + # Find an update that is already installed + $exportOut = '{"updates": [{"isInstalled": true}]}' | dsc resource export -r $resourceType -f - 2>&1 + + if ($LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'export operation failed' + return + } + + $exported = $exportOut | ConvertFrom-Json + if ($exported.updates.Count -eq 0) { + Set-ItResult -Skipped -Because 'no installed updates are available' + return + } + + $testUpdate = $exported.updates[0] + $json = @{ + updates = @( + @{ + id = $testUpdate.id + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + $out = $json | dsc resource set -r $resourceType -f - -w 2>&1 + $LASTEXITCODE | Should -Be 0 + + $result = $out | ConvertFrom-Json + $result.afterState.updates[0].id | Should -Be $testUpdate.id + $result.afterState.updates[0].isInstalled | Should -Be $true + $result.afterState.updates[0]._metadata | Should -BeNullOrEmpty + } + } +} diff --git a/resources/WindowsUpdate/windowsupdate.dsc.resource.json b/resources/WindowsUpdate/windowsupdate.dsc.resource.json index 853f9ca56..142e9e166 100644 --- a/resources/WindowsUpdate/windowsupdate.dsc.resource.json +++ b/resources/WindowsUpdate/windowsupdate.dsc.resource.json @@ -19,11 +19,15 @@ "set": { "executable": "wu_dsc", "args": [ - "set" + "set", + { + "whatIfArg": "--what-if" + } ], "input": "stdin", - "preTest": true, - "return": "state" + "implementsPretest": true, + "return": "state", + "whatIfReturns": "state" }, "export": { "executable": "wu_dsc", @@ -138,6 +142,20 @@ "title": "Installation behavior", "description": "Indicates the reboot behavior expected from installing this update. NeverReboots means the update never requires a reboot, AlwaysRequiresReboot means it always requires one, and CanRequestReboot means it may request a reboot.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/updatelist/resource#installationbehavior\n", "markdownDescription": "Indicates the reboot behavior expected from installing this update.\n\n- **NeverReboots**: The update never requires a reboot\n- **AlwaysRequiresReboot**: The update always requires a reboot\n- **CanRequestReboot**: The update may request a reboot\n\n[Online documentation][01]\n\n[01]: https://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/updatelist/resource#installationbehavior\n" + }, + "_metadata": { + "type": "object", + "title": "Metadata", + "description": "Metadata populated during what-if operations.", + "readOnly": true, + "additionalProperties": false, + "properties": { + "whatIf": { + "type": "array", + "description": "Messages describing what the set operation would do.", + "items": { "type": "string" } + } + } } } } From 6800641de92a7bba4abf881938208b90aa58bffe Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:40:21 +0200 Subject: [PATCH 2/7] Take copilot suggestion --- resources/WindowsUpdate/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index 8eb477526..774b8bc39 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -108,5 +108,5 @@ fn main() { #[cfg(windows)] fn parse_what_if_arg(args: &[String]) -> bool { - args.iter().skip(2).any(|arg| arg == "-w" || arg == "--what-if") + args.iter().any(|arg| arg == "-w" || arg == "--what-if") } From 2a0c4bf9f3c1e3c8e9496c3fa05c7514ca7cd32b Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:31:41 +0200 Subject: [PATCH 3/7] feat: Add --what-if for Microsoft.Windows/UpdateList --- resources/WindowsUpdate/locales/en-us.toml | 1 + resources/WindowsUpdate/src/main.rs | 7 +- .../src/windows_update/export.rs | 1 + .../WindowsUpdate/src/windows_update/set.rs | 13 ++- .../WindowsUpdate/src/windows_update/types.rs | 9 ++ .../tests/windowsupdate_whatif.tests.ps1 | 93 +++++++++++++++++++ .../windowsupdate.dsc.resource.json | 24 ++++- 7 files changed, 142 insertions(+), 6 deletions(-) create mode 100644 resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 diff --git a/resources/WindowsUpdate/locales/en-us.toml b/resources/WindowsUpdate/locales/en-us.toml index 4c2129bd7..45639e018 100644 --- a/resources/WindowsUpdate/locales/en-us.toml +++ b/resources/WindowsUpdate/locales/en-us.toml @@ -47,6 +47,7 @@ noMatchingUpdateForCriteria = "No matching update found for criteria: %{criteria failedDownloadUpdate = "Failed to download update. Result code: %{code}" failedInstallUpdate = "Failed to install update. Result code: %{code}" failedSerializeOutput = "Failed to serialize output: %{err}" +whatIfInstallUpdate = "Would install update '%{title}'" criteriaTitle = "title '%{value}'" criteriaId = "id '%{value}'" criteriaIsInstalled = "is_installed %{value}" diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index be51553e1..8eb477526 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -81,7 +81,7 @@ fn main() { } #[cfg(windows)] - match windows_update::handle_set(&buffer) { + match windows_update::handle_set(&buffer, parse_what_if_arg(&args)) { Ok(output) => { println!("{}", output); std::process::exit(0); @@ -105,3 +105,8 @@ fn main() { } } } + +#[cfg(windows)] +fn parse_what_if_arg(args: &[String]) -> bool { + args.iter().skip(2).any(|arg| arg == "-w" || arg == "--what-if") +} diff --git a/resources/WindowsUpdate/src/windows_update/export.rs b/resources/WindowsUpdate/src/windows_update/export.rs index 81ab9b6ba..514331bdc 100644 --- a/resources/WindowsUpdate/src/windows_update/export.rs +++ b/resources/WindowsUpdate/src/windows_update/export.rs @@ -18,6 +18,7 @@ pub fn handle_export(input: &str) -> Result { UpdateList { restart_required: None, updates: vec![UpdateInfo { + metadata: None, description: None, id: None, installation_behavior: None, diff --git a/resources/WindowsUpdate/src/windows_update/set.rs b/resources/WindowsUpdate/src/windows_update/set.rs index 5528b826c..6452ad95e 100644 --- a/resources/WindowsUpdate/src/windows_update/set.rs +++ b/resources/WindowsUpdate/src/windows_update/set.rs @@ -10,14 +10,14 @@ use windows::{ Win32::System::UpdateAgent::*, }; -use crate::windows_update::types::{UpdateList, UpdateInfo, extract_update_info}; +use crate::windows_update::types::{UpdateList, UpdateInfo, Metadata, extract_update_info}; /// Gets the computer name using the COMPUTERNAME environment variable fn get_computer_name() -> String { std::env::var("COMPUTERNAME").unwrap_or_else(|_| "localhost".to_string()) } -pub fn handle_set(input: &str) -> Result { +pub fn handle_set(input: &str, what_if: bool) -> Result { // Parse input as UpdateList let update_list: UpdateList = serde_json::from_str(input) .map_err(|e| Error::new(E_INVALIDARG, t!("set.failedParseInput", err = e.to_string())))?; @@ -217,6 +217,15 @@ pub fn handle_set(input: &str) -> Result { let update_info = if is_installed { // Already installed, just return current state extract_update_info(&update)? + } else if what_if { + // What-if: project the state after installation without making changes + let mut projected = extract_update_info(&update)?; + let title = projected.title.clone().unwrap_or_default(); + projected.is_installed = Some(true); + projected.metadata = Some(Metadata { + what_if: Some(vec![t!("set.whatIfInstallUpdate", title = title).to_string()]), + }); + projected } else { // Not installed - proceed with installation // Create update collection for download/install diff --git a/resources/WindowsUpdate/src/windows_update/types.rs b/resources/WindowsUpdate/src/windows_update/types.rs index 99408dfbd..51f94cb22 100644 --- a/resources/WindowsUpdate/src/windows_update/types.rs +++ b/resources/WindowsUpdate/src/windows_update/types.rs @@ -12,9 +12,17 @@ pub struct UpdateList { pub updates: Vec, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Metadata { + #[serde(rename = "whatIf", skip_serializing_if = "Option::is_none")] + pub what_if: Option>, +} + #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct UpdateInfo { + #[serde(rename = "_metadata", skip_serializing_if = "Option::is_none")] + pub metadata: Option, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, #[serde(skip_serializing_if = "Option::is_none")] @@ -175,6 +183,7 @@ pub fn extract_update_info(update: &IUpdate) -> Result { }; Ok(UpdateInfo { + metadata: None, title: Some(title), is_installed: Some(is_installed), description: Some(description), diff --git a/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 b/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 new file mode 100644 index 000000000..4ba35b2e7 --- /dev/null +++ b/resources/WindowsUpdate/tests/windowsupdate_whatif.tests.ps1 @@ -0,0 +1,93 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Describe 'Windows Update WhatIf operation tests' -Skip:(!$IsWindows) { + BeforeDiscovery { + $isAdmin = if ($IsWindows) { + $identity = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = [Security.Principal.WindowsPrincipal]$identity + $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + } + else { + $false + } + } + + BeforeAll { + $resourceType = 'Microsoft.Windows/UpdateList' + } + + Context 'Set what-if operation' -Skip:(!$isAdmin -or !$IsWindows) { + It 'Can whatif installing an update without installing it' { + # Find an update that is not installed + $exportOut = '{"updates": [{"isInstalled": false}]}' | dsc resource export -r $resourceType -f - 2>&1 + + if ($LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'export operation failed' + return + } + + $exported = $exportOut | ConvertFrom-Json + if ($exported.updates.Count -eq 0) { + Set-ItResult -Skipped -Because 'no uninstalled updates are available' + return + } + + $testUpdate = $exported.updates[0] + $json = @{ + updates = @( + @{ + id = $testUpdate.id + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + $out = $json | dsc resource set -r $resourceType -f - -w 2>&1 + $LASTEXITCODE | Should -Be 0 + + $result = $out | ConvertFrom-Json + $result.afterState.updates[0].id | Should -Be $testUpdate.id + $result.afterState.updates[0].isInstalled | Should -Be $true + $result.afterState.updates[0]._metadata.whatIf | Should -Not -BeNullOrEmpty + $result.afterState.updates[0]._metadata.whatIf | Should -Contain "Would install update '$($testUpdate.title)'" + + # Assert no mutation happened + $getOut = $json | dsc resource get -r $resourceType -f - 2>&1 + $LASTEXITCODE | Should -Be 0 + ($getOut | ConvertFrom-Json).actualState.updates[0].isInstalled | Should -Be $false + } + + It 'Returns current state without whatIf messages for an already installed update' { + # Find an update that is already installed + $exportOut = '{"updates": [{"isInstalled": true}]}' | dsc resource export -r $resourceType -f - 2>&1 + + if ($LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'export operation failed' + return + } + + $exported = $exportOut | ConvertFrom-Json + if ($exported.updates.Count -eq 0) { + Set-ItResult -Skipped -Because 'no installed updates are available' + return + } + + $testUpdate = $exported.updates[0] + $json = @{ + updates = @( + @{ + id = $testUpdate.id + } + ) + } | ConvertTo-Json -Depth 10 -Compress + + $out = $json | dsc resource set -r $resourceType -f - -w 2>&1 + $LASTEXITCODE | Should -Be 0 + + $result = $out | ConvertFrom-Json + $result.afterState.updates[0].id | Should -Be $testUpdate.id + $result.afterState.updates[0].isInstalled | Should -Be $true + $result.afterState.updates[0]._metadata | Should -BeNullOrEmpty + } + } +} diff --git a/resources/WindowsUpdate/windowsupdate.dsc.resource.json b/resources/WindowsUpdate/windowsupdate.dsc.resource.json index 853f9ca56..142e9e166 100644 --- a/resources/WindowsUpdate/windowsupdate.dsc.resource.json +++ b/resources/WindowsUpdate/windowsupdate.dsc.resource.json @@ -19,11 +19,15 @@ "set": { "executable": "wu_dsc", "args": [ - "set" + "set", + { + "whatIfArg": "--what-if" + } ], "input": "stdin", - "preTest": true, - "return": "state" + "implementsPretest": true, + "return": "state", + "whatIfReturns": "state" }, "export": { "executable": "wu_dsc", @@ -138,6 +142,20 @@ "title": "Installation behavior", "description": "Indicates the reboot behavior expected from installing this update. NeverReboots means the update never requires a reboot, AlwaysRequiresReboot means it always requires one, and CanRequestReboot means it may request a reboot.\n\nhttps://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/updatelist/resource#installationbehavior\n", "markdownDescription": "Indicates the reboot behavior expected from installing this update.\n\n- **NeverReboots**: The update never requires a reboot\n- **AlwaysRequiresReboot**: The update always requires a reboot\n- **CanRequestReboot**: The update may request a reboot\n\n[Online documentation][01]\n\n[01]: https://learn.microsoft.com/powershell/dsc/reference/microsoft.windows/updatelist/resource#installationbehavior\n" + }, + "_metadata": { + "type": "object", + "title": "Metadata", + "description": "Metadata populated during what-if operations.", + "readOnly": true, + "additionalProperties": false, + "properties": { + "whatIf": { + "type": "array", + "description": "Messages describing what the set operation would do.", + "items": { "type": "string" } + } + } } } } From 0c5badd50463c610bd2bdb72437e14308130dacc Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Fri, 10 Jul 2026 14:40:21 +0200 Subject: [PATCH 4/7] Take copilot suggestion --- resources/WindowsUpdate/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index 8eb477526..774b8bc39 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -108,5 +108,5 @@ fn main() { #[cfg(windows)] fn parse_what_if_arg(args: &[String]) -> bool { - args.iter().skip(2).any(|arg| arg == "-w" || arg == "--what-if") + args.iter().any(|arg| arg == "-w" || arg == "--what-if") } From 18cba44ea94bc3192e15f5248b6ff2bab07bf35c Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:56:15 +0200 Subject: [PATCH 5/7] Update manifest --- resources/WindowsUpdate/windowsupdate.dsc.resource.json | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/WindowsUpdate/windowsupdate.dsc.resource.json b/resources/WindowsUpdate/windowsupdate.dsc.resource.json index 142e9e166..6042397e2 100644 --- a/resources/WindowsUpdate/windowsupdate.dsc.resource.json +++ b/resources/WindowsUpdate/windowsupdate.dsc.resource.json @@ -26,6 +26,7 @@ ], "input": "stdin", "implementsPretest": true, + "requireSecurityContext": "elevated", "return": "state", "whatIfReturns": "state" }, From c0c4855bc982f43693d7dc8e1ed7e23bad782acb Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Sat, 11 Jul 2026 06:56:53 +0200 Subject: [PATCH 6/7] Update version --- resources/WindowsUpdate/windowsupdate.dsc.resource.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/WindowsUpdate/windowsupdate.dsc.resource.json b/resources/WindowsUpdate/windowsupdate.dsc.resource.json index 6042397e2..708bf8b28 100644 --- a/resources/WindowsUpdate/windowsupdate.dsc.resource.json +++ b/resources/WindowsUpdate/windowsupdate.dsc.resource.json @@ -8,7 +8,7 @@ "security" ], "type": "Microsoft.Windows/UpdateList", - "version": "0.1.0", + "version": "0.1.1", "get": { "executable": "wu_dsc", "args": [ From 96bec8f08213eac61a56151a3386a398838a2949 Mon Sep 17 00:00:00 2001 From: "G.Reijn" <26114636+Gijsreyn@users.noreply.github.com> Date: Sat, 11 Jul 2026 07:27:39 +0200 Subject: [PATCH 7/7] Add tests --- resources/WindowsUpdate/src/main.rs | 29 +++++++ .../WindowsUpdate/src/windows_update/set.rs | 71 ++++++++++++++-- .../WindowsUpdate/src/windows_update/types.rs | 82 +++++++++++++++++++ 3 files changed, 175 insertions(+), 7 deletions(-) diff --git a/resources/WindowsUpdate/src/main.rs b/resources/WindowsUpdate/src/main.rs index 774b8bc39..07e9e9c78 100644 --- a/resources/WindowsUpdate/src/main.rs +++ b/resources/WindowsUpdate/src/main.rs @@ -110,3 +110,32 @@ fn main() { fn parse_what_if_arg(args: &[String]) -> bool { args.iter().any(|arg| arg == "-w" || arg == "--what-if") } + +#[cfg(all(test, windows))] +mod tests { + use super::parse_what_if_arg; + + fn to_args(args: &[&str]) -> Vec { + args.iter().map(ToString::to_string).collect() + } + + #[test] + fn detects_short_what_if_flag() { + assert!(parse_what_if_arg(&to_args(&["windows_update", "set", "-w"]))); + } + + #[test] + fn detects_long_what_if_flag() { + assert!(parse_what_if_arg(&to_args(&["windows_update", "set", "--what-if"]))); + } + + #[test] + fn returns_false_without_what_if_flag() { + assert!(!parse_what_if_arg(&to_args(&["windows_update", "set"]))); + } + + #[test] + fn does_not_match_similar_arguments() { + assert!(!parse_what_if_arg(&to_args(&["windows_update", "set", "-what-if", "--w", "what-if"]))); + } +} diff --git a/resources/WindowsUpdate/src/windows_update/set.rs b/resources/WindowsUpdate/src/windows_update/set.rs index 6452ad95e..758af6b05 100644 --- a/resources/WindowsUpdate/src/windows_update/set.rs +++ b/resources/WindowsUpdate/src/windows_update/set.rs @@ -17,6 +17,15 @@ fn get_computer_name() -> String { std::env::var("COMPUTERNAME").unwrap_or_else(|_| "localhost".to_string()) } +fn project_what_if_install(mut info: UpdateInfo) -> UpdateInfo { + let title = info.title.clone().unwrap_or_default(); + info.is_installed = Some(true); + info.metadata = Some(Metadata { + what_if: Some(vec![t!("set.whatIfInstallUpdate", title = title).to_string()]), + }); + info +} + pub fn handle_set(input: &str, what_if: bool) -> Result { // Parse input as UpdateList let update_list: UpdateList = serde_json::from_str(input) @@ -219,13 +228,7 @@ pub fn handle_set(input: &str, what_if: bool) -> Result { extract_update_info(&update)? } else if what_if { // What-if: project the state after installation without making changes - let mut projected = extract_update_info(&update)?; - let title = projected.title.clone().unwrap_or_default(); - projected.is_installed = Some(true); - projected.metadata = Some(Metadata { - what_if: Some(vec![t!("set.whatIfInstallUpdate", title = title).to_string()]), - }); - projected + project_what_if_install(extract_update_info(&update)?) } else { // Not installed - proceed with installation // Create update collection for download/install @@ -310,3 +313,57 @@ pub fn handle_set(input: &str, what_if: bool) -> Result { Err(e) => Err(e), } } + +#[cfg(test)] +mod tests { + use super::project_what_if_install; + use crate::windows_update::types::UpdateInfo; + + fn test_update_info() -> UpdateInfo { + UpdateInfo { + metadata: None, + description: None, + id: Some("aeb14a2c-5540-481a-8a4d-6a4f9b962692".to_string()), + installation_behavior: None, + is_installed: Some(false), + is_uninstallable: None, + kb_article_ids: None, + msrc_severity: None, + recommended_hard_disk_space: None, + security_bulletin_ids: None, + title: Some("Test Update".to_string()), + update_type: None, + } + } + + #[test] + fn projects_installed_state_with_what_if_message() { + let projected = project_what_if_install(test_update_info()); + + assert_eq!(projected.is_installed, Some(true)); + let metadata = projected.metadata.expect("metadata should be set"); + let messages = metadata.what_if.expect("whatIf messages should be set"); + assert_eq!(messages.len(), 1); + assert!(messages[0].contains("Test Update")); + } + + #[test] + fn preserves_other_fields() { + let projected = project_what_if_install(test_update_info()); + + assert_eq!(projected.id.as_deref(), Some("aeb14a2c-5540-481a-8a4d-6a4f9b962692")); + assert_eq!(projected.title.as_deref(), Some("Test Update")); + assert!(projected.description.is_none()); + } + + #[test] + fn handles_missing_title() { + let mut info = test_update_info(); + info.title = None; + + let projected = project_what_if_install(info); + + assert_eq!(projected.is_installed, Some(true)); + assert!(projected.metadata.and_then(|m| m.what_if).is_some()); + } +} diff --git a/resources/WindowsUpdate/src/windows_update/types.rs b/resources/WindowsUpdate/src/windows_update/types.rs index 51f94cb22..76ce45584 100644 --- a/resources/WindowsUpdate/src/windows_update/types.rs +++ b/resources/WindowsUpdate/src/windows_update/types.rs @@ -198,3 +198,85 @@ pub fn extract_update_info(update: &IUpdate) -> Result { }) } } + +#[cfg(test)] +mod tests { + use super::{Metadata, UpdateInfo, UpdateList}; + + #[test] + fn metadata_serializes_with_what_if_field() { + let metadata = Metadata { + what_if: Some(vec!["Would install update 'Test'".to_string()]), + }; + + let json = serde_json::to_string(&metadata).expect("serialization should succeed"); + assert_eq!(json, r#"{"whatIf":["Would install update 'Test'"]}"#); + } + + #[test] + fn metadata_skips_empty_what_if_field() { + let metadata = Metadata { what_if: None }; + + let json = serde_json::to_string(&metadata).expect("serialization should succeed"); + assert_eq!(json, "{}"); + } + + #[test] + fn update_info_serializes_metadata_as_underscore_metadata() { + let info = UpdateInfo { + metadata: Some(Metadata { + what_if: Some(vec!["Would install update 'Test'".to_string()]), + }), + description: None, + id: Some("some-id".to_string()), + installation_behavior: None, + is_installed: Some(true), + is_uninstallable: None, + kb_article_ids: None, + msrc_severity: None, + recommended_hard_disk_space: None, + security_bulletin_ids: None, + title: None, + update_type: None, + }; + + let value = serde_json::to_value(&info).expect("serialization should succeed"); + assert_eq!(value["_metadata"]["whatIf"][0], "Would install update 'Test'"); + assert_eq!(value["id"], "some-id"); + assert_eq!(value["isInstalled"], true); + } + + #[test] + fn update_info_skips_metadata_when_none() { + let info = UpdateInfo { + metadata: None, + description: None, + id: Some("some-id".to_string()), + installation_behavior: None, + is_installed: None, + is_uninstallable: None, + kb_article_ids: None, + msrc_severity: None, + recommended_hard_disk_space: None, + security_bulletin_ids: None, + title: None, + update_type: None, + }; + + let value = serde_json::to_value(&info).expect("serialization should succeed"); + assert!(value.get("_metadata").is_none()); + } + + #[test] + fn update_list_round_trips_metadata() { + let json = r#"{"updates":[{"id":"some-id","_metadata":{"whatIf":["message"]}}]}"#; + + let list: UpdateList = serde_json::from_str(json).expect("deserialization should succeed"); + let metadata = list.updates[0].metadata.as_ref().expect("metadata should be present"); + assert_eq!(metadata.what_if.as_deref(), Some(&["message".to_string()][..])); + + let round_tripped = serde_json::to_string(&list).expect("serialization should succeed"); + let reparsed: UpdateList = serde_json::from_str(&round_tripped).expect("round-trip should succeed"); + assert_eq!(reparsed.updates[0].id.as_deref(), Some("some-id")); + } +}