Pass Variables to Powershell Pester Tests v5 - powershell

I am at the last stages with my pester tests, but am having issues with the variable scoping.
I have quite a few tests and would like use something like Global scoping in order to declare variables such as $ModuleName, $ProjectRoot, and other similar things that are needed in each test.
I have tried using New-Variable -Scope Global and $Global:ModuleName but these don't seem to be visible in the Pester Tests.
I am calling Invoke-Pester from a build.ps1 file which has these declared.
Has anyone seen any good ways to use centrally defined variables, without using $env: variables?

I have found using $Env: variables to be the best approach. Thanks to the BuildHelpers module for showing me this.
This is now what the beginning of my Pester Test looks like:
Describe "Core Module Validation" {
BeforeAll {
$Script:moduleName = $ENV:BHProjectName
$Script:modulePath = $ENV:BHModulePath
Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue
$Script:manifestTestResult = Test-ModuleManifest -Path $Env:BHPSModuleManifest
$Script:publicScriptFiles = Get-ChildItem -Path "$modulePath\Public" -Recurse
}
It "should cleanly import Module '$ENV:BHProjectName'" {
{ Import-Module -Name $modulePath -Force } | Should -Not -Throw
}
It "should export one or more functions" {
Import-Module -Name $modulePath -Force
Get-Command -Module $moduleName | Measure-Object | Select-Object -ExpandProperty Count | Should -BeGreaterThan 0
}
It "should have a valid PSD1 Module Manifest" {
{ Test-ModuleManifest -Path $ENV:BHPSModuleManifest } | Should -Not -Throw
}
These $Env: variables are set in my build.ps1 file, and are only temporary for the session.

Related

PowerShell - ShouldProcess not working when called from inside Invoke-Command

I am trying to add ShouldProcess logic to a script that deletes files on a remote server to I can use the -WhatIf parameter, but it is returning an error. Here is the function:
function testshouldprocess {
[CmdletBinding(SupportsShouldProcess = $true]
param(
$server
)
invoke-command $server {
Get-ChildItem c:\temp\ | ForEach-Object {
if($pscmdlet.ShouldProcess($Server)) {
remove-item $_.fullname
}
}
}
}
testshouldprocess 'Server1' -WhatIf
When the script is run, it returns error
InvalidOperation: You cannot call a method on a null-valued expression.
as each file passes through the pipeline. If I change the code to
if ($pscmdlet.ShouldProcess($server)) {
invoke-command $server {
Get-ChildItem c:\temp\ | ForEach-Object {
remove-item $_.fullname
}
}
}
it works, but the WhatIf only executes one time for the entire directory listing. If I change the code to
Get-ChildItem \\$server\c$\temp\ | ForEach-Object {
if ($pscmdlet.ShouldProcess($server)) {
remove-item $_.fullname
}
}
it works, but I would would much prefer to use Invoke-Command.
Is ShouldProcess not compatible with Invoke-Command?
Any insight is appreciated.
Hazrelle's answer provides the crucial pointer regarding the need to use the $using: scope in order for the remotely executing script block to have access to values from the caller's scope.
To fully support your scenario - both for -WhatIf and for -Confirm functionality, both of which are implied by turning SupportShouldProces on - you must:
Make your remotely executing script block an advanced one too, with its own [CmdletBinding(SupportsShouldProcess)] attribute above the param() block, and therefore its own $PSCmdlet instance.
Refer to the what-if/confirm-relevant values from the caller's scope via $using:WhatIfPreference and $using:ConfirmPreference
Note that for advanced functions and scripts PowerShell translates the -WhatIf and -Confirm switches into the equivalent preference-variable values, using function-local variables; that is, passing -WhatIf creates a function-local $WhatIfPreference variable with value $true, and passing -Confirm creates a function-local $ConfirmPreference with value High.
function testshouldprocess {
[CmdletBinding(SupportsShouldProcess)]
param(
$server
)
Invoke-Command $server {
[CmdletBinding(SupportsShouldProcess)]
param()
# Use the caller's WhatIf / Confirm preferences.
$WhatIfPreference = $using:WhatIfPreference
$ConfirmPreference = $using:ConfirmPreference
Get-ChildItem c:\temp\ | ForEach-Object {
if ($pscmdlet.ShouldProcess($using:server, "delete file: $($_.FullName)")) {
Remove-Item $_.FullName
}
}
}
}
testshouldprocess 'Server1' -WhatIf
The remote server knows only the command you execute. Not the values from the remote caller. Try with remove-item $_.fullname -Whatif:$($using:pscmdlet.ShouldProcess($server)). See Remote variables
Another option is to specify $WhatIfPreference on the remote server and use that in next statements
$WhatIfPreference = $using:pscmdlet.ShouldProcess($server);
Then remove-item $_.fullname -WhatIf:$WhatIfPreference

Powershell Scripting Variables

I have set some variables in PowerShell. The variables are created at the beginning of my script. However, the values for the variables are being executed at start which in turns gives and error message. Ex:
$checker = get-item -path C:\users\user\desktop\Foldername
$finder = Test-path -Path $checker
if($finder -eq $finder )
{
}
Else
{
Create-Item -Path C:/users/user/desktop -name "Foldername" -itemtype Directory
}
I do know that if I run this it will give me an error because the directory never existed and I can just change the variable order to avoid errors.
My question is that this script is going to be more lines of code than this and I would have to create the variable right when its needed to avoid errors.
How can I use these variables like a regular programming language where the variables are ignored until called upon.
Using Get-Item and checking with Test-Path afterwards is not a good design. Two better ways:
Use Get-Item only and check for $null to check for its existence:
$checker = Get-Item -Path C:\users\user\desktop\Foldername -ErrorAction SilentlyContinue
if ($checker) {
# do something with the existing folder
} else {
Create-Item -Path C:/users/user/desktop -Name "Foldername" -ItemType Directory
}
Use Test-Path only to check for its existence:
if (Test-Path -Path C:\users\user\desktop\Foldername) {
# do something with the existing folder
} else {
Create-Item -Path C:/users/user/desktop -Name "Foldername" -ItemType Directory
}

Dynamically call Export-ModuleMember

Can you tell me why the following code does not export any functions:
# load all functions
$moduleRoot = Split-Path -Path $MyInvocation.MyCommand.Path
$functionFiles = "$moduleRoot\functions\*.ps1" | Resolve-Path
$functionFiles | ForEach-Object { . $_.ProviderPath }
$functionFiles | ForEach-Object { [io.path]::GetFileNameWithoutExtension($_) } | Export-ModuleMember
Or the difference between this:
Not working:
$test = "Cmd1", "Cmd2"
Export-ModuleMember $test
Working:
Export-ModuleMember "Cmd1", "Cmd2"
UPDATE:
When calling "import-module MyCrazyModule", I can see the exported commands (get-module).
However, when calling "get-module -ListAvailable", the exported commands property is empty. (the module is in the PSModulePath)
UPDATE 2: Evidence
$PSModulePath contains D:\powershell
Module: "D:\powershell\MyCrazyModule\MyCrazyModule.psm1"
$test = "Cmd1", "Cmd2"
Export-ModuleMember $test
No exports visible:
However with content
Export-ModuleMember "Cmd1", "Cmd2"
Thank you

Powershell - Remove all properties in a registry item

The following functions actually does the trick:
function Remove-AllItemProperties([String] $path) {
Get-ItemProperty $path | Get-Member -MemberType Properties | Foreach-Object {
if (("PSChildName","PSDrive","PSParentPath","PSPath","PSProvider") -notcontains $_.Name) {
Remove-itemproperty -path $path -Name $_.Name
}
}
}
For example: To delete all typed urls from the registry you can use
Remove-AllItemProperties("HKCU:\SOFTWARE\Microsoft\Internet Explorer\TypedURLs")
My Problems are:
Since im relatively new to Powershell: I wonder if there is not a more beautiful (i.e. compact solution for the problem.
The functions throws an error if the item (registry key) has no properties (Get-Member complains about a missing object).
Thanks for your ideas!
I wonder if there is not a more beautiful (i.e. compact solution for the problem.
I would simply use Remove-ItemProperty -Name *:
function Remove-AllItemProperties
{
[CmdletBinding()]
param([string]$Path)
Remove-ItemProperty -Name * #PSBoundParameters
}
Remove-ItemProperty -Name * will remove any existing value in the registry key at $Path.
The [CmdletBinding()] attribute will automatically add Common Parameters (-Verbose, -Debug, -ErrorAction etc.) to your function.
By splatting $PSBoundParameters to the inner call, you automatically pass these options directly to Remove-ItemProperty
The functions throws an error if the item (registry key) has no properties (Get-Member complains about a missing object).
The above approach won't have that problem

Is it possible to create a user defined PowerShell environment?

I would like to clear a PowerShell session of mostly all alias definitions, except for common aliases like cd, sort, mkdir, ...
After I finished my session, I would like to restore all previous known the aliases.
There is no need to unload modules or to unregister CmdLets. I just want to clear the alias namespace for my session.
I could specify the allowed aliases in a list like this:
$AllowedAliases = #(
"cd", "mkdir", "rm", "rmdir",
"cd", "mkdir", "rm", "rmdir",
"where", "select",
"sort"
)
How can I save the aliases and restore them?
or
How can I start a clean PoSh and load only basic aliases?
What I have tested so far:
The following lines are from my example module called poc.psm1.
$Aliases = #()
function Register-PoC
{ foreach ($a in (Get-Item Alias:))
{ $script:Aliases += $a
Write-Host "$($a.Name) => $($a.ReferencedCommand) ($($a.Visibility))"
Remove-Item "Alias:$($a.Name)" -Force
}
}
function Unregister-PoC
{ foreach ($a in $script:Aliases)
{ Write-Host "$($a.Name) <= $($a.ReferencedCommand)"
if (Test-Path "Alias:$($a.Name)")
{ Write-Host "$($a.Name) exists." }
else
{ Set-Alias -Name $a.Name -Value $a.ReferencedCommand -Scope $a.Visibility }
}
if (Test-Path Alias:quit) { Remove-Item Alias:quit }
Remove-Module PoC
}
Export-ModuleMember -Function 'Register-PoC'
Export-ModuleMember -Function 'Unregister-PoC'
Register-PoC
Set-Alias -Name quit -Value Unregister-PoC -Description "Unload this module." -Scope Global
Usage example:
Import-Module .\poc.psm1
dir Alias:
quit
dir Alias:
Unfortunately, dir Alias: is not empty after invoking my script...
Another thing is, that I should preserve some settings of these aliases, because manual test showed, that dir does not behave like dir in before:
Remove-Item dir
Set-Alias dir Get-Item
dir
Cmdlet Get-Item an der Befehlspipelineposition 1
Geben Sie Werte für die folgenden Parameter an:
Path[0]:
So dir seams to append a default path to Get-Item if non is set to the alias.
Aliases are scoped. When you remove all aliases within a function, the aliases in the global scope aren't affected. Here is code that worked for me (simplifies your code a bit, although I didn't touch unregister-PoC, which could also be simplified I think):
function Register-PoC {
$script:aliases = get-item alias:
remove-item alias:* -force
}
function Unregister-PoC
{ foreach ($a in $script:Aliases)
{ Write-Host "$($a.Name) <= $($a.ReferencedCommand)"
if (Test-Path "Alias:$($a.Name)")
{ Write-Host "$($a.Name) exists." }
else
{ Set-Alias -Name $a.Name -Value $a.ReferencedCommand -Scope $a.Visibility }
}
if (Test-Path Alias:quit) { Remove-Item Alias:quit }
Remove-Module PoC
}
. Register-PoC
Set-Alias -Name quit -Value Unregister-PoC -Description "Unload this module." -Scope Global
Note the dot operator on Register-PoC. You will need to dot source quit to restore the aliases to global scope.
BTW, rather than the foreach loop in Unregister-PoC you could use copy-item.
For your situation, I would recommend using PowerShell profiles. These can be defined per user, per machine, and other situations. You can automatically run functions stored in the profile just by calling the function after it's defined in the profile.
For just your current user on the current machine, see Example 3 here.
New-Item -Path $PROFILE -ItemType File -Force
For other profile options, check out Understanding the Six PowerShell Profiles.
To ignore a profile, you can do that by directly running powershell.exe -NoProfile -NoExit but beware of nested sessions when doing that in another PowerShell session.
For a way to wipe all aliases except your desired list, you can export the aliases and re-import them after wiping all aliases. This can be added to the profile, if desired. Otherwise assign it to a function in the profile and call as needed. Change path if not using the profile folder(or wherever you like to keep things):
$allowedaliases = "cd","dir","rm","rmdir","where","select","sort"
Export-Alias -Path "$((Get-ChildItem $PROFILE).DirectoryName)\aliases.csv" -Name $allowedaliases
Remove-Item Alias:* -Force
Import-Alias -Path "$((Get-ChildItem $PROFILE).DirectoryName)\aliases.csv" -Force
Note: One of the original listed aliases(mkdir) is a function, not an alias, at least in Powershell v5.0. Also added dir into the list since that was mentioned by the OP.