diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f5215a4..391f26b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -95,6 +95,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- SystemProtection + - New resource to configure System Protection settings (formerly xWindowsRestore) - Fixes [Issue #364](https://github.com/dsccommunity/ComputerManagementDsc/issues/364). +- SystemRestorePoint + - New resource to create and delete restore points (formerly xSystemRestorePoint) - Fixed [Issue #364](https://github.com/dsccommunity/ComputerManagementDsc/issues/364). - WindowsEventLog - Added support to restrict guest access - Fixes [Issue #338](https://github.com/dsccommunity/ComputerManagementDsc/issues/338). - Added support to create custom event sources and optionally register diff --git a/README.md b/README.md index 2a258312..9555660b 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,11 @@ The **ComputerManagementDsc** module contains the following resources: settings on the local machine. - **SmbShare**: This resource is used to manage SMB shares on a machine. - **SystemLocale**: This resource is used to set the system locale on a - Windows machine. + Windows machine +- **SystemProtection**: This resource is used to configure System Protection settings + on a workstation (servers are not supported). +- **SystemRestorePoint**: This resource is used to create and delete restore points + when System Protection is enabled (servers are not supported). - **TimeZone**: This resource is used for setting the time zone on a machine. - **UserAccountControl**: This resource allows you to configure the notification level or granularly configure the User Account Control for the computer. diff --git a/source/ComputerManagementDsc.psd1 b/source/ComputerManagementDsc.psd1 index d29c9572..f7bda2a2 100644 --- a/source/ComputerManagementDsc.psd1 +++ b/source/ComputerManagementDsc.psd1 @@ -39,7 +39,26 @@ AliasesToExport = @() # DSC resources to export from this module - DscResourcesToExport = @() + DscResourcesToExport = @( + 'Computer' + 'OfflineDomainJoin' + 'PendingReboot' + 'PowerPlan' + 'PowerShellExecutionPolicy' + 'RemoteDesktopAdmin' + 'ScheduledTask' + 'SmbServerConfiguration' + 'SmbShare' + 'SystemLocale' + 'SystemProtection' + 'SystemRestorePoint' + 'TimeZone' + 'VirtualMemory' + 'WindowsEventLog' + 'WindowsCapability' + 'IEEnhancedSecurityConfiguration' + 'UserAccountControl' + ) # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. PrivateData = @{ diff --git a/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.psm1 b/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.psm1 new file mode 100644 index 00000000..01ec2e39 --- /dev/null +++ b/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.psm1 @@ -0,0 +1,563 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the current system protection state. + + .PARAMETER Ensure + Specifies the desired state of the resource. + + .PARAMETER DriveLetter + Specifies the drive letter to get. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidatePattern('^[A-Za-z]$')] + [System.String] + $DriveLetter + ) +` + $returnValue = @{ + Ensure = 'Absent' + } + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -eq 1) + { + Write-Verbose -Message ($script:localizedData.FoundWorkstationOS -f $productType) + + $systemProtectionState = Get-SystemProtectionState + Write-Verbose -Message ($script:localizedData.SystemProtectionState -f $systemProtectionState) + + if ($systemProtectionState -eq 'Present') + { + $enabledDrives = Get-SppRegistryValue + + if ($null -eq $enabledDrives) + { + $message = $script:localizedData.GetEnabledDrivesFailure + New-InvalidOperationException -Message $message + } + + foreach ($drive in $enabledDrives) + { + $currentDriveLetter = ConvertTo-DriveLetter -Drive $drive + if ($currentDriveLetter -eq $DriveLetter) + { + $maxPercent = Get-DiskUsageConfiguration -Drive $drive + Write-Verbose -Message ($script:localizedData.DriveFound -f $currentDriveLetter, $maxPercent) + break + } + else + { + Write-Verbose -Message ($script:localizedData.DriveSkipped -f $currentDriveLetter) + } + } + } + + $returnValue = @{ + Ensure = $systemProtectionState + DriveLetter = $currentDriveLetter + DiskUsage = $maxPercent + } + } + else + { + Write-Verbose -Message ($script:localizedData.FoundServerOS -f $productType) + Write-Warning -Message $script:localizedData.NotWorkstationOS + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the desired system protection state for a drive. + + .PARAMETER Ensure + Indicates whether system protection should be enabled or disabled. + + .PARAMETER DriveLetter + Specifies the drive letter to be configured. + + .PARAMETER DiskUsage + Specifies the maximum disk space to use for protection as a percentage. + + .PARAMETER Force + If a resize operation fails, force the deletion of all checkpoints. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidatePattern('^[A-Za-z]$')] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateRange(1,100)] + [System.Int32] + $DiskUsage, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -ne 1) + { + Write-Verbose -Message ($script:localizedData.FoundServerOS -f $productType) + $message = $script:localizedData.NotWorkstationOS + New-InvalidOperationException -Message $message + } + else + { + Write-Verbose -Message ($script:localizedData.FoundWorkstationOS -f $productType) + } + + switch ($Ensure) + { + 'Present' + { + try + { + Enable-ComputerRestore -Drive ($DriveLetter + ':') -ErrorAction Stop + } + catch + { + $message = ($script:localizedData.EnableComputerRestoreFailure -f $DriveLetter) + New-InvalidOperationException -Message $message + } + + Write-Verbose -Message ($script:localizedData.EnableComputerRestoreSuccess -f $DriveLetter) + + if ($PSBoundParameters.ContainsKey('DiskUsage')) + { + $process = Invoke-VssAdmin ` + -Operation Resize -Drive ($DriveLetter + ':') -DiskUsage $DiskUsage + + Write-Verbose -Message ($script:localizedData.VssAdminReturnValues -f $process.ExitCode, 'Resize', $Force) + + if ($process.ExitCode -ne 0 -and $Force -eq $true) + { + Write-Warning ` + -Message ($script:localizedData.VssShadowResizeFailureWithForce -f $DriveLetter) + + $process = Invoke-VssAdmin -Operation Delete -Drive ($DriveLetter + ':') + + Write-Verbose -Message ($script:localizedData.VssAdminReturnValues -f $process.ExitCode, 'Delete', $Force) + + if ($process.ExitCode -ne 0) + { + $message = ($script:localizedData.VssShadowDeleteFailure -f $DriveLetter) + New-InvalidOperationException -Message $message + } + else + { + $process = Invoke-VssAdmin ` + -Operation Resize -Drive ($DriveLetter + ':') -DiskUsage $DiskUsage + + Write-Verbose -Message ($script:localizedData.VssAdminReturnValues -f $process.ExitCode, 'Resize', $Force) + + if ($process.ExitCode -ne 0) + { + $message = ($script:localizedData.VssShadowResizeFailureWithForce2 -f $DriveLetter) + New-InvalidOperationException -Message $message + } + } + } + elseif ($process.ExitCode -ne 0) + { + Write-Verbose -Message ($script:localizedData.VssAdminReturnValues -f $process.ExitCode, 'Resize', $Force) + + $message = ($script:localizedData.VssShadowResizeFailure -f $DriveLetter) + New-InvalidOperationException -Message $message + } + else + { + Write-Verbose -Message ($script:localizedData.VssShadowResizeSuccess -f $DriveLetter) + } + } + } + + 'Absent' + { + try + { + Disable-ComputerRestore -Drive ($DriveLetter + ':') -ErrorAction Stop + } + catch + { + $message = ($script:localizedData.DisableComputerRestoreFailure -f $DriveLetter) + New-InvalidOperationException -Message $message + } + + Write-Verbose -Message ($script:localizedData.DisableComputerRestoreSuccess -f $DriveLetter) + } + } +} + +<# + .SYNOPSIS + Tests if the current drive protection state is the same as the desired state. + + .PARAMETER Ensure + Indicates whether system protection should be enabled or disabled. + + .PARAMETER DriveLetter + Specifies the drive letter to be tested. + + .PARAMETER DiskUsage + Specifies the maximum disk space to use for protection as a percentage. + + .PARAMETER Force + If a resize operation fails, force the deletion of all checkpoints. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [ValidatePattern('^[A-Za-z]$')] + [System.String] + $DriveLetter, + + [Parameter()] + [ValidateRange(1,100)] + [System.Int32] + $DiskUsage, + + [Parameter()] + [System.Boolean] + $Force = $false + ) + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -eq 1) + { + $enabledDrives = @() + $registryDrives = Get-SppRegistryValue + + foreach ($drive in $registryDrives) + { + $enabledDrives += ConvertTo-DriveLetter -Drive $drive + } + + $foundDrive = $false + $currentEnabledDrives = Get-SppRegistryValue + + foreach ($drive in $currentEnabledDrives) + { + $currentDriveLetter = ConvertTo-DriveLetter -Drive $drive + + if ($currentDriveLetter -eq $DriveLetter) + { + $foundDrive = $true + $maxPercent = Get-DiskUsageConfiguration -Drive $drive + Write-Verbose -Message ($script:localizedData.DriveFound -f $currentDriveLetter, $maxPercent) + break + } + else + { + Write-Verbose -Message ($script:localizedData.DriveSkipped -f $currentDriveLetter) + } + } + + if ($Ensure -eq 'Present') + { + $inDesiredState = $foundDrive + } + else + { + $inDesiredState = -not $foundDrive + } + + Write-Verbose -Message ($script:localizedData.InDesiredStateDriveLetter -f $currentDriveLetter) + + if ($PSBoundParameters.ContainsKey('DiskUsage') -and $foundDrive -and $DiskUsage -ne $maxPercent) + { + $inDesiredState = $false + Write-Verbose -Message ($script:localizedData.InDesiredStateDiskUsageFalse -f $currentDriveLetter) + } + else + { + Write-Verbose -Message ($script:localizedData.InDesiredStateDiskUsageUnchanged -f $currentDriveLetter) + } + } + else + { + Write-Warning -Message $script:localizedData.NotWorkstationOS + Write-Warning -Message $script:localizedData.ReturningTrueToBeSafe + $inDesiredState = $true + } + + return $inDesiredState +} + +<# + .SYNOPSIS + Converts an SPP registry entry into a drive letter. + + .PARAMETER Drive + Specifies the SPP query to parse. +#> +function ConvertTo-DriveLetter +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive + ) + + $driveLetter = $Drive | + Select-String -Pattern '\w%3A' | + Select-Object -ExpandProperty Matches | + Select-Object -ExpandProperty Value + + return $driveLetter -replace '%3A', '' +} + +<# + .SYNOPSIS + Calculates the maximum configured disk usage for a protected drive. + + .PARAMETER Drive + Specifies the SPP query to calculate the percentage from. +#> +function Get-DiskUsageConfiguration +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Drive + ) + + try + { + $vssStorage = Get-CimInstance -ClassName 'Win32_ShadowStorage' -ErrorAction Stop + } + catch + { + $message = $script:localizedData.UnknownOperatingSystemError + New-InvalidOperationException -Message $message + } + + $driveGuid = $Drive | + Select-String -Pattern '\\\\\?\\Volume{[-0-9A-F]+?}\\' | + Select-Object -ExpandProperty Matches | + Select-Object -ExpandProperty Value + + try + { + $volumeSize = (Get-Volume -UniqueId $driveGuid -ErrorAction Stop).Size + } + catch + { + $message = $script:localizedData.UnknownOperatingSystemError + New-InvalidOperationException -Message $message + } + + foreach ($instance in $vssStorage) + { + if ($driveGuid -eq $instance.Volume.DeviceID) + { + $maxPercent = [int]($instance.MaxSpace / $volumeSize * 100) + break + } + } + + return $maxPercent +} + +<# + .SYNOPSIS + Gets the contents of the SPP registry key. +#> +function Get-SppRegistryValue +{ + $sppRegistryKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\SPP\Clients' + $sppRegistryName = '{09F7EDC5-294E-4180-AF6A-FB0E6A0E9513}' + + if (Get-ItemProperty -Path $sppRegistryKey -Name $sppRegistryName -ErrorAction SilentlyContinue) + { + $enabledDrives = Get-ItemPropertyValue ` + -Path $sppRegistryKey ` + -Name $sppRegistryName ` + -ErrorAction SilentlyContinue + } + + return $enabledDrives +} + +<# + .SYNOPSIS + Gets the overall system protection state. +#> +function Get-SystemProtectionState +{ + try + { + $state = Get-CimInstance -ClassName 'SystemRestoreConfig' -Namespace 'root\DEFAULT' -ErrorAction Stop + } + catch + { + $message = $script:localizedData.UnknownOperatingSystemError + New-InvalidOperationException -Message $message + } + + if ($state.RPSessionInterval -eq 1) + { + return 'Present' + } + else + { + return 'Absent' + } +} + +<# + .SYNOPSIS + Invokes vssadmin to change the maximum disk usage. + + .PARAMETER Operation + Specifies what VSS operation to execute. + + .PARAMETER Drive + Specifies the drive letter to be configured. + + .PARAMETER DiskUsage + Specifies the maximum disk space to use for protection as a percentage. +#> +function Invoke-VssAdmin +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Resize', 'Delete')] + [System.String] + $Operation, + + [Parameter(Mandatory = $true)] + [System.String] + $Drive, + + [Parameter()] + [System.Int32] + $DiskUsage + ) + + $ErrorActionPreference = 'Stop' + $command = "$env:SystemRoot\System32\vssadmin.exe" + $resizeArguments = "Resize ShadowStorage /For=$Drive /On=$Drive /MaxSize=$($DiskUsage)%" + $deleteArguments = "Delete Shadows /For=$Drive /quiet" + + switch ($Operation) + { + 'Resize' + { + $arguments = $resizeArguments + } + + 'Delete' + { + $arguments = $deleteArguments + } + } + + $process = New-Object -TypeName System.Diagnostics.ProcessStartInfo + $process.FileName = $command + $process.RedirectStandardError = $true + $process.RedirectStandardOutput = $true + $process.UseShellExecute = $false + $process.WindowStyle = 'Hidden' + $process.CreateNoWindow = $true + $process.Arguments = $arguments + + $result = Start-VssAdminProcess -Process $process + + return $result +} + +<# + .SYNOPSIS + Starts the vsssadmin process + + .PARAMETER Process + Specifies everything neceded to run vssadmin. +#> +function Start-VssAdminProcess +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.Diagnostics.ProcessStartInfo] + $Process + ) + + $p = New-Object -TypeName System.Diagnostics.Process + $p.StartInfo = $process + + $p.Start() | Out-Null + + $result = @{ + Command = $command + Arguments = $arguments + StdOut = $p.StandardOutput.ReadToEnd() + StdErr = $p.StandardError.ReadToEnd() + ExitCode = $p.ExitCode + } + + $p.WaitForExit() + + return $result +} + +Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.schema.mof b/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.schema.mof new file mode 100644 index 00000000..7d3ed80a --- /dev/null +++ b/source/DSCResources/DSC_SystemProtection/DSC_SystemProtection.schema.mof @@ -0,0 +1,8 @@ +[ClassVersion("1.0.0.0"), FriendlyName("SystemProtection")] +class DSC_SystemProtection : OMI_BaseResource +{ + [Required, Description("Indicates that the computer restore is enabled or is disabled."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Key, Description("Specifies the drive letter to enable or disable protection on.")] String DriveLetter; + [Write, Description("Specifies the maximum disk space to use for protection as a percentage.")] Sint32 DiskUsage; + [Write, Description("Forces desired state to be applied regardless of data loss. Defaults to False.")] Boolean Force; +}; diff --git a/source/DSCResources/DSC_SystemProtection/README.md b/source/DSCResources/DSC_SystemProtection/README.md new file mode 100644 index 00000000..611ca415 --- /dev/null +++ b/source/DSCResources/DSC_SystemProtection/README.md @@ -0,0 +1,22 @@ +# Description + +This resource is used configure System Protection. System +Protection is only applicable to workstation operating +systems. Server operating systems are not supported. + +## DiskUsage and Force Parameters + +The amount of disk that can be allocated for System Protection +is configurable on a per-drive basis which is why this +resource doesn't accept an array of drives like xWindowsRestore +did. + +If you reduce the disk usage for a protected drive, the resource +will try to resize it but VSS could throw an error because you +have to delete checkpoints first. When you set Force to $true, +SystemProtection will attempt the resize and if VSS throws an +error, SystemProtection will delete **all** checkpoints on the +the protected drive and try the resize operation again. + +Make sure you fully understand and accept the risks associated +with using the Force parameter. diff --git a/source/DSCResources/DSC_SystemProtection/en-US/DSC_SystemProtection.strings.psd1 b/source/DSCResources/DSC_SystemProtection/en-US/DSC_SystemProtection.strings.psd1 new file mode 100644 index 00000000..1488c7f5 --- /dev/null +++ b/source/DSCResources/DSC_SystemProtection/en-US/DSC_SystemProtection.strings.psd1 @@ -0,0 +1,25 @@ +# Culture = "en-US" +ConvertFrom-StringData -StringData @' + NotWorkstationOS = This resource can only be used on workstation operating systems. (SP0001) + FoundServerOS = It appears we are running on a server operating system (ProductType = {0}). (SP0002) + FoundWorkstationOS = It appears we are running on a worksstation operating system (ProductType = {0}). (SP0003) + ReturningTrueToBeSafe = The test will evaluate to True to prevent an unintentional set targeting a server operating system. (SP0004) + SystemProtectionState = Overall system protection state is {0}. (SP0005) + UnknownOperatingSystemError = The operating system threw an error trying to retrieve system protection settings. (SP0006) + GetEnabledDrivesFailure = An error occurred trying to retrieve the list of drives where system protection is enabled. (SP0007) + DriveFound = Drive {0} has system protection enabled with maximum disk usage of {1} percent. (SP0008) + DriveSkipped = Skipping drive {0} because it is not the drive to find (SP0009) + VssAdminReturnValues = {1} Operation: VSS returned exit code {0} (Force = {2}). (SP0010) + InDesiredStateDriveLetter = Set inDesiredState to {0} based on drive letter evaluation. (SP0011) + EnableComputerRestoreFailure = An unexpected error occurred trying to enable system protection on drive {0}. (SP0012) + DisableComputerRestoreFailure = An unexpected error occurred trying to disable system protection on drive {0}. (SP0013) + InDesiredStateDiskUsageFalse = Set inDesiredState to false because the current maximum disk usage for drive {0} is not in the desired state. (SP0014) + InDesiredStateDiskUsageUnchanged = Not changing inDesiredState because the current maximum disk usage for drive {0} is in the desired state. (SP0015) + EnableComputerRestoreSuccess = System protection for drive {0} has been enabled successfully. (SP0016) + DisableComputerRestoreSuccess = System protection for drive {0} has been disabled successfully. (SP0017) + VssShadowResizeSuccess = System protection disk usage was changed successfully. (SP0018) + VssShadowResizeFailure = An unexpected error occurred trying to configure the maximum disk usage for drive {0}. (SP0019) + VssShadowDeleteFailure = An unexpected error occurred tying to delete restore points for drive {0}. VSS corruption and/or orphaned shadow copies are the most likely reasons for the failure. (SP0020) + VssShadowResizeFailureWithForce = Deleting restore points for drive {0} because the resize operation failed and the Force option was specified. (SP0021) + VssShadowResizeFailureWithForce2 = An unexpected error occurred trying to configure the maximum disk usage for drive {0} even after deleting its restore points. VSS corruption and/or orphaned shadow copies are the most likely reasons for the failure. (SP0022) +'@ diff --git a/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.psm1 b/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.psm1 new file mode 100644 index 00000000..269a4aa5 --- /dev/null +++ b/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.psm1 @@ -0,0 +1,335 @@ +$modulePath = Join-Path -Path (Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent) -ChildPath 'Modules' + +# Import the ComputerManagementDsc Common Modules +Import-Module -Name (Join-Path -Path $modulePath ` + -ChildPath (Join-Path -Path 'ComputerManagementDsc.Common' ` + -ChildPath 'ComputerManagementDsc.Common.psm1')) + +Import-Module -Name (Join-Path -Path $modulePath -ChildPath 'DscResource.Common') + +# Import Localization Strings +$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' + +<# + .SYNOPSIS + Gets the requested restore point. + + .PARAMETER Ensure + Indicates whether a restore point should be created or deleted. + + .PARAMETER Description + Specifies the description of the restore point. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [System.String] + $Description + ) + + $returnValue = @{ + Ensure = 'Absent' + } + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -eq 1) + { + $latestRestorePoint = Get-ComputerRestorePoint | Select-Object -Last 1 + + if ($Description -eq $latestRestorePoint.Description) + { + $returnValue.Ensure = 'Present' + $returnValue.Description = $latestRestorePoint.Description + $returnValue.RestorePointType = ConvertTo-RestorePointName -Type $latestRestorePoint.RestorePointType + } + else + { + Write-Verbose -Message $script:localizedData.NoRestorePointsFound + } + } + else + { + Write-Warning -Message $script:localizedData.NotWorkstationOS + } + + return $returnValue +} + +<# + .SYNOPSIS + Sets the desired state of a restore point. + + .PARAMETER Ensure + Indicates whether a restore point should be created or deleted. + + .PARAMETER Description + Specifies the description of the restore point. + + .PARAMETER RestorePointType + Specifies the restore point type to act upon. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [System.String] + $Description, + + [Parameter()] + [ValidateSet( + 'APPLICATION_INSTALL', + 'APPLICATION_UNINSTALL', + 'DEVICE_DRIVER_INSTALL', + 'MODIFY_SETTINGS', + 'CANCELLED_OPERATION' + )] + [System.String] + $RestorePointType = 'APPLICATION_INSTALL' + ) + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -ne 1) + { + $message = $script:localizedData.NotWorkstationOS + New-InvalidOperationException -Message $message + } + + switch ($Ensure) + { + 'Present' + { + Write-Verbose -Message ($script:localizedData.CreateRestorePoint -f $Description) + + try + { + Checkpoint-Computer -Description $Description -RestorePointType $RestorePointType + } + catch + { + $message = $script:localizedData.CheckpointFailure + New-InvalidOperationException -Message $message + } + } + + 'Absent' + { + $assemblies = [AppDomain]::CurrentDomain.GetAssemblies() + $assembly = $assemblies | + ForEach-Object -Process { $PSItem.GetTypes() } | + Where-Object -Property FullName -EQ 'SystemRestore.DeleteRestorePoint' + + if ($null -eq $assembly) + { + $definition = '[DllImport ("srclient.dll")]public static extern int SRRemoveRestorePoint (int index);' + Add-Type -MemberDefinition $definition ` + -Name DeleteRestorePoint -NameSpace SystemRestore -PassThru | Out-Null + } + + $type = ConvertTo-RestorePointValue -Type $RestorePointType + + $restorePoints = Get-ComputerRestorePoint | + Where-Object -Property Description -EQ $Description | + Where-Object -Property RestorePointType -EQ $type + + if ($null -eq $restorePoints) + { + Write-Verbose -Message ($script:localizedData.NumRestorePoints -f '0') + } + else + { + $count = 0 + Write-Verbose -Message ($script:localizedData.NumRestorePoints -f $restorePoints.Count) + + foreach ($restorePoint in $restorePoints) + { + Write-Verbose -Message ($script:localizedData.DeleteRestorePoint -f $count++, $restorePoints.Count) + + $success = Remove-RestorePoint -RestorePoint $restorePoint + + if (-not $success) + { + $message = $script:localizedData.DeleteCheckpointFailure + New-InvalidOperationException -Message $message + } + } + } + } + } +} + +<# + .SYNOPSIS + Tests if the current state is the same as the desired state. + + .PARAMETER Ensure + Indicates whether a restore point should be present or absent. + + .PARAMETER Description + Specifies the description to be tested. + + .PARAMETER RestorePointType + Specifies the restore point type to tested. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter(Mandatory = $true)] + [System.String] + $Description, + + [Parameter()] + [ValidateSet( + 'APPLICATION_INSTALL', + 'APPLICATION_UNINSTALL', + 'DEVICE_DRIVER_INSTALL', + 'MODIFY_SETTINGS', + 'CANCELLED_OPERATION' + )] + [System.String] + $RestorePointType = 'APPLICATION_INSTALL' + ) + + $productType = (Get-CimInstance -ClassName Win32_OperatingSystem).ProductType + + if ($productType -eq 1) + { + $restorePoint = Get-TargetResource -Ensure 'Present' -Description $Description + + Write-Verbose ` + -Message ($script:localizedData.RestorePointProperties -f $restorePoint.Ensure, $restorePoint.RestorePointType) + + if ($Ensure -eq $restorePoint.Ensure -and $RestorePointType -eq $restorePoint.RestorePointType) + { + $inDesiredState = $true + } + else + { + $inDesiredState = $false + } + } + else + { + Write-Warning -Message $script:localizedData.NotWorkstationOS + Write-Warning -Message $script:localizedData.ReturningTrueToBeSafe + $inDesiredState = $true + } + + return $inDesiredState +} + +<# + .SYNOPSIS + Converts the name of a restore point to a numerical value. + + .PARAMETER Type + Specifies the type of restore point to convert. +#> +function ConvertTo-RestorePointValue +{ + [CmdletBinding()] + [OutputType([System.Int32])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Type + ) + + switch ($Type) + { + 'APPLICATION_INSTALL' { $value = 0 } + 'APPLICATION_UNINSTALL' { $value = 1 } + 'DEVICE_DRIVER_INSTALL' { $value = 10 } + 'MODIFY_SETTINGS' { $value = 12 } + 'CANCELLED_OPERATION' { $value = 13 } + } + + return $value +} + +<# + .SYNOPSIS + Converts the numerical value of a restore point to a name. + + .PARAMETER Type + Specifies the type of restore point to convert. +#> +function ConvertTo-RestorePointName +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $Type + ) + + switch ($Type) + { + 0 { $value = 'APPLICATION_INSTALL' } + 1 { $value = 'APPLICATION_UNINSTALL' } + 10 { $value = 'DEVICE_DRIVER_INSTALL' } + 12 { $value = 'MODIFY_SETTINGS' } + 13 { $value = 'CANCELLED_OPERATION' } + } + + return $value +} + +<# + .SYNOPSIS + Deletes a restore point. + + .PARAMETER RestorePoint + Specifies the restore point to delete. +#> +function Remove-RestorePoint +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + $RestorePoint + ) + + $result = [SystemRestore.DeleteRestorePoint]::SRRemoveRestorePoint($RestorePoint.SequenceNumber) + + if ($result -eq 0) + { + return $true + } + else + { + return $false + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.schema.mof b/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.schema.mof new file mode 100644 index 00000000..730c32a1 --- /dev/null +++ b/source/DSCResources/DSC_SystemRestorePoint/DSC_SystemRestorePoint.schema.mof @@ -0,0 +1,7 @@ +[ClassVersion("1.0.0.0"), FriendlyName("SystemRestorePoint")] +class DSC_SystemRestorePoint : OMI_BaseResource +{ + [Required, Description("Indicates that the computer restore is enabled or is disabled."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; + [Key, Description("Specifies a descriptive name for the restore point.")] String Description; + [Write, Description("Specifies the restore point type. Defaults to APPLICATION_INSTALL."), ValueMap{"APPLICATION_INSTALL","APPLICATION_UNINSTALL","DEVICE_DRIVER_INSTALL","MODIFY_SETTINGS","CANCELLED_OPERATION"}, Values{"APPLICATION_INSTALL","APPLICATION_UNINSTALL","DEVICE_DRIVER_INSTALL","MODIFY_SETTINGS","CANCELLED_OPERATION"}] String RestorePointType; +}; diff --git a/source/DSCResources/DSC_SystemRestorePoint/README.md b/source/DSCResources/DSC_SystemRestorePoint/README.md new file mode 100644 index 00000000..09264844 --- /dev/null +++ b/source/DSCResources/DSC_SystemRestorePoint/README.md @@ -0,0 +1,8 @@ +# Description + +This resource is used to create and delete restore points. +System Protection must be enabled on at least one drive for +this module to work. + +System restore points are only applicable to workstation +operating systems. Server operating systems are not supported. diff --git a/source/DSCResources/DSC_SystemRestorePoint/en-US/DSC_SystemRestorePoint.strings.psd1 b/source/DSCResources/DSC_SystemRestorePoint/en-US/DSC_SystemRestorePoint.strings.psd1 new file mode 100644 index 00000000..eb29b24b --- /dev/null +++ b/source/DSCResources/DSC_SystemRestorePoint/en-US/DSC_SystemRestorePoint.strings.psd1 @@ -0,0 +1,12 @@ +# Culture = "en-US" +ConvertFrom-StringData -StringData @' + NotWorkstationOS = This resource can only be used on workstation operating systems. (SR0001) + ReturningTrueToBeSafe = The test will evaluate to True to prevent an unintentional set targeting a server operating system. (SP0002) + NoRestorePointsFound = No checkpoints have been created on the computer. (SR0003) + CreateRestorePoint = Creating restore point on the target computer. Description = [{0}]. (SR0004) + NumRestorePoints = Found {0} restore points that match the parameters provided. (SR0005) + CheckpointFailure = An error occurred trying to create the restore point. (SR0006) + DeleteRestorePoint = Deleting restore point ({0}/{1}). (SR0007) + DeleteCheckpointFailure = An error occurred trying to delete the restore point. (SR0008) + RestorePointProperties = Retrieved restore point with Ensure = [{0}] and restore point type = [{1}]. (SR0009) +'@ diff --git a/source/Examples/Resources/SystemProtection/1-SystemProtection_EnableDriveC_Config.ps1 b/source/Examples/Resources/SystemProtection/1-SystemProtection_EnableDriveC_Config.ps1 new file mode 100644 index 00000000..c802c474 --- /dev/null +++ b/source/Examples/Resources/SystemProtection/1-SystemProtection_EnableDriveC_Config.ps1 @@ -0,0 +1,37 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 4f0d0a70-30e3-4f16-86f3-76631587bdd0 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Enables system protection for the C drive using the + default value of 10 percent disk usage. +#> +Configuration SystemProtection_EnableDriveC_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemProtection DriveC + { + Ensure = 'Present' + DriveLetter = 'C' + } + } +} diff --git a/source/Examples/Resources/SystemProtection/2-SystemProtection_EnableDriveC_5Percent_Config.ps1 b/source/Examples/Resources/SystemProtection/2-SystemProtection_EnableDriveC_5Percent_Config.ps1 new file mode 100644 index 00000000..64cd97a9 --- /dev/null +++ b/source/Examples/Resources/SystemProtection/2-SystemProtection_EnableDriveC_5Percent_Config.ps1 @@ -0,0 +1,38 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 6a49b259-754d-4825-b559-31029a10f5d7 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Enables system protection for the C drive and sets + the maximum restore point disk usage to 5 percent. +#> +Configuration SystemProtection_EnableDriveC_5Percent_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemProtection DriveC + { + Ensure = 'Present' + DriveLetter = 'C' + DiskUsage = 5 + } + } +} diff --git a/source/Examples/Resources/SystemProtection/3-SystemProtection_DisableDriveF_Config.ps1 b/source/Examples/Resources/SystemProtection/3-SystemProtection_DisableDriveF_Config.ps1 new file mode 100644 index 00000000..a7392004 --- /dev/null +++ b/source/Examples/Resources/SystemProtection/3-SystemProtection_DisableDriveF_Config.ps1 @@ -0,0 +1,36 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID fbe5e8dd-1327-4d1a-aa08-06b2063b960e +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Disables system protection for the F drive. +#> +Configuration SystemProtection_DisableDriveF_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemProtection DriveF + { + Ensure = 'Absent' + DriveLetter = 'F' + } + } +} diff --git a/source/Examples/Resources/SystemProtection/4-SystemProtection_ReduceDriveCDiskUsage_Config.ps1 b/source/Examples/Resources/SystemProtection/4-SystemProtection_ReduceDriveCDiskUsage_Config.ps1 new file mode 100644 index 00000000..18ca730e --- /dev/null +++ b/source/Examples/Resources/SystemProtection/4-SystemProtection_ReduceDriveCDiskUsage_Config.ps1 @@ -0,0 +1,40 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID f0e26404-16fd-407c-832a-e69b30ec43b0 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Sets the maximum disk usage for Drive C to 15 percent. + Assumes the current disk usage is configured for a + higher percentage and you want to delete checkpoints. +#> +Configuration SystemProtection_ReduceDriveCDiskUsage_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemProtection DriveC + { + Ensure = 'Present' + DriveLetter = 'C' + DiskUsage = 15 + Force = $true + } + } +} diff --git a/source/Examples/Resources/SystemProtection/5-SystemProtection_MultiDrive_Config.ps1 b/source/Examples/Resources/SystemProtection/5-SystemProtection_MultiDrive_Config.ps1 new file mode 100644 index 00000000..13328641 --- /dev/null +++ b/source/Examples/Resources/SystemProtection/5-SystemProtection_MultiDrive_Config.ps1 @@ -0,0 +1,46 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID ef7f184e-1b0f-4eba-91a8-2aafe209b25c +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Enables system protection for the C drive using the + default value of 10 percent disk usage and the D + drive with 25 percent disk usage. +#> +Configuration SystemProtection_MultiDrive_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemProtection DriveC + { + Ensure = 'Present' + DriveLetter = 'C' + DiskUsage = 15 + } + + SystemProtection DriveD + { + Ensure = 'Present' + DriveLetter = 'D' + DiskUsage = 25 + } + } +} diff --git a/source/Examples/Resources/SystemRestorePoint/1-SystemRestorePoint_CreateModifySettings_Config.ps1 b/source/Examples/Resources/SystemRestorePoint/1-SystemRestorePoint_CreateModifySettings_Config.ps1 new file mode 100644 index 00000000..b8bdafed --- /dev/null +++ b/source/Examples/Resources/SystemRestorePoint/1-SystemRestorePoint_CreateModifySettings_Config.ps1 @@ -0,0 +1,37 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID c3eab687-2f94-4321-b985-e0c128676bfe +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Creates a system restore point. +#> +Configuration SystemRestorePoint_CreateModifySettings_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemRestorePoint ModifySettings + { + Ensure = 'Present' + Description = 'Modify system settings' + RestorePointType = 'MODIFY_SETTINGS' + } + } +} diff --git a/source/Examples/Resources/SystemRestorePoint/2-SystemRestorePoint_DeleteApplicationInstalls_Config.ps1 b/source/Examples/Resources/SystemRestorePoint/2-SystemRestorePoint_DeleteApplicationInstalls_Config.ps1 new file mode 100644 index 00000000..4bffc966 --- /dev/null +++ b/source/Examples/Resources/SystemRestorePoint/2-SystemRestorePoint_DeleteApplicationInstalls_Config.ps1 @@ -0,0 +1,38 @@ +<#PSScriptInfo +.VERSION 1.0.0 +.GUID 7f114f0d-9a93-427d-a81f-2fd991fd4c65 +.AUTHOR DSC Community +.COMPANYNAME DSC Community +.COPYRIGHT Copyright the DSC Community contributors. All rights reserved. +.TAGS DSCConfiguration +.LICENSEURI https://github.com/dsccommunity/ComputerManagementDsc/blob/master/LICENSE +.PROJECTURI https://github.com/dsccommunity/ComputerManagementDsc +.ICONURI +.EXTERNALMODULEDEPENDENCIES +.REQUIREDSCRIPTS +.EXTERNALSCRIPTDEPENDENCIES +.RELEASENOTES First version. +.PRIVATEDATA 2016-Datacenter,2016-Datacenter-Server-Core +#> + +#Requires -module ComputerManagementDsc + +<# + .DESCRIPTION + Deletes all restore points matching the description + and the APPLICATION_INSTALL restore point type. +#> +Configuration SystemRestorePoint_DeleteApplicationInstalls_Config +{ + Import-DSCResource -ModuleName ComputerManagementDsc + + Node localhost + { + SystemRestorePoint DeleteTestApplicationinstalls + { + Ensure = 'Absent' + Description = 'Test Restore Point' + RestorePointType = 'APPLICATION_INSTALL' + } + } +} diff --git a/source/en-US/about_ComputerManagementDsc.help.txt b/source/en-US/about_ComputerManagementDsc.help.txt index 71a4fcbc..4d909430 100644 --- a/source/en-US/about_ComputerManagementDsc.help.txt +++ b/source/en-US/about_ComputerManagementDsc.help.txt @@ -31,4 +31,5 @@ SEE ALSO KEYWORDS DSC, DscResource, Computer, OfflineDomainJoin, PendingReboot, PowerPlan, PowerShellExecutionPolicy, RemoteDesktopAdmin, ScheduledTask, SmbServerConfiguration - SmbShare, SystemLocale, TimeZone, VirtualMemory, WindowsEventLog, WindowsCapability + SmbShare, SystemLocale, SystemProtection, SystemRestorePoint, TimeZone, + VirtualMemory, WindowsEventLog, WindowsCapability diff --git a/tests/Unit/DSC_SystemProtection.Tests.ps1 b/tests/Unit/DSC_SystemProtection.Tests.ps1 new file mode 100644 index 00000000..889cb72b --- /dev/null +++ b/tests/Unit/DSC_SystemProtection.Tests.ps1 @@ -0,0 +1,473 @@ +$script:dscModuleName = 'ComputerManagementDsc' +$script:dscResourceName = 'DSC_SystemProtection' + +function Invoke-TestSetup +{ + try + { + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} + +Invoke-TestSetup + +# Begin Testing +try +{ + InModuleScope $script:dscResourceName { + $fullDriveParams = @{ + Ensure = 'Present' + DriveLetter = 'P' + DiskUsage = 5 + } + + $partialDriveParams = @{ + Ensure = 'Present' + DriveLetter = 'P' + } + + $workstationMock = @{ + ParameterFilter = $([scriptblock]::Create('$ClassName -eq ''Win32_OperatingSystem''')) + MockWith = $([scriptblock]::Create('@{ ProductType = 1 }')) + } + + $serverMock = @{ + ParameterFilter = $([scriptblock]::Create('$ClassName -eq ''Win32_OperatingSystem''')) + MockWith = $([scriptblock]::Create('@{ ProductType = 3 }')) + } + + $mocks = @{ + MaxPercentValueDriveP = 5 + SystemProtectionStateEnabled = 'Present' + SanitizedDriveGuid = '\\?\Volume{f3f152fa-a383-4fb9-a823-fb7e1bfd1db1}\' + SppRegistryValueDriveP = '\\?\Volume{f3f152fa-a383-4fb9-a823-fb7e1bfd1db1}\:(P%3A)' + + SppRegistryItemDriveP = @{ + {09F7EDC5-294E-4180-AF6A-FB0E6A0E9513} = '\\?\Volume{f3f152fa-a383-4fb9-a823-fb7e1bfd1db1}\:(P%3A)' + } + + VssAdminFailure = @{ + ExitCode = 2 + } + + VssAdminSuccess = @{ + ExitCode = 0 + } + + GetVolumeSize = @{ + Size = 200GB + } + + GetCimInstanceestoreEnabled = @{ + RPSessionInterval = 1 + } + + GetCimInstanceestoreDisabled = @{ + RPSessionInterval = 0 + } + + GetCimInstanceShadowStorage = @{ + AllocatedSpace = [System.Uint64]40GB + DiffVolume = @{ + DeviceID = '\\?\Volume{f3f152fa-a383-4fb9-a823-fb7e1bfd1db1}\' + } + MaxSpace = [System.UInt64]45GB + UsedSpace = [System.UInt64]35GB + Volume = @{ + DeviceID = '\\?\Volume{f3f152fa-a383-4fb9-a823-fb7e1bfd1db1}\' + } + } + } + + Describe "DSC_SystemProtection\Get-TargetResource" -Tag 'Get' { + Context 'When running on a workstation OS' { + Context 'When getting the target resource' { + It 'Should throw when the system is corrupt' { + $errorRecord = Get-InvalidOperationRecord -Message $script:localizedData.GetEnabledDrivesFailure + + Mock -CommandName Get-SystemProtectionState -MockWith { return $mocks.SystemProtectionStateEnabled } + Mock -CommandName Get-SppRegistryValue -MockWith { return $null } + Mock -CommandName Get-CimInstance @workstationMock + + { Get-TargetResource -Ensure 'Present' -DriveLetter 'P' } | Should -Throw $errorRecord + } + } + + Context 'When getting the target resource with Drive P protected' { + It 'Should get the system protection settings' { + Mock -CommandName Get-SystemProtectionState -MockWith { return $mocks.SystemProtectionStateEnabled } + Mock -CommandName Get-SppRegistryValue -MockWith { return $mocks.SppRegistryValueDriveP } + Mock -CommandName Get-DiskUsageConfiguration -MockWith { return $mocks.MaxPercentValueDriveP } + Mock -CommandName Get-CimInstance @workstationMock + + $protectionSettings = Get-TargetResource -Ensure 'Present' -DriveLetter 'P' + + $protectionSettings | Should -BeOfType Hashtable + $protectionSettings.Ensure | Should -Be 'Present' + $protectionSettings.DriveLetter | Should -Be 'P' + $protectionSettings.DiskUsage | Should -Be 5 + } + } + } + + Context 'When running on a server OS' { + Context 'When getting the target resource' { + It 'Should return Absent and write a warning on a server operating system' { + Mock -CommandName Write-Warning + Mock -CommandName Get-CimInstance @serverMock + + $protectionSettings = Get-TargetResource -Ensure 'Present' -DriveLetter 'C' + + $protectionSettings.Ensure | Should -Be 'Absent' + Assert-MockCalled -CommandName Write-Warning -Times 1 + } + } + } + } + + Describe "DSC_SystemProtection\Test-TargetResource" -Tag 'Test' { + Context 'When running on a workstation OS' { + Context 'When testing the target resource' { + It 'Should throw when Ensure is neither Present nor Absent' { + { Test-TargetResource -Ensure 'Purgatory' } | Should -Throw + } + + It 'Should throw when DriveLetter is invalid' { + { Test-TargetResource -Ensure 'Present' -DriveLetter '5' } | Should -Throw + { Test-TargetResource -Ensure 'Present' -DriveLetter 'CD' } | Should -Throw + } + + It 'Should throw when DiskUsage is less than 1' { + { Test-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 0 } | Should -Throw + } + + It 'Should throw when DiskUsage is greater than 100' { + { Test-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 101 } | Should -Throw + } + } + + Context 'When system protection is in the desired state' { + Mock -CommandName Get-SppRegistryValue -MockWith { return $mocks.SppRegistryValueDriveP } + Mock -CommandName Get-DiskUsageConfiguration -MockWith { return $mocks.MaxPercentValueDriveP } + Mock -CommandName Get-CimInstance @workstationMock + + It 'Should return true when only the drive letter is specified' { + $result = Test-TargetResource @partialDriveParams + $result | Should -BeTrue + } + + It 'Should return true when drive letter and disk usage are specified' { + $result = Test-TargetResource @fullDriveParams + $result | Should -BeTrue + } + } + + Context 'When system protection is not in the desired state' { + Mock -CommandName Get-SppRegistryValue -MockWith { return $mocks.SppRegistryValueDriveP } + Mock -CommandName Get-DiskUsageConfiguration -MockWith { return $mocks.MaxPercentValueDriveP } + Mock -CommandName Get-CimInstance @workstationMock + + It 'Should return false when drive protection changes are necessary' { + $result = Test-TargetResource -Ensure 'Absent' -DriveLetter 'P' + + $result | Should -BeFalse + } + + It 'Should return false when disk usage changes are necessary' { + $result = Test-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 25 + + $result | Should -BeFalse + } + } + } + + Context 'When running on a server OS' { + It 'Should return Absent and write warnings on a server operating system' { + Mock -CommandName Write-Warning + Mock -CommandName Get-CimInstance @serverMock + + $desiredState = Test-TargetResource -Ensure 'Present' -DriveLetter 'C' + + $desiredState | Should -BeTrue + Assert-MockCalled -CommandName Write-Warning -Times 2 + } + } + } + + Describe "DSC_SystemProtection\Set-TargetResource" -Tag 'Set' { + Context 'When running on a workstation OS' { + Context 'When setting the target resource' { + It 'Should throw when Ensure is neither Present nor Absent' { + { Set-TargetResource -Ensure 'Purgatory' } | Should -Throw + } + + It 'Should throw when DriveLetter is invalid' { + { Set-TargetResource -Ensure 'Present' -DriveLetter '5' } | Should -Throw + { Set-TargetResource -Ensure 'Present' -DriveLetter 'CD' } | Should -Throw + } + + It 'Should throw when DiskUsage is less than 1' { + { Set-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 0 } | Should -Throw + } + + It 'Should throw when DiskUsage is greater than 100' { + { Set-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 110 } | Should -Throw + } + + It 'Should throw when the operating system cannot enable system protection' { + $errorRecord = Get-InvalidOperationRecord ` + -Message ($script:localizedData.EnableComputerRestoreFailure -f 'C') + + Mock -CommandName Enable-ComputerRestore -MockWith { throw } + Mock -CommandName Get-CimInstance @workstationMock + + { Set-TargetResource -Ensure 'Present' -DriveLetter 'C' } | Should -Throw $errorRecord + } + + It 'Should throw when the operating system cannot disable system protection' { + $errorRecord = Get-InvalidOperationRecord ` + -Message ($script:localizedData.DisableComputerRestoreFailure -f 'C') + + Mock -CommandName Disable-ComputerRestore -MockWith { throw } + Mock -CommandName Get-CimInstance @workstationMock + + { Set-TargetResource -Ensure 'Absent' -DriveLetter 'C' } | Should -Throw $errorRecord + } + } + + Context 'When configuration is required' { + Mock -CommandName Get-CimInstance @workstationMock + + It 'Should enable system protection for Drive P' { + Mock -CommandName Enable-ComputerRestore + + Set-TargetResource -Ensure 'Present' -DriveLetter 'P' + + Assert-MockCalled -CommandName Enable-ComputerRestore -Times 1 + } + + It 'Should disable system protection for Drive P' { + Mock -CommandName Disable-ComputerRestore + + Set-TargetResource -Ensure 'Absent' -DriveLetter 'P' + + Assert-MockCalled -CommandName Disable-ComputerRestore + } + + It 'Should enable set maximum disk usage to 20%' { + Mock -CommandName Enable-ComputerRestore + Mock -CommandName Invoke-VssAdmin -MockWith { return $mocks.VssAdminSuccess } + + Set-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 20 + + Assert-MockCalled -CommandName Enable-ComputerRestore -Times 1 + Assert-MockCalled -CommandName Invoke-VssAdmin -Times 1 + } + + It 'Should throw if the attempt to reisze fails without the Force option' { + $errorRecord = Get-InvalidOperationRecord ` + -Message ($script:localizedData.VssShadowResizeFailure -f 'P') + + Mock -CommandName Enable-ComputerRestore + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminFailure } ` + -ParameterFilter { $Operation -eq 'Resize' } + + { Set-TargetResource ` + -Ensure 'Present' -DriveLetter 'P' -DiskUsage 1 } | Should -Throw $errorRecord + } + + It 'Should delete restore points on disk usage reduction with Force option' { + $script:wasCalledPreviously = $false + + Mock -CommandName Enable-ComputerRestore + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminSuccess } ` + -ParameterFilter { $Operation -eq 'Delete' } + Mock -CommandName Invoke-VssAdmin ` + -ParameterFilter { $Operation -eq 'Resize' } ` + -MockWith { + if ($script:wasCalledPreviously) + { + $mocks.VssAdminSuccess + } + else + { + $script:wasCalledPreviously = $true + $mocks.VssAdminFailure + } + } + + Set-TargetResource -Ensure 'Present' -DriveLetter 'P' -DiskUsage 1 -Force $true + + Assert-MockCalled -CommandName Enable-ComputerRestore -Times 1 + Assert-MockCalled -CommandName Invoke-VssAdmin ` + -ParameterFilter { $Operation -eq 'Resize' } -Times 2 + Assert-MockCalled -CommandName Invoke-VssAdmin ` + -ParameterFilter { $Operation -eq 'Delete' } -Times 1 + } + + It 'Should throw if the attempt to delete restore points fails' { + $errorRecord = Get-InvalidOperationRecord ` + -Message ($script:localizedData.VssShadowDeleteFailure -f 'P') + + Mock -CommandName Enable-ComputerRestore + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminFailure } ` + -ParameterFilter { $Operation -eq 'Resize' } + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminFailure } ` + -ParameterFilter { $Operation -eq 'Delete' } + + { Set-TargetResource ` + -Ensure 'Present' -DriveLetter 'P' -DiskUsage 1 -Force $true } | Should -Throw $errorRecord + } + + It 'Should throw if all attempts to resize fails with Force option' { + $errorRecord = Get-InvalidOperationRecord ` + -Message ($script:localizedData.VssShadowResizeFailureWithForce2 -f 'P') + + Mock -CommandName Enable-ComputerRestore + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminFailure } ` + -ParameterFilter { $Operation -eq 'Resize' } + Mock -CommandName Invoke-VssAdmin ` + -MockWith { $mocks.VssAdminSuccess } ` + -ParameterFilter { $Operation -eq 'Delete' } + + { Set-TargetResource ` + -Ensure 'Present' -DriveLetter 'P' -DiskUsage 1 -Force $true } | Should -Throw $errorRecord + } + } + } + + Context 'When running on a server OS' { + It 'Should throw when applied to a server operating system' { + $errorRecord = Get-InvalidOperationRecord -Message $script:localizedData.NotWorkstationOS + + Mock -CommandName Get-CimInstance @serverMock + + { Set-TargetResource -Ensure 'Present' -DriveLetter 'C' } | Should -Throw $errorRecord + } + } + } + + Describe "DSC_SystemProtection\Get-DiskUsageConfiguration" -Tag 'Helper' { + Context 'When getting maximum disk usage for a protected drive' { + It 'Should throw if the shadow storage lookup fails' { + $errorRecord = Get-InvalidOperationRecord ` + -Message $script:localizedData.UnknownOperatingSystemError + + Mock -CommandName Get-CimInstance -MockWith { throw } + + { Get-DiskUsageConfiguration -Drive $mocks.SppRegistryValueDriveP } | Should -Throw $errorRecord + } + + It 'Should throw if the volume size lookup fails' { + $errorRecord = Get-InvalidOperationRecord ` + -Message $script:localizedData.UnknownOperatingSystemError + + Mock -CommandName Get-Volume -MockWith { throw } + + { Get-DiskUsageConfiguration -Drive $mocks.SppRegistryValueDriveP } | Should -Throw $errorRecord + } + + It 'Should return an integer' { + Mock -CommandName Get-CimInstance -MockWith { return $mocks.GetCimInstanceShadowStorage } + Mock -CommandName Get-Volume -MockWith { return $mocks.GetVolumeSize } + + $result = Get-DiskUsageConfiguration -Drive $mocks.SppRegistryValueDriveP + + $result | Should -BeOfType Int + } + } + } + + Describe "DSC_SystemProtection\Get-SppRegistryValue" -Tag 'Helper' { + Context 'When getting system protection settings from the registry' { + It 'Should return null if the registry settings are not found' { + Mock -CommandName Get-ItemProperty -MockWith { return $null } + + $result = Get-SppRegistryValue + + $result | Should -BeNull + } + + It 'Should return the registry value when settings exist' { + Mock -CommandName Get-ItemProperty -MockWith { return $mocks.SppRegistryItemDriveP } + Mock -CommandName Get-ItemPropertyValue -MockWith { return $mocks.SppRegistryValueDriveP } + + $result = Get-SppRegistryValue + + $result | Should -Not -BeNull + } + } + } + + Describe "DSC_SystemProtection\Get-SystemProtectionState" -Tag 'Helper' { + Context 'When getting system protection state' { + It 'Should throw when we encounter a problem from CIM' { + $errorRecord = Get-InvalidOperationRecord ` + -Message $script:localizedData.UnknownOperatingSystemError + + Mock -CommandName Get-CimInstance -MockWith { throw } + + { Get-SystemProtectionState } | Should -Throw $errorRecord + } + + It 'Should return Present when system protection is enabled' { + Mock -CommandName Get-CimInstance -MockWith { return $mocks.GetCimInstanceestoreEnabled } + + $result = Get-SystemProtectionState + + $result | Should -Be 'Present' + } + + It 'Should return Absent when system protection is disabled' { + Mock -CommandName Get-CimInstance -MockWith { return $mocks.GetCimInstanceestoreDisabled } + + $result = Get-SystemProtectionState + + $result | Should -Be 'Absent' + } + } + } + + Describe "DSC_SystemProtection\Invoke-VssAdmin" -Tag 'Helper' { + Context 'When configuring volume shadow copies' { + It 'Should resize volume shadow storage ' { + Mock -CommandName Start-VssAdminProcess -MockWith { return $mocks.VssAdminSuccess } + + { Invoke-VssAdmin -Operation Resize -Drive 'P:' -DiskUsage 20 } | Should -Not -Throw + } + + It 'Should delete volume shadow storage ' { + Mock -CommandName Start-VssAdminProcess -MockWith { return $mocks.VssAdminSuccess } + + { Invoke-VssAdmin -Operation Delete -Drive 'P:' } | Should -Not -Throw + } + } + } + } +} +finally +{ + Invoke-TestCleanup +} diff --git a/tests/Unit/DSC_SystemRestorePoint.Tests.ps1 b/tests/Unit/DSC_SystemRestorePoint.Tests.ps1 new file mode 100644 index 00000000..68f07559 --- /dev/null +++ b/tests/Unit/DSC_SystemRestorePoint.Tests.ps1 @@ -0,0 +1,267 @@ +$script:dscModuleName = 'ComputerManagementDsc' +$script:dscResourceName = 'DSC_SystemRestorePoint' + +function Invoke-TestSetup +{ + try + { + Import-Module -Name DscResource.Test -Force -ErrorAction 'Stop' + } + catch [System.IO.FileNotFoundException] + { + throw 'DscResource.Test module dependency not found. Please run ".\build.ps1 -Tasks build" first.' + } + + $script:testEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:dscModuleName ` + -DSCResourceName $script:dscResourceName ` + -ResourceType 'Mof' ` + -TestType 'Unit' + + Import-Module -Name (Join-Path -Path $PSScriptRoot -ChildPath '..\TestHelpers\CommonTestHelper.psm1') +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $script:testEnvironment +} + +Invoke-TestSetup + +# Begin Testing +try +{ + InModuleScope $script:dscResourceName { + $targetPresentArguments = @{ + Ensure = 'Present' + Description = 'DSC Unit Test' + RestorePointType = 'MODIFY_SETTINGS' + } + + $targetAbsentArguments = @{ + Ensure = 'Absent' + Description = 'DSC Unit Test' + RestorePointType = 'MODIFY_SETTINGS' + } + + $dnePresentArguments = @{ + Ensure = 'Present' + Description = 'Does Not Exist' + RestorePointType = 'MODIFY_SETTINGS' + } + + $dneAbsentArguments = @{ + Ensure = 'Absent' + Description = 'Does Not Exist' + RestorePointType = 'MODIFY_SETTINGS' + } + + $getComputerRestorePoint = New-Object -TypeName PSObject + $getComputerRestorePoint | Add-Member -MemberType NoteProperty -Name Description -Value 'DSC Unit Test' + $getComputerRestorePoint | Add-Member -MemberType NoteProperty -Name SequenceNumber -Value 1 + $getComputerRestorePoint | Add-Member -MemberType NoteProperty -Name RestorePointType -Value 12 + + $workstationMock = @{ + ParameterFilter = $([scriptblock]::Create('$ClassName -eq ''Win32_OperatingSystem''')) + MockWith = $([scriptblock]::Create('@{ ProductType = 1 }')) + } + + $serverMock = @{ + ParameterFilter = $([scriptblock]::Create('$ClassName -eq ''Win32_OperatingSystem''')) + MockWith = $([scriptblock]::Create('@{ ProductType = 3 }')) + } + + Describe "DSC_SystemRestorePoint\Get-TargetResource" -Tag 'Get' { + Context 'When running on a workstation OS' { + Context 'When getting the target resource' { + It 'Should return Absent when requested restore point does not exist' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Get-TargetResource -Ensure 'Present' -Description 'Does Not Exist' + + $result | Should -BeOfType Hashtable + $result.Ensure | Should -Be 'Absent' + } + + It 'Should return present when requested restore point exists' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Get-TargetResource -Ensure 'Present' -Description 'DSC Unit Test' + + $result | Should -BeOfType Hashtable + $result.Ensure | Should -Be 'Present' + } + } + } + + Context 'When running on a server OS' { + Context 'When getting the target resource' { + It 'Should return Absent and write a warning on a server operating system' { + Mock -CommandName Write-Warning + Mock -CommandName Get-CimInstance @serverMock + + $protectionSettings = Get-TargetResource -Ensure 'Present' -Description 'DSC Unit Test' + + $protectionSettings.Ensure | Should -Be 'Absent' + Assert-MockCalled -CommandName Write-Warning -Times 1 + } + } + } + } + + Describe "DSC_SystemRestorePoint\Test-TargetResource" -Tag 'Test' { + Context 'When running on a workstation OS' { + Context 'When testing the target resource' { + It 'Should return true if the restore point exists' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Test-TargetResource @targetPresentArguments + + $result | Should -BeTrue + } + + It 'Should return false if the restore point does not exist' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Test-TargetResource @dnePresentArguments + + $result | Should -BeFalse + } + + It 'Should return false if the restore point description matches but the type is different' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Test-TargetResource ` + -Ensure Present ` + -Description 'DSC Unit Test' ` + -RestorePointType 'APPLICATION_INSTALL' + + $result | Should -BeFalse + } + + It 'Should return false if the restore point exists but should be absent' { + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + Mock -CommandName Get-CimInstance @workstationMock + + $result = Test-TargetResource @targetAbsentArguments + + $result | Should -BeFalse + } + } + } + + Context 'When running on a server OS' { + Context 'When testing the target resource' { + It 'Should return Absent and write warnings on a server operating system' { + Mock -CommandName Write-Warning + Mock -CommandName Get-CimInstance @serverMock + + $desiredState = Test-TargetResource -Ensure 'Present' -Description 'DSC Unit Test' + + $desiredState | Should -BeTrue + Assert-MockCalled -CommandName Write-Warning -Times 2 + } + } + } + } + + Describe "DSC_SystemRestorePoint\Set-TargetResource" -Tag 'Set' { + Context 'When running on a workstation OS' { + Context 'When setting the target resource to Present' { + Mock -CommandName Get-CimInstance @workstationMock + + It 'Should throw if the operating system encounters a problem' { + $errorRecord = Get-InvalidOperationRecord -Message $script:localizedData.CheckpointFailure + + Mock -CommandName Checkpoint-Computer -MockWith { throw } + + { Set-TargetResource @targetPresentArguments } | Should -Throw $errorRecord + } + + It 'Should create the restore point' { + Mock -CommandName Checkpoint-Computer + + { Set-TargetResource @targetPresentArguments } | Should -Not -Throw + } + } + + Context 'When setting the target resource to Absent' { + Mock -CommandName Add-Type + Mock -CommandName Get-CimInstance @workstationMock + Mock -CommandName Get-ComputerRestorePoint -MockWith { return $getComputerRestorePoint } + + It 'Should not throw even if the requested restore point does not exist' { + { Set-TargetResource @dneAbsentArguments } | Should -Not -Throw + } + + It 'Should delete the requested restore point' { + Mock -CommandName Remove-RestorePoint -MockWith { return $true } + + { Set-TargetResource @targetAbsentArguments } | Should -Not -Throw + } + + It 'Should throw if the operating system encountered a problem deleting the restore point' { + $errorRecord = Get-InvalidOperationRecord -Message $script:localizedData.DeleteCheckpointFailure + + Mock -CommandName Remove-RestorePoint -MockWith { return $false } + + { Set-TargetResource @targetAbsentArguments } | Should -Throw $errorRecord + } + } + } + + Context 'When running on a server OS' { + Context 'When setting the target resource' { + It 'Should throw when applied to a server operating system' { + $errorRecord = Get-InvalidOperationRecord -Message $script:localizedData.NotWorkstationOS + + Mock -CommandName Get-CimInstance @serverMock + + { Set-TargetResource @targetPresentArguments } | Should -Throw $errorRecord + } + } + } + } + + Describe "DSC_SystemRestorePoint\ConvertTo-RestorePointValue" -Tag 'Helper' { + Context 'When testing the helper function' { + It 'Should return null for an unrecognized name' { + ( ConvertTo-RestorePointValue -Type 'DOES_NOT_EXIST' ) | Should -BeNullOrEmpty + } + + It 'Should return correct values for restore point names' { + ( ConvertTo-RestorePointValue -Type 'APPLICATION_INSTALL' ) | Should -Be 0 + ( ConvertTo-RestorePointValue -Type 'APPLICATION_UNINSTALL' ) | Should -Be 1 + ( ConvertTo-RestorePointValue -Type 'DEVICE_DRIVER_INSTALL' ) | Should -Be 10 + ( ConvertTo-RestorePointValue -Type 'MODIFY_SETTINGS' ) | Should -Be 12 + ( ConvertTo-RestorePointValue -Type 'CANCELLED_OPERATION' ) | Should -Be 13 + } + } + } + + Describe "DSC_SystemRestorePoint\ConvertTo-RestorePointName" -Tag 'Helper' { + Context 'When testing the helper function' { + It 'Should return null for an unrecognized value' { + ( ConvertTo-RestorePointName -Type '-1' ) | Should -BeNullOrEmpty + } + + It 'Should return correct names for restore point values' { + ( ConvertTo-RestorePointName -Type '0' ) | Should -Be 'APPLICATION_INSTALL' + ( ConvertTo-RestorePointName -Type '1' ) | Should -Be 'APPLICATION_UNINSTALL' + ( ConvertTo-RestorePointName -Type '10' ) | Should -Be 'DEVICE_DRIVER_INSTALL' + ( ConvertTo-RestorePointName -Type '12' ) | Should -Be 'MODIFY_SETTINGS' + ( ConvertTo-RestorePointName -Type '13' ) | Should -Be 'CANCELLED_OPERATION' + } + } + } + } +} +finally +{ + Invoke-TestCleanup +}