Repository navigation
test(rules): the 7-only cmdlet, parameter and value tables are checked against both hosts #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| function Get-IslCoreOnlyFeature { | ||
| <# | ||
| .SYNOPSIS | ||
| The cmdlets, parameters and parameter values that PowerShell 7 has and Windows PowerShell 5.1 lacks. | ||
|
|
||
| .DESCRIPTION | ||
| The table IslPowerShell7Syntax reports from, kept in one place so a test can hold it against | ||
| both hosts: every command listed must be absent from Windows PowerShell 5.1 and present in | ||
| PowerShell 7, every parameter likewise on its command, and every value refused by the 5.1 | ||
| parameter's ValidateSet and taken by 7's. The 0.26.0 audit found two parameters here that | ||
| exist on neither host, and an Out-File value that the rule never matched; the test is what | ||
| keeps that from happening again. | ||
|
|
||
| A script that uses one of these parses under 5.1, so it starts: the call fails with an | ||
| error and the script carries on to its own exit (REM-PS7-CMDLET, REM-PS7-PARAM, | ||
| REM-PS7-ENCODING). | ||
|
|
||
| .EXAMPLE | ||
| (Get-IslCoreOnlyFeature).Parameters['ConvertFrom-Json'] | ||
|
|
||
| AsHashtable, Depth, NoEnumerate, DateKind. | ||
|
|
||
| .OUTPUTS | ||
| IntuneScriptLab.CoreOnlyFeature: Commands (string[]), Parameters (hashtable of command to | ||
| parameter names) and Values (hashtable of command to hashtable of parameter to values). | ||
| #> | ||
| [CmdletBinding()] | ||
| [OutputType('IntuneScriptLab.CoreOnlyFeature')] | ||
| param() | ||
|
|
||
| [pscustomobject]@{ | ||
| PSTypeName = 'IntuneScriptLab.CoreOnlyFeature' | ||
| # Switch-Process is not here: PowerShell 7 has it on Linux and macOS only, so a script that | ||
| # calls it fails on both Windows hosts. Get-ChildItem -FollowSymlink is not here either: | ||
| # Windows PowerShell 5.0 already had it. Both were listed until the test below ran | ||
| Commands = [string[]]@( | ||
| 'Get-Error', 'Join-String', 'Test-Json', 'ConvertFrom-Markdown', 'Get-Uptime', 'Remove-Alias', | ||
| 'Get-ExperimentalFeature', 'Get-MarkdownOption', 'Show-Markdown', 'ConvertTo-CliXml', | ||
| 'ConvertFrom-CliXml' | ||
| ) | ||
| Parameters = @{ | ||
| 'ConvertFrom-Json' = 'AsHashtable', 'Depth', 'NoEnumerate', 'DateKind' | ||
| 'ConvertTo-Json' = 'AsArray', 'EnumsAsStrings', 'EscapeHandling' | ||
| 'Split-Path' = 'LeafBase', 'Extension' | ||
| 'Invoke-WebRequest' = 'SkipCertificateCheck', 'SkipHttpErrorCheck', 'Form', 'Resume', | ||
| 'Authentication', 'Token', 'AllowInsecureRedirect', 'RetryIntervalSec' | ||
| 'Invoke-RestMethod' = 'SkipCertificateCheck', 'SkipHttpErrorCheck', 'Form', 'Resume', | ||
| 'StatusCodeVariable', 'Authentication', 'Token', 'AllowInsecureRedirect', 'ResponseHeadersVariable' | ||
| 'Select-String' = 'Raw', 'Culture', 'NoEmphasis' | ||
| 'Test-Connection' = 'TargetName', 'TcpPort', 'Ping', 'Traceroute', 'IPv4', 'IPv6', 'Repeat' | ||
| 'Get-Content' = 'AsByteStream' | ||
| 'Set-Content' = 'AsByteStream' | ||
| 'Add-Content' = 'AsByteStream' | ||
| 'Start-Process' = 'Environment' | ||
| 'Compress-Archive' = 'PassThru' | ||
| 'Import-Module' = 'UseWindowsPowerShell', 'SkipEditionCheck' | ||
| } | ||
| # A parameter both hosts have, with a value only PowerShell 7 accepts | ||
| Values = @{ | ||
| 'Out-File' = @{ Encoding = [string[]]@('utf8NoBOM') } | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| #Requires -Modules @{ ModuleName = 'Pester'; ModuleVersion = '6.2.0' } | ||
|
|
||
| <# | ||
| The table IslPowerShell7Syntax reports from, held against the two hosts themselves: each entry | ||
| must be missing from Windows PowerShell 5.1 and present in PowerShell 7. Both hosts are asked | ||
| as child processes, so the test gives the same answer whichever one runs it; it is skipped | ||
| where either host is missing. | ||
| #> | ||
|
|
||
| BeforeDiscovery { | ||
| $script:BothHosts = [bool](Get-Command powershell.exe -ErrorAction SilentlyContinue) -and | ||
| [bool](Get-Command pwsh.exe -ErrorAction SilentlyContinue) | ||
| } | ||
|
|
||
| BeforeAll { | ||
| $script:ModuleRoot = Split-Path -Parent (Split-Path -Parent (Split-Path -Parent $PSScriptRoot)) | ||
| Import-Module (Join-Path $script:ModuleRoot 'IntuneScriptLab.psd1') -Force | ||
| $script:Table = InModuleScope IntuneScriptLab { Get-IslCoreOnlyFeature } | ||
|
|
||
| # What each host says about the table: for every command whether it exists, for every parameter | ||
| # whether its command has it, and for every value whether the parameter's ValidateSet takes it | ||
| # (no ValidateSet means the value is not refused there) | ||
| $probe = { | ||
| param($Json) | ||
| $table = $Json | ConvertFrom-Json | ||
| $result = @{ Commands = @{}; Parameters = @{}; Values = @{} } | ||
| foreach ($name in $table.Commands) { | ||
| $result.Commands[$name] = [bool](Get-Command -Name $name -ErrorAction SilentlyContinue) | ||
| } | ||
| foreach ($entry in $table.Parameters) { | ||
| $command = Get-Command -Name $entry.Command -ErrorAction SilentlyContinue | ||
| foreach ($parameter in $entry.Parameters) { | ||
| $result.Parameters["$($entry.Command) -$parameter"] = | ||
| [bool]($command -and $command.Parameters.ContainsKey($parameter)) | ||
| } | ||
| } | ||
| foreach ($entry in $table.Values) { | ||
| $command = Get-Command -Name $entry.Command -ErrorAction SilentlyContinue | ||
| $set = @() | ||
| if ($command -and $command.Parameters.ContainsKey($entry.Parameter)) { | ||
| $set = @($command.Parameters[$entry.Parameter].Attributes | | ||
| Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] } | | ||
| ForEach-Object { $_.ValidValues }) | ||
| } | ||
| foreach ($value in $entry.Values) { | ||
| $result.Values["$($entry.Command) -$($entry.Parameter) $value"] = | ||
| if ($set.Count) { [bool]($set -contains $value) } else { [bool]$command } | ||
| } | ||
| } | ||
| $result | ConvertTo-Json -Depth 4 -Compress | ||
| } | ||
| $flat = [pscustomobject]@{ | ||
| Commands = @($script:Table.Commands) | ||
| Parameters = @(foreach ($command in $script:Table.Parameters.Keys) { | ||
| @{ Command = $command; Parameters = @($script:Table.Parameters[$command]) } | ||
| }) + @(@{ Command = 'ForEach-Object'; Parameters = @('Parallel') }) | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ForEach-Object -Parallel is not in the table because the rule handles it on its own path, with the REM-PS7-PARALLEL evidence. Added to the probe here so it is held against the hosts all the same. |
||
| Values = @(foreach ($command in $script:Table.Values.Keys) { | ||
| foreach ($parameter in $script:Table.Values[$command].Keys) { | ||
| $values = @($script:Table.Values[$command][$parameter]) | ||
| @{ Command = $command; Parameter = $parameter; Values = $values } | ||
| } | ||
| }) | ||
| } | ||
| $json = $flat | ConvertTo-Json -Depth 5 -Compress | ||
| $command = "& { $($probe.ToString()) } '$($json.Replace("'", "''"))'" | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The table travels as a single-quoted literal argument inside the encoded command, so no JSON quoting crosses the command line, and -EncodedCommand carries the same bytes to both hosts. |
||
| $encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($command)) | ||
| function script:Get-HostAnswer { | ||
| param([string]$Exe, [string]$Command) | ||
| $raw = & $Exe -NoProfile -NonInteractive -EncodedCommand $Command 2>&1 | ||
| ($raw | Where-Object { "$_".StartsWith('{') } | Select-Object -Last 1) | ConvertFrom-Json | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Either host can print lines of its own before the answer: module import noise, or a stderr record merged in by 2>&1. The answer is the last line that starts with a brace. |
||
| } | ||
| # Discovery-time variables do not reach the run, so the lookup is repeated here | ||
| $script:BothHosts = [bool](Get-Command powershell.exe -ErrorAction SilentlyContinue) -and | ||
| [bool](Get-Command pwsh.exe -ErrorAction SilentlyContinue) | ||
| if ($script:BothHosts) { | ||
| $script:Desktop = Get-HostAnswer 'powershell.exe' $encoded | ||
| $script:Core = Get-HostAnswer 'pwsh.exe' $encoded | ||
| } | ||
| } | ||
|
|
||
| AfterAll { | ||
| Remove-Module IntuneScriptLab -Force -ErrorAction SilentlyContinue | ||
| } | ||
|
|
||
| Describe 'Get-IslCoreOnlyFeature' -Tag 'Unit', 'Private' { | ||
|
|
||
| It 'returns the three tables' { | ||
| $script:Table.PSObject.TypeNames | Should-ContainCollection 'IntuneScriptLab.CoreOnlyFeature' | ||
| $script:Table.Commands.Count | Should-BeGreaterThan 5 | ||
| $script:Table.Parameters.Keys.Count | Should-BeGreaterThan 5 | ||
| $script:Table.Values['Out-File'].Encoding | Should-ContainCollection 'utf8NoBOM' | ||
| } | ||
|
|
||
| Context 'Against both hosts' -Skip:(-not $script:BothHosts) { | ||
| It 'every listed command is missing from Windows PowerShell 5.1 and present in PowerShell 7' { | ||
| $wrong = @(foreach ($name in $script:Table.Commands) { | ||
| if ($script:Desktop.Commands.$name) { "$name exists in 5.1" } | ||
| if (-not $script:Core.Commands.$name) { "$name is missing from 7" } | ||
| }) | ||
| $wrong | Should-BeCollection @() | ||
| } | ||
|
|
||
| It 'every listed parameter is missing from the 5.1 command and present on the 7 one' { | ||
| $keys = @($script:Core.Parameters.PSObject.Properties.Name) | ||
| $keys.Count | Should-BeGreaterThan 30 | ||
|
Owner
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A guard that the probe answered at all. Without it a probe that failed to read the table would return no keys, and the three assertions would pass on an empty set. |
||
| $wrong = @(foreach ($key in $keys) { | ||
| if ($script:Desktop.Parameters.$key) { "$key exists in 5.1" } | ||
| if (-not $script:Core.Parameters.$key) { "$key is missing from 7" } | ||
| }) | ||
| $wrong | Should-BeCollection @() | ||
| } | ||
|
|
||
| It 'every listed value is refused by the 5.1 parameter and taken by the 7 one' { | ||
| $keys = @($script:Core.Values.PSObject.Properties.Name) | ||
| $keys.Count | Should-BeGreaterThan 0 | ||
| $wrong = @(foreach ($key in $keys) { | ||
| if ($script:Desktop.Values.$key) { "$key is accepted by 5.1" } | ||
| if (-not $script:Core.Values.$key) { "$key is refused by 7" } | ||
| }) | ||
| $wrong | Should-BeCollection @() | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typed, here and for the Out-File value below, so a table cut down to one entry still serializes as an array when the test sends it to the hosts as JSON; a lone string would arrive as a scalar and the probe would iterate its characters.