Public/Tests/New-FunctionTemplate.Tests.ps1
|
BeforeDiscovery { $HasScriptAnalyzer = [bool](Get-Module -ListAvailable -Name PSScriptAnalyzer) } BeforeAll { $env:TCS_CONFIG_ROOT = Join-Path -Path $TestDrive -ChildPath 'config' $env:TCS_SKIP_UPDATE_CHECK = '1' $env:TCS_TELEMETRY_OPTOUT = '1' $ModuleRoot = Split-Path -Path (Split-Path -Path $PSScriptRoot -Parent) -Parent Import-Module -Name (Join-Path -Path $ModuleRoot -ChildPath 'tcs.utils.psd1') -Force } AfterAll { Remove-Module -Name tcs.utils -Force -ErrorAction SilentlyContinue } Describe 'New-FunctionTemplate' { BeforeAll { function Get-ParseErrorCount { param([string]$Path) $tokens = $null $errors = $null $null = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$tokens, [ref]$errors) @($errors).Count } $SettingsPath = Join-Path -Path (Split-Path -Path (Split-Path -Path $ModuleRoot -Parent) -Parent) -ChildPath 'PSScriptAnalyzerSettings.psd1' } BeforeEach { $Target = Join-Path -Path $TestDrive -ChildPath ([guid]::NewGuid().ToString('N')) $null = New-Item -Path $Target -ItemType Directory } Context 'With telemetry (default)' { BeforeEach { New-FunctionTemplate -Name 'Get-Widget' -Path $Target $File = Join-Path -Path $Target -ChildPath 'Get-Widget.ps1' $Content = Get-Content -Path $File -Raw } It 'Creates a file that parses without errors' { Test-Path -Path $File | Should -BeTrue Get-ParseErrorCount -Path $File | Should -Be 0 } It 'Uses the function name and the current telemetry pattern' { $Content | Should -Match 'function Get-Widget \{' $Content | Should -Not -Match '###TEMPLATE' $Content | Should -Match ([regex]::Escape('$TelemetryArgs = @{')) $Content | Should -Match ([regex]::Escape('Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer')) $Content | Should -Match ([regex]::Escape('Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_')) $Content | Should -Match ([regex]::Escape('Invoke-TelemetryCollection @TelemetryArgs -Stage End')) } It 'Does not contain obsolete telemetry code' { $Content | Should -Not -Match 'NOTYETDEFINED' $Content | Should -Not -Match 'TelmetryArgs' $Content | Should -Not -Match 'Minimal' $Content | Should -Not -Match 'Get-ModuleConfig' $Content | Should -Not -Match 'BasicTelemetry' } It 'Sends Start and End when the generated function succeeds' { . $File Mock Invoke-TelemetryCollection { } Get-Widget -Name 'a' Should -Invoke Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'Start' } Should -Invoke Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'End' -and -not $Failed } } It 'Sends End with -Failed and rethrows when the generated function fails' { $failing = $Content.Replace("Write-Verbose `"Processing '`$Name'.`"", "throw 'boom'") Set-Content -Path $File -Value $failing . $File Mock Invoke-TelemetryCollection { } { Get-Widget -Name 'a' } | Should -Throw 'boom' Should -Invoke Invoke-TelemetryCollection -Times 1 -Exactly -ParameterFilter { $Stage -eq 'End' -and $Failed -eq $true } Should -Invoke Invoke-TelemetryCollection -Times 0 -Exactly -ParameterFilter { $Stage -eq 'End' -and -not $Failed } } It 'Has no PSScriptAnalyzer findings with the repository settings' -Skip:(-not $HasScriptAnalyzer) { Invoke-ScriptAnalyzer -Path $File -Settings $SettingsPath | Should -BeNullOrEmpty } } Context 'Without telemetry' { It 'Creates a parseable function without telemetry calls' { New-FunctionTemplate -Name 'Set-Widget' -Path $Target -ExcludeTelemetryCollection $file = Join-Path -Path $Target -ChildPath 'Set-Widget.ps1' Get-ParseErrorCount -Path $file | Should -Be 0 $content = Get-Content -Path $file -Raw $content | Should -Match 'function Set-Widget \{' $content | Should -Not -Match 'Invoke-TelemetryCollection' $content | Should -Not -Match '###TEMPLATE' } It 'Has no PSScriptAnalyzer findings with the repository settings' -Skip:(-not $HasScriptAnalyzer) { New-FunctionTemplate -Name 'Set-Widget' -Path $Target -ExcludeTelemetryCollection Invoke-ScriptAnalyzer -Path (Join-Path -Path $Target -ChildPath 'Set-Widget.ps1') -Settings $SettingsPath | Should -BeNullOrEmpty } } Context 'State-changing verbs' { It 'Adds SupportsShouldProcess and a ShouldProcess check for <Name>' -TestCases @( @{ Name = 'Set-Widget'; Telemetry = $false } @{ Name = 'Remove-Widget'; Telemetry = $true } ) { New-FunctionTemplate -Name $Name -Path $Target -ExcludeTelemetryCollection:(-not $Telemetry) $file = Join-Path -Path $Target -ChildPath "$Name.ps1" Get-ParseErrorCount -Path $file | Should -Be 0 $content = Get-Content -Path $file -Raw $content | Should -Match ([regex]::Escape('[CmdletBinding(SupportsShouldProcess = $true)]')) $content | Should -Match ([regex]::Escape('$PSCmdlet.ShouldProcess($Name')) . $file Mock Invoke-TelemetryCollection { } { & $Name -Name 'a' -WhatIf } | Should -Not -Throw } It 'Does not add ShouldProcess for read-only verbs' { New-FunctionTemplate -Name 'Get-Widget' -Path $Target Get-Content -Path (Join-Path -Path $Target -ChildPath 'Get-Widget.ps1') -Raw | Should -Not -Match 'SupportsShouldProcess' } It 'Has no PSScriptAnalyzer findings for a Remove- function' -Skip:(-not $HasScriptAnalyzer) { New-FunctionTemplate -Name 'Remove-Widget' -Path $Target Invoke-ScriptAnalyzer -Path (Join-Path -Path $Target -ChildPath 'Remove-Widget.ps1') -Settings $SettingsPath | Should -BeNullOrEmpty } } Context 'Behaviour' { It 'Creates one file per name, including piped names' { 'Get-A', 'Get-B' | New-FunctionTemplate -Path $Target (Get-ChildItem -Path $Target -Filter '*.ps1').Name | Sort-Object | Should -Be @('Get-A.ps1', 'Get-B.ps1') } It 'Writes nothing to the pipeline unless -PassThru is used' { New-FunctionTemplate -Name 'Get-Quiet' -Path $Target | Should -BeNullOrEmpty $result = New-FunctionTemplate -Name 'Get-Loud' -Path $Target -PassThru $result | Should -BeOfType [System.IO.FileInfo] $result.Name | Should -Be 'Get-Loud.ps1' } It 'Overwrites an existing file with -Force' { $file = Join-Path -Path $Target -ChildPath 'Get-Old.ps1' Set-Content -Path $file -Value 'old' New-FunctionTemplate -Name 'Get-Old' -Path $Target -Force Get-Content -Path $file -Raw | Should -Match 'function Get-Old' } It 'Creates nothing with -WhatIf' { New-FunctionTemplate -Name 'Get-Nothing' -Path $Target -WhatIf Test-Path -Path (Join-Path -Path $Target -ChildPath 'Get-Nothing.ps1') | Should -BeFalse } It 'Rejects a path that is not a folder' { { New-FunctionTemplate -Name 'Get-X' -Path (Join-Path -Path $Target -ChildPath 'missing') } | Should -Throw } } } |