tests/Test-DuplicateParameterBinding.Tests.ps1

<#
.SYNOPSIS
    Pester-Tests fuer Tools/Test-DuplicateParameterBinding.ps1.
 
.DESCRIPTION
    Jeder Fall schreibt ein kleines Fixture-Skript nach TestDrive und laesst den Checker darueber
    laufen. Die Fixtures stehen hier nur als Here-Strings, damit der Checker beim Lauf ueber das
    Repository selbst keine Treffer in dieser Datei meldet.
#>


BeforeAll {
    $script:Checker = Join-Path (Split-Path -Parent $PSScriptRoot) 'Tools\Test-DuplicateParameterBinding.ps1'
    $script:FixtureCount = 0

    function Invoke-DuplicateBindingCheck
    {
        param([string]$Code)
        $script:FixtureCount++
        $folder = Join-Path $TestDrive "case$($script:FixtureCount)"
        $null = New-Item -ItemType Directory -Path $folder -Force
        Set-Content -Path (Join-Path $folder 'Fixture.ps1') -Value $Code -Encoding UTF8
        $findings = @(& $script:Checker -Root $folder -Quiet -PassThru)
        [pscustomobject]@{ ExitCode = $LASTEXITCODE; Findings = $findings }
    }
}

Describe 'Test-DuplicateParameterBinding' {

    Context 'Nicht-Common-Parameter (sqmSQLTool 1.9.178.0)' {

        It 'Findet @connParams mit Database plus explizites -Database' {
            $result = Invoke-DuplicateBindingCheck @'
function Install-Something {
    $connParams = @{
        SqlInstance = $SqlInstance
        SqlCredential = $SqlCredential
        Database = $Database
    }
    Invoke-DbaQuery @connParams -Query 'SELECT 1' -ErrorAction Stop
    Invoke-DbaQuery @connParams -Database 'master' -Query 'SELECT 1' -ErrorAction Stop
}
'@

            $result.ExitCode | Should -Be 1
            $result.Findings.Count | Should -Be 1
            $result.Findings[0].Parameter | Should -Be 'Database'
            $result.Findings[0].Line | Should -Be 8
            $result.Findings[0].Splat | Should -Be '@connParams'
        }

        It 'Akzeptiert die Korrektur mit Clone und ueberschriebenem Schluessel' {
            $result = Invoke-DuplicateBindingCheck @'
function Install-Something {
    $connParams = @{ SqlInstance = $SqlInstance; Database = $Database }
    $masterParams = $connParams.Clone()
    $masterParams['Database'] = 'master'
    Invoke-DbaQuery @masterParams -Query 'SELECT 1'
}
'@

            $result.ExitCode | Should -Be 0
            $result.Findings.Count | Should -Be 0
        }

        It 'Findet Schluessel aus nachtraeglicher Index-, Member- und Add-Zuweisung' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $a = @{ SqlInstance = $s }
    $a['Database'] = $db
    Invoke-DbaQuery @a -Database 'master'
    $b = @{ SqlInstance = $s }
    $b.Database = $db
    Invoke-DbaQuery @b -Database 'master'
    $c = @{ SqlInstance = $s }
    $c.Add('Database', $db)
    Invoke-DbaQuery @c -Database 'master'
    $d = @{ SqlInstance = $s }
    $d += @{ Database = $db }
    Invoke-DbaQuery @d -Database 'master'
}
'@

            $result.Findings.Count | Should -Be 4
        }

        It 'Uebernimmt die Schluessel einer Kopie (Clone und Zuweisung)' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $p = [ordered]@{ SqlInstance = $s; Database = $db }
    $q = $p.Clone()
    $r = $p
    Invoke-DbaQuery @q -Database 'master'
    Invoke-DbaQuery @r -Database 'master'
}
'@

            $result.Findings.Count | Should -Be 2
        }

        It 'Ignoriert Schluessel, die vor dem Aufruf entfernt werden' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $p = @{ SqlInstance = $s; Database = $db }
    if ($PSBoundParameters.ContainsKey('Database')) { [void]$p.Remove('Database') }
    Invoke-DbaQuery @p -Database 'master'
}
'@

            $result.Findings.Count | Should -Be 0
        }

        It 'Ignoriert Schluessel, die erst nach dem Aufruf hinzukommen' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $p = @{ SqlInstance = $s }
    Invoke-DbaQuery @p -Database 'master'
    $p['Database'] = $db
}
'@

            $result.Findings.Count | Should -Be 0
        }

        It 'Ignoriert eine unbedingt ueberschriebene Hashtable und .Clear()' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $p = @{ SqlInstance = $s; Database = $db }
    $p = @{ SqlInstance = $s }
    Invoke-DbaQuery @p -Database 'master'
    $q = @{ Database = $db }
    $q.Clear()
    Invoke-DbaQuery @q -Database 'master'
}
'@

            $result.Findings.Count | Should -Be 0
        }

        It 'Trennt gleichnamige Variablen verschiedener Funktionen' {
            $result = Invoke-DuplicateBindingCheck @'
function First-Thing {
    $p = @{ SqlInstance = $s; Database = $db }
    Invoke-DbaQuery @p -Query 'SELECT 1'
}
function Second-Thing {
    $p = @{ SqlInstance = $s }
    Invoke-DbaQuery @p -Database 'master'
}
'@

            $result.Findings.Count | Should -Be 0
        }

        It 'Wertet bedingte Zuweisungen als moeglich, aber nicht im Nachbarzweig' {
            $result = Invoke-DuplicateBindingCheck @'
function Test-It {
    $p = @{ SqlInstance = $s }
    if ($db) { $p.Database = $db }
    Invoke-DbaQuery @p -Database 'master'
 
    if ($x) {
        $q = @{ SqlInstance = $s; Database = $db }
    }
    else {
        $q = @{ SqlInstance = $s }
        Invoke-DbaQuery @q -Database 'master'
    }
}
'@

            $result.Findings.Count | Should -Be 1
            $result.Findings[0].Line | Should -Be 4
        }
    }

    Context 'Abkuerzungen und Aliase' {

        It 'Findet Common Parameter ueber Alias und Praefix' {
            $result = Invoke-DuplicateBindingCheck @'
$p = @{ Path = 'C:\'; ErrorAction = 'Stop' }
Get-Item @p -ea SilentlyContinue
Get-Item @p -ErrorAct SilentlyContinue
'@

            $result.Findings.Count | Should -Be 2
            $result.Findings | ForEach-Object { $_.Parameter | Should -Be 'ErrorAction' }
        }

        It 'Wertet bei fremden Befehlen keinen Praefix aus (-Query neben QueryTimeout)' {
            $result = Invoke-DuplicateBindingCheck @'
$p = @{ SqlInstance = $s; QueryTimeout = 60 }
Invoke-DbaQuery @p -Query 'SELECT 1'
'@

            $result.Findings.Count | Should -Be 0
        }

        It 'Loest Praefix und Alias bei Funktionen aus dem geprueften Verzeichnis auf' {
            $result = Invoke-DuplicateBindingCheck @'
function Get-Thing {
    [CmdletBinding()]
    param(
        [Alias('DbName')]
        [string]$DatabaseName,
        [string]$Database
    )
}
$p = @{ DatabaseName = 'a' }
Get-Thing @p -DatabaseN 'b'
Get-Thing @p -DbName 'b'
Get-Thing @p -Database 'b'
'@

            $result.Findings.Count | Should -Be 2
            $result.Findings | ForEach-Object { $_.Parameter | Should -Be 'DatabaseName' }
        }
    }

    Context 'Verhalten unter Windows PowerShell 5.1' {

        It 'Bestaetigt, dass 5.1 das gefundene Muster wirklich ablehnt und die Korrektur annimmt' -Skip:(-not (Get-Command powershell.exe -ErrorAction SilentlyContinue)) {
            $script51 = Join-Path $TestDrive 'Ps51Check.ps1'
            Set-Content -Path $script51 -Encoding UTF8 -Value @'
function Invoke-Probe { [CmdletBinding()] param($SqlInstance, $Database) $Database }
$connParams = @{ SqlInstance = 'x'; Database = 'userdb' }
try { Invoke-Probe @connParams -Database 'master' -ErrorAction Stop; 'DUPLICATE:accepted' }
catch { 'DUPLICATE:' + $_.FullyQualifiedErrorId }
$masterParams = $connParams.Clone()
$masterParams['Database'] = 'master'
'FIXED:' + (Invoke-Probe @masterParams)
'@

            $output = & powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $script51
            $output | Should -Contain 'FIXED:master'
            ($output | Where-Object { $_ -like 'DUPLICATE:*' }) | Should -BeLike 'DUPLICATE:ParameterAlreadyBound*'
        }
    }
}