diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index 1c720079..110f65bc 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -38,6 +38,7 @@ uilt Windo ELSPROBLEMS requ +PDevice whatif pscustomobject VGpu diff --git a/.github/actions/spelling/expect/generic_terms.txt b/.github/actions/spelling/expect/generic_terms.txt index 870c3e87..7c576a24 100644 --- a/.github/actions/spelling/expect/generic_terms.txt +++ b/.github/actions/spelling/expect/generic_terms.txt @@ -19,4 +19,6 @@ ssh usr versioning VGpu +pnp +RTX ADDLOCAL diff --git a/pipelines/azure-pipelines.yml b/pipelines/azure-pipelines.yml index e68067ed..12367095 100644 --- a/pipelines/azure-pipelines.yml +++ b/pipelines/azure-pipelines.yml @@ -80,6 +80,10 @@ extends: displayName: 'Publish Pipeline Microsoft.DotNet.Dsc' targetPath: $(Build.SourcesDirectory)\resources\Microsoft.DotNet.Dsc\ artifactName: Microsoft.DotNet.Dsc + - output: pipelineArtifact + displayName: 'Publish Pipeline Microsoft.Windows.Assertion' + targetPath: $(Build.SourcesDirectory)\resources\Microsoft.Windows.Assertion\ + artifactName: Microsoft.Windows.Assertion" steps: - checkout: self @@ -92,6 +96,11 @@ extends: pwsh: true targetType: 'inline' script: | + $InstalledPesterModules = (Get-Module -ListAvailable -Name Pester) + if ($InstalledPesterModules -and $InstalledPesterModules[0].Version -lt [System.Version]::Parse('5.6.1')) { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force + Install-Module -Name Pester -RequiredVersion 5.6.1 -Force -SkipPublisherCheck + } $env:PSModulePath += ";$(Build.SourcesDirectory)\resources" Invoke-Pester -CI workingDirectory: $(Build.SourcesDirectory)\tests\ diff --git a/resources/Help/Microsoft.Windows.Assertion/PnPDevice.md b/resources/Help/Microsoft.Windows.Assertion/PnPDevice.md new file mode 100644 index 00000000..b27ddecf --- /dev/null +++ b/resources/Help/Microsoft.Windows.Assertion/PnPDevice.md @@ -0,0 +1,71 @@ +--- +external help file: Microsoft.Windows.Assertion.psm1-Help.xml +Module Name: Microsoft.Windows.Assertion +ms.date: 11/2/2024 +online version: +schema: 2.0.0 +title: PnPDevice +--- + +# PnPDevice + +## SYNOPSIS + +Ensures at least one PnP Device is connected which matches the required parameters + +## DESCRIPTION + +The `PnPDevice` DSC Resource allows you to check for specific PnP Devices on the system as a pre-requisite for invoking other DSC Resources. This resource does not have the capability to modify PnP Device information. + +## PARAMETERS + +**Parameter**|**Attribute**|**DataType**|**Description**|**Allowed Values** +:-----|:-----|:-----|:-----|:----- +`FriendlyName`|Optional|String[]|The name of the PnP Device to be found|-- +`DeviceClass`|Optional|String[]|The PnP Class of the PnP Device to be found.| For example: `Display` or `Keyboard` or `PrintQueue` +`Status`|Optional|String[]|The current status of the PnP Device to be found|`OK`, `ERROR`, `DEGRADED`, `UNKNOWN` + +## EXAMPLES + +### Example 1 + +```powershell +# Check that a device with a specific name is connected +$params = @{ + FriendlyName = 'NVIDIA RTX A1000 Laptop GPU' +} +Invoke-DscResource -Name PnPDevice -Method Test -Property $params -ModuleName Microsoft.Windows.Assertion +``` + +### EXAMPLE 2 + +```powershell +# Check that any PnP Device is in the error state +$params = @{ + Status = 'ERROR' +} +Invoke-DscResource -Name PnPDevice -Method Test -Property $params -ModuleName Microsoft.Windows.Assertion +``` + +### EXAMPLE 3 + +```powershell +# Check that any Keyboard or Mouse is in the error state +$params = @{ + DeviceClass = @('Keyboard'; 'Mouse') + Status = 'ERROR' +} +Invoke-DscResource -Name PnPDevice -Method Test -Property $params -ModuleName Microsoft.Windows.Assertion +``` + +### EXAMPLE 4 + +```powershell +# Check that a specific device is operational +$params = @{ + FriendlyName = 'Follow-You-Printing' + DeviceClass = 'PrintQueue' + Status = 'OK' +} +Invoke-DscResource -Name PnPDevice -Method Test -Property $params -ModuleName Microsoft.Windows.Assertion +``` diff --git a/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psd1 b/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psd1 new file mode 100644 index 00000000..f82b3053 --- /dev/null +++ b/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psd1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +@{ + RootModule = 'Microsoft.Windows.Assertion.psm1' + ModuleVersion = '0.1.0' + GUID = 'e3510ba2-cc19-4fb2-872a-a40833c30e58' + Author = 'Microsoft Corporation' + CompanyName = 'Microsoft Corporation' + Copyright = '(c) Microsoft Corp. All rights reserved.' + Description = 'DSC Module for ensuring the system meets certain specifications' + PowerShellVersion = '7.2' + DscResourcesToExport = @( + 'OsEditionId', + 'SystemArchitecture', + 'ProcessorArchitecture', + 'HyperVisor', + 'OsInstallDate', + 'OsVersion', + 'CsManufacturer', + 'CsModel', + 'CsDomain', + 'PowerShellVersion', + 'PnPDevice' + ) + PrivateData = @{ + PSData = @{ + # A URL to the license for this module. + LicenseUri = 'https://github.com/microsoft/winget-dsc#MIT-1-ov-file' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/microsoft/winget-dsc' + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @( + 'PSDscResource_OsEditionId', + 'PSDscResource_SystemArchitecture', + 'PSDscResource_ProcessorArchitecture', + 'PSDscResource_HyperVisor', + 'PSDscResource_OsInstallDate', + 'PSDscResource_OsVersion', + 'PSDscResource_CsManufacturer', + 'PSDscResource_CsModel', + 'PSDscResource_CsDomain', + 'PSDscResource_PowerShellVersion', + 'PSDscResource_PnPDevice' + ) + + # Prerelease string of this module + Prerelease = 'alpha' + } + } +} diff --git a/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psm1 b/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psm1 new file mode 100644 index 00000000..faf2c88b --- /dev/null +++ b/resources/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.psm1 @@ -0,0 +1,353 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +enum Ensure { + Present + Absent +} + +enum PnPDeviceState { + OK + ERROR + DEGRADED + UNKNOWN +} + +[DSCResource()] +class OsEditionId { + + [DscProperty(Key)] + [string] $Edition + + [OsEditionId] Get() { + $currentState = [OsEditionId]::new() + $currentState.Edition = Get-ComputerInfo | Select-Object -ExpandProperty WindowsEditionId + return $currentState + } + + [bool] Test() { + return $this.Get().Edition -eq $this.Edition + } + + [void] Set() { + # This resource is only for asserting the Edition ID requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Edition)' but received '$($this.Get().Edition)'") + } +} + +[DSCResource()] +class SystemArchitecture { + + [DscProperty(Key)] + [string] $Architecture + + [SystemArchitecture] Get() { + $currentState = [SystemArchitecture]::new() + $currentState.Architecture = Get-ComputerInfo | Select-Object -ExpandProperty OsArchitecture + return $currentState + } + + [bool] Test() { + return $this.Get().Architecture -eq $this.Architecture + } + + [void] Set() { + # This resource is only for asserting the System Architecture requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Architecture)' but received '$($this.Get().Architecture)'") + } +} + +[DSCResource()] +class ProcessorArchitecture { + + [DscProperty(Key)] + [string] $Architecture + + [ProcessorArchitecture] Get() { + $currentState = [ProcessorArchitecture]::new() + $currentState.Architecture = $env:PROCESSOR_ARCHITECTURE + return $currentState + } + + [bool] Test() { + return $this.Get().Architecture -eq $this.Architecture + } + + [void] Set() { + # This resource is only for asserting the Processor Architecture requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Architecture)' but received '$($this.Get().Architecture)'") + } +} + +[DSCResource()] +class HyperVisor { + + [DscProperty(Key)] + [Ensure] $Ensure + + [HyperVisor] Get() { + $currentState = [HyperVisor]::new() + $currentState.Ensure = (Get-ComputerInfo | Select-Object -ExpandProperty HyperVisorPresent) ? [Ensure]::Present : [Ensure]::Absent + return $currentState + } + + [bool] Test() { + return $this.Get().Ensure -eq $this.Ensure + } + + [void] Set() { + # This resource is only for asserting the presence of a HyperVisor. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Ensure)' but received '$($this.Get().Ensure)'") + } +} + +[DSCResource()] +class OsInstallDate { + + [DscProperty(Key)] + [string] $Before + + [DscProperty()] + [string] $After + + [DscProperty(NotConfigurable)] + [string] $InstallDate + + [OsInstallDate] Get() { + $currentState = [OsInstallDate]::new() + + # Try-Catch isn't a good way to do this, but `[System.DateTimeOffset]::TryParse($this.Before, [ref]$parsedBefore)` is erroring + try { + if ($this.Before) { [System.DateTimeOffset]::Parse($this.Before) } + } catch { + throw "'$($this.Before)' is not a valid Date string." + } + + # Try-Catch isn't a good way to do this, but `[System.DateTimeOffset]::TryParse($this.After, [ref]$parsedAfter)` is erroring + try { + if ($this.After) { [System.DateTimeOffset]::Parse($this.After) } + } catch { + throw "'$($this.After)' is not a valid Date string." + } + + $currentState.Before = $this.Before + $currentState.After = $this.After + $currentState.InstallDate = Get-ComputerInfo | Select-Object -ExpandProperty OsInstallDate + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + if ($this.Before -and [System.DateTimeOffset]$currentState.InstallDate -gt [System.DateTimeOffset]$this.Before) { return $false } # The InstallDate was later than the specified 'Before' date + if ($this.After -and [System.DateTimeOffset]$currentState.InstallDate -lt [System.DateTimeOffset]$this.After) { return $false } # The InstallDate was earlier than the specified 'After' date + return $true + } + + [void] Set() { + # This resource is only for asserting the OS Install Date. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. '$($this.Before)' >= '$($this.Get().InstallDate)' >= '$($this.After)' evaluated to 'False'") + } +} + +# This is the same function from Microsoft.Windows.Developer, just included here as it seemed to make sense +[DSCResource()] +class OsVersion { + + [DscProperty(Key)] + [string] $MinVersion + + [DscProperty(NotConfigurable)] + [string] $OsVersion + + [OsVersion] Get() { + + if ($this.MinVersion -and ![System.Version]::TryParse($this.MinVersion, [ref]$null)) { + throw "'$($this.MinVersion)' is not a valid Version string." + } + + $currentState = [OsVersion]::new() + $currentState.MinVersion = $this.MinVersion + $currentState.OsVersion = Get-ComputerInfo | Select-Object -ExpandProperty OsVersion + return $currentState + } + + [bool] Test() { + return [System.Version]$this.Get().OsVersion -ge [System.Version]$this.MinVersion + } + + [void] Set() { + # This resource is only for asserting the os version requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. '$($this.Get().OsVersion)' >= '$($this.MinVersion)' evaluated to 'False'") + } +} + +[DSCResource()] +class CsManufacturer { + + [DscProperty(Key)] + [string] $Manufacturer + + [CsManufacturer] Get() { + $currentState = [CsManufacturer]::new() + $currentState.Manufacturer = Get-ComputerInfo | Select-Object -ExpandProperty CsManufacturer + return $currentState + } + + [bool] Test() { + return $this.Get().Manufacturer -eq $this.Manufacturer + } + + [void] Set() { + # This resource is only for asserting the Computer Manufacturer requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Manufacturer)' but received '$($this.Get().Manufacturer)'") + } +} + +[DSCResource()] +class CsModel { + + [DscProperty(Key)] + [string] $Model + + [CsModel] Get() { + $currentState = [CsModel]::new() + $currentState.Model = Get-ComputerInfo | Select-Object -ExpandProperty CsModel + return $currentState + } + + [bool] Test() { + return $this.Get().Model -eq $this.Model + } + + [void] Set() { + # This resource is only for asserting the Computer Model requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Model)' but received '$($this.Get().Model)'") + } +} + +[DSCResource()] +class CsDomain { + + [DscProperty(Key)] + [string] $Domain + + [DscProperty()] + [string] $Role + + [CsDomain] Get() { + $domainInfo = Get-ComputerInfo | Select-Object -Property CsDomain, CsDomainRole + + $currentState = [CsDomain]::new() + $currentState.Domain = $domainInfo.CsDomain + $currentState.Role = $domainInfo.CsDomainRole + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + if ($currentState.Domain -ne $this.Domain) { return $false } # If domains don't match + if (!$this.Role) { return $true } # Required Role is null and domains match + return ($currentState.Role -eq $this.Role) # Return whether the roles match + } + + [void] Set() { + # This resource is only for asserting the Computer Domain requirement. + if ($this.Test()) { return } + $currentState = $this.Get() + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. Expected '$($this.Domain)<$($this.Role)>' but received '$($currentState.Domain)<$($currentState.Role)>'") + } +} + +[DSCResource()] +class PowerShellVersion { + + [DscProperty(Key)] + [string] $MinVersion + + [DscProperty(NotConfigurable)] + [string] $PowerShellVersion + + [PowerShellVersion] Get() { + + if ($this.MinVersion -and ![System.Version]::TryParse($this.MinVersion, [ref]$null)) { + throw "'$($this.MinVersion)' is not a valid Version string." + } + + $currentState = [PowerShellVersion]::new() + $currentState.MinVersion = $this.MinVersion + $currentState.PowerShellVersion = $global:PSVersionTable.PSVersion + return $currentState + } + + [bool] Test() { + $currentState = $this.Get() + return [System.Version]$currentState.PowerShellVersion -ge [System.Version]$currentState.MinVersion + } + + [void] Set() { + # This resource is only for asserting the PowerShell version requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New("Assertion Failed. '$($this.Get().PowerShellVersion)' >= '$($this.MinVersion)' evaluated to 'False'") + } +} + +[DSCResource()] +class PnPDevice { + + [DscProperty(Key)] + [Ensure] $Ensure + + [DscProperty()] + [string[]] $FriendlyName + + [DscProperty()] + [string[]] $DeviceClass + + [DscProperty()] + [PnPDeviceState[]] $Status + + [PnPDevice] Get() { + $params = @{} + $params += $this.FriendlyName ? @{FriendlyName = $this.FriendlyName } : @{} + $params += $this.DeviceClass ? @{Class = $this.DeviceClass } : @{} + $params += $this.Status ? @{Status = $this.Status } : @{} + + $pnpDevice = @(Get-PnpDevice @params) + + # It's possible that multiple PNP devices match, but as long as one matches then the assertion succeeds + $currentState = [PnPDevice]::new() + $currentState.Ensure = $pnpDevice ? [Ensure]::Present : [Ensure]::Absent + $currentState.FriendlyName = $this.FriendlyName + $currentState.DeviceClass = $this.DeviceClass + $currentState.Status = $this.Status + return $currentState + } + + [bool] Test() { + return $this.Get().Ensure -eq $this.Ensure + } + + [void] Set() { + # This resource is only for asserting the PnP Device requirement. + if ($this.Test()) { return } + throw [System.Configuration.ConfigurationException]::New('Assertion Failed. ' + + $( if ($this.Ensure -eq [Ensure]::Present) { + 'No PnP devices found which matched the parameters' + } else { + 'One or more PnP devices found which matched the parameters' + } + ) + ) + } +} diff --git a/tests/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.Tests.ps1 b/tests/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.Tests.ps1 new file mode 100644 index 00000000..f9a1156e --- /dev/null +++ b/tests/Microsoft.Windows.Assertion/Microsoft.Windows.Assertion.Tests.ps1 @@ -0,0 +1,552 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +using module Microsoft.Windows.Assertion + +$ErrorActionPreference = 'Stop' +Set-StrictMode -Version Latest + +<# +.Synopsis + Pester tests related to the Microsoft.WinGet.Developer PowerShell module. +#> + +BeforeAll { + if ($null -eq (Get-Module -ListAvailable -Name PSDesiredStateConfiguration)) { + Install-Module -Name PSDesiredStateConfiguration -Force -SkipPublisherCheck + } + Import-Module Microsoft.Windows.Assertion +} + +Describe 'List available DSC resources' { + It 'Shows DSC Resources' { + $expectedDSCResources = 'OsEditionId', 'SystemArchitecture', 'ProcessorArchitecture', 'HyperVisor', 'OsInstallDate', 'OsVersion', 'CsManufacturer', 'CsModel', 'CsDomain', 'PowerShellVersion', 'PnPDevice' + $availableDSCResources = (Get-DscResource -Module Microsoft.Windows.Assertion).Name + $availableDSCResources.length | Should -Be 11 + $availableDSCResources | Where-Object { $expectedDSCResources -notcontains $_ } | Should -BeNullOrEmpty -ErrorAction Stop + } +} + +InModuleScope -ModuleName Microsoft.Windows.Assertion { + Describe 'SystemArchitecture' { + BeforeAll { + Mock Get-ComputerInfo { return @{OsArchitecture = 'TestValue' } } + } + + $script:SystemArchitectureResource = [SystemArchitecture]::new() + + It 'Get Current Property' -Tag 'Get' { + $SystemArchitectureResource.Get().Architecture | Should -Be 'TestValue' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $SystemArchitectureResource.Architecture = 'TestValue' + $SystemArchitectureResource.Test() | Should -Be $true + } + It 'Should not match' { + $SystemArchitectureResource.Architecture = 'Value' + $SystemArchitectureResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $SystemArchitectureResource.Architecture = 'TestValue' + { $SystemArchitectureResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $SystemArchitectureResource.Architecture = 'Value' + { $SystemArchitectureResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'OsEditionId' { + BeforeAll { + Mock Get-ComputerInfo { return @{WindowsEditionId = 'TestValue' } } + } + + $script:OsEditionResource = [OsEditionId]::new() + + It 'Get Current Property' -Tag 'Get' { + $OsEditionResource.Get().Edition | Should -Be 'TestValue' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $OsEditionResource.Edition = 'TestValue' + $OsEditionResource.Test() | Should -Be $true + } + It 'Should not match' { + $OsEditionResource.Edition = 'Value' + $OsEditionResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $OsEditionResource.Edition = 'TestValue' + { $OsEditionResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $OsEditionResource.Edition = 'Value' + { $OsEditionResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'ProcessorArchitecture' { + BeforeAll { + $script:CurrentArchitecture = $env:PROCESSOR_ARCHITECTURE + $env:PROCESSOR_ARCHITECTURE = 'TestValue' + } + + $script:ProcessorArchitectureResource = [ProcessorArchitecture]::new() + + It 'Get Current Property' -Tag 'Get' { + $ProcessorArchitectureResource.Get().Architecture | Should -Be 'TestValue' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $ProcessorArchitectureResource.Architecture = 'TestValue' + $ProcessorArchitectureResource.Test() | Should -Be $true + } + It 'Should not match' { + $ProcessorArchitectureResource.Architecture = 'Value' + $ProcessorArchitectureResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $ProcessorArchitectureResource.Architecture = 'TestValue' + { $ProcessorArchitectureResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $ProcessorArchitectureResource.Architecture = 'Value' + { $ProcessorArchitectureResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + + AfterAll { + $env:PROCESSOR_ARCHITECTURE = $script:CurrentArchitecture + } + + } + + Describe 'HyperVisor' { + BeforeAll { + Mock Get-ComputerInfo { return @{HyperVisorPresent = $true } } + } + + $script:HyperVisorResource = [HyperVisor]::new() + + It 'Get Current Property' -Tag 'Get' { + $HyperVisorResource.Get().Ensure | Should -Be 'Present' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $HyperVisorResource.Ensure = 'Present' + $HyperVisorResource.Test() | Should -Be $true + } + It 'Should not match' { + $HyperVisorResource.Ensure = 'Absent' + $HyperVisorResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $HyperVisorResource.Ensure = 'Present' + { $HyperVisorResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $HyperVisorResource.Ensure = 'Absent' + { $HyperVisorResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'OsInstallDate' { + BeforeAll { + $script:MockOsInstallDate = 'Saturday, November 2, 2024 12:30:00 AM' + Mock Get-ComputerInfo { return @{OsInstallDate = $script:MockOsInstallDate } } + } + + $script:OsInstallDateResource = [OsInstallDate]::new() + + It 'Get Current Property' -Tag 'Get' { + $initialState = $OsInstallDateResource.Get() + [String]::IsNullOrEmpty($initialState.Before) | Should -Be $true + [String]::IsNullOrEmpty($initialState.After) | Should -Be $true + $initialState.InstallDate | Should -Be $([System.DateTimeOffset]::Parse($script:MockOsInstallDate)) + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match between dates' { + $OsInstallDateResource.Before = 'Sunday, November 3, 2024 12:00:00 AM' + $OsInstallDateResource.After = 'Friday, November 1, 2024 12:00:00 AM' + $OsInstallDateResource.Test() | Should -Be $true + } + It 'Should fail if before constraint is violated' { + $OsInstallDateResource.Before = 'Friday, November 1, 2024 12:00:00 AM' + $OsInstallDateResource.Test() | Should -Be $false + } + It 'Should fail if after constraint is violated' { + $OsInstallDateResource.After = 'Sunday, November 3, 2024 12:00:00 AM' + $OsInstallDateResource.Test() | Should -Be $false + } + It 'Should take minutes and seconds into consideration' { + $OsInstallDateResource.Before = 'Saturday, November 2, 2024 12:29:59 AM' + $OsInstallDateResource.Test() | Should -Be $false + } + It 'Should throw if before is not a date' { + $OsInstallDateResource.Before = 'This is not a date' + { $OsInstallDateResource.Test() } | Should -Throw + } + It 'Should throw if after is not a date' { + $OsInstallDateResource.Before = 'This is not a date' + { $OsInstallDateResource.Test() } | Should -Throw + } + } + + Context 'Set Current Property' -Tag 'Set' { + BeforeAll { + $script:OsInstallDateResource = [OsInstallDate]::new() # Reset properties from the -Tag 'Test' methods + } + It 'Should succeed when setting is not required' { + $OsInstallDateResource.Before = 'Sunday, November 3, 2024 12:00:00 AM' + { $OsInstallDateResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $OsInstallDateResource.Before = 'Friday, November 1, 2024 12:00:00 AM' + { $OsInstallDateResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'OsVersion' { + BeforeAll { + Mock Get-ComputerInfo { return @{OsVersion = '1.2.0' } } + } + + $script:OsVersionResource = [OsVersion]::new() + + It 'Get Current Property' -Tag 'Get' { + $initialState = $OsVersionResource.Get() + [String]::IsNullOrEmpty($initialState.MinVersion) | Should -Be $true + $initialState.OsVersion | Should -Be '1.2.0' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should succeed' { + $OsVersionResource.MinVersion = '1.0.0' + $OsVersionResource.Test() | Should -Be $true + } + It 'Should fail' { + $OsVersionResource.MinVersion = '2.0.0' + $OsVersionResource.Test() | Should -Be $false + } + It 'Should throw if MinVersion is not a version' { + $OsVersionResource.MinVersion = 'This is not a version' + { $OsVersionResource.Test() } | Should -Throw + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $OsVersionResource.MinVersion = '1.0.0' + { $OsVersionResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $OsVersionResource.MinVersion = '2.0.0' + { $OsVersionResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'CsManufacturer' { + BeforeAll { + Mock Get-ComputerInfo { return @{CsManufacturer = 'TestValue' } } + } + + $script:CsManufacturerResource = [CsManufacturer]::new() + + It 'Get Current Property' -Tag 'Get' { + $CsManufacturerResource.Get().Manufacturer | Should -Be 'TestValue' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $CsManufacturerResource.Manufacturer = 'TestValue' + $CsManufacturerResource.Test() | Should -Be $true + } + It 'Should not match' { + $CsManufacturerResource.Manufacturer = 'Value' + $CsManufacturerResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $CsManufacturerResource.Manufacturer = 'TestValue' + { $CsManufacturerResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $CsManufacturerResource.Manufacturer = 'Value' + { $CsManufacturerResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'CsModel' { + BeforeAll { + Mock Get-ComputerInfo { return @{CsModel = 'TestValue' } } + } + + $script:CsModelResource = [CsModel]::new() + + It 'Get Current Property' -Tag 'Get' { + $CsModelResource.Get().Model | Should -Be 'TestValue' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match' { + $CsModelResource.Model = 'TestValue' + $CsModelResource.Test() | Should -Be $true + } + It 'Should not match' { + $CsModelResource.Model = 'Value' + $CsModelResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $CsModelResource.Model = 'TestValue' + { $CsModelResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $CsModelResource.Model = 'Value' + { $CsModelResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'CsDomain' { + BeforeAll { + Mock Get-ComputerInfo { return @{CsDomain = 'TestDomain'; CsDomainRole = 'TestRole' } } + } + + $script:CsDomainResource = [CsDomain]::new() + + It 'Get Current Property' -Tag 'Get' { + $initialState = $CsDomainResource.Get() + $initialState.Domain | Should -Be 'TestDomain' + $initialState.Role | Should -Be 'TestRole' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Domain is specified and role is null should match' { + $CsDomainResource.Domain = 'TestDomain' + $CsDomainResource.Test() | Should -Be $true + } + It 'Domain is specified and role is null should not match' { + $CsDomainResource.Domain = 'Domain' + $CsDomainResource.Test() | Should -Be $false + } + It 'Domain and role specified should match' { + $CsDomainResource.Domain = 'TestDomain' + $CsDomainResource.Role = 'TestRole' + $CsDomainResource.Test() | Should -Be $true + } + It 'Domain and role specified should not match' { + $CsDomainResource.Domain = 'TestDomain' + $CsDomainResource.Role = 'Role' + $CsDomainResource.Test() | Should -Be $false + } + } + + Context 'Set Current Property' -Tag 'Set' { + BeforeAll { + $script:CsDomainResource = [CsDomain]::new() # Reset properties from the -Tag 'Test' methods + } + It 'Should succeed when setting is not required' { + $CsDomainResource.Domain = 'TestDomain' + { $CsDomainResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $CsDomainResource.Domain = 'Domain' + { $CsDomainResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + } + + Describe 'PowerShellVersion' { + BeforeAll { + $script:OriginalPsVersion = $global:PSVersionTable.PSVersion + $global:PSVersionTable.PSVersion = [System.Version]'7.2.0.0' + } + + $script:PowerShellVersionResource = [PowerShellVersion]::new() + + It 'Get Current Property' -Tag 'Get' { + $initialState = $PowerShellVersionResource.Get() + [String]::IsNullOrEmpty($initialState.MinVersion) | Should -Be $true + $initialState.PowerShellVersion | Should -Be '7.2.0.0' + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should succeed' { + $PowerShellVersionResource.MinVersion = '7.2' + $PowerShellVersionResource.Test() | Should -Be $true + } + It 'Should fail' { + $PowerShellVersionResource.MinVersion = '7.2.1' + $PowerShellVersionResource.Test() | Should -Be $false + } + It 'Should throw if MinVersion is not a version' { + $PowerShellVersionResource.MinVersion = 'This is not a version' + { $PowerShellVersionResource.Test() } | Should -Throw + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $PowerShellVersionResource.MinVersion = '7.2' + { $PowerShellVersionResource.Set() } | Should -Not -Throw + } + It 'Should throw otherwise' { + $PowerShellVersionResource.MinVersion = '7.2.1' + { $PowerShellVersionResource.Set() } | Should -Throw 'Assertion Failed. *' + } + } + + AfterAll { + $global:PSVersionTable.PSVersion = $script:OriginalPsVersion + } + } + + Describe 'PnPDevice' { + BeforeAll { + + $script:TestPnPDevice = @{ + FriendlyName = 'TestName' + Class = 'TestClass' + Status = 'OK' + } + + # Mock when all parameters are present + Mock Get-PnPDevice -ParameterFilter { $FriendlyName -eq 'TestName' -and $DeviceClass -eq 'TestClass' -and $Status -eq 'OK' } -MockWith { return $script:TestPnPDevice } + # Mock when two parameters are present + Mock Get-PnPDevice -ParameterFilter { $FriendlyName -eq 'TestName' -and $DeviceClass -eq 'TestClass' -and [String]::IsNullOrWhiteSpace($Status) } -MockWith { return $script:TestPnPDevice } + # Mock when one parameter is present + Mock Get-PnPDevice -ParameterFilter { $FriendlyName -eq 'TestName' -and [String]::IsNullOrWhiteSpace($DeviceClass) -and [String]::IsNullOrWhiteSpace($Status) } -MockWith { return $script:TestPnPDevice } + # Catch-all Mock + Mock Get-PnPDevice -ParameterFilter { } -MockWith { return @{ FriendlyName = $null; Class = $null; Status = 'UNKNOWN' } } + } + + BeforeEach { + # Because of the way the Status enum works, the instance of the resource needs to be re-created for each test + $script:PnPDeviceResource = [PnPDevice]::new() + } + + $script:PnPDeviceResource = [PnPDevice]::new() + Context 'Get Current Property' -Tag 'Get' { + It 'Should match a device with one property specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $initialState = $PnPDeviceResource.Get() + $initialState.Ensure | Should -Be 'Present' + } + It 'Should match a device with two properties specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $initialState = $PnPDeviceResource.Get() + $initialState.Ensure | Should -Be 'Present' + } + It 'Should match a device with all properties specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Status = 'OK' + $initialState = $PnPDeviceResource.Get() + $initialState.Ensure | Should -Be 'Present' + } + It 'Should not match a device with bad FriendlyName' { + $PnPDeviceResource.FriendlyName = 'Name' + $initialState = $PnPDeviceResource.Get() + $initialState.Ensure | Should -Be 'Absent' + } + It 'Should not match a device with bad status' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Status = 'ERROR' + $initialState = $PnPDeviceResource.Get() + $initialState.Ensure | Should -Be 'Absent' + } + } + + Context 'Test Current Property' -Tag 'Test' { + It 'Should match a device with one property specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.Ensure = 'Present' + $PnPDeviceResource.Test() | Should -Be $true + } + It 'Should match a device with two properties specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Ensure = 'Present' + $PnPDeviceResource.Test() | Should -Be $true + } + It 'Should match a device with all properties specified' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Status = 'OK' + $PnPDeviceResource.Ensure = 'Present' + $PnPDeviceResource.Test() | Should -Be $true + } + It 'Should not match a device with bad FriendlyName' { + $PnPDeviceResource.FriendlyName = 'Name' + $PnPDeviceResource.Status = 'OK' + $PnPDeviceResource.Ensure = 'Present' + $PnPDeviceResource.Test() | Should -Be $false + } + It 'Should not match a device with bad status' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Status = 'ERROR' + $PnPDeviceResource.Ensure = 'Present' + $PnPDeviceResource.Test() | Should -Be $false + } + It 'Should match a device with bad status being absent' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.DeviceClass = 'TestClass' + $PnPDeviceResource.Status = 'ERROR' + $PnPDeviceResource.Ensure = 'Absent' + $PnPDeviceResource.Test() | Should -Be $true + } + } + + Context 'Set Current Property' -Tag 'Set' { + It 'Should succeed when setting is not required' { + $PnPDeviceResource.FriendlyName = 'TestName' + { $PnPDeviceResource.Set() } | Should -Not -Throw + } + It 'Should throw with One or more PnP devices when ensuring absent' { + $PnPDeviceResource.FriendlyName = 'TestName' + $PnPDeviceResource.Ensure = 'Absent' + { $PnPDeviceResource.Set() } | Should -Throw 'Assertion Failed. One or more PnP devices found which matched the parameters' + } + It 'Should throw with no PnP devices when ensuring present' { + $PnPDeviceResource.FriendlyName = 'Name' + $PnPDeviceResource.Ensure = 'Present' + { $PnPDeviceResource.Set() } | Should -Throw 'Assertion Failed. No PnP devices found which matched the parameters' + } + } + } +} + + +AfterAll { +}