tests/Unit/Public/Grant-sqmVolumeAccess.Tests.ps1

#Requires -Modules Pester
<#
.SYNOPSIS
    Unit Tests fuer Grant-sqmVolumeAccess. Kein Mock - echtes icacls auf einem Ordnerbaum im TestDrive
    (unterbrochene Vererbung, Junction, geschuetzte Datei). Laeuft ohne erhoehte Sitzung, weil der
    Testbenutzer Besitzer der Ordner ist. Der TestDrive liegt auf dem Systemlaufwerk; die Sperre wird
    dafuer ueber $env:SystemDrive umgangen.
#>


BeforeAll {
    . "$PSScriptRoot\..\..\..\tests\TestHelpers.ps1"
    Import-sqmTestModule
    $script:realSystemDrive = $env:SystemDrive

    function script:Get-SidAces
    {
        param ([string]$LiteralPath, [string]$Sid)
        @((Get-Acl -LiteralPath $LiteralPath).Access |
            Where-Object { $_.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value -eq $Sid })
    }

    function script:New-Tree
    {
        $root = Join-Path $TestDrive ([guid]::NewGuid().ToString('N').Substring(0, 8))
        $tree = Join-Path $root 'Tree'
        New-Item -ItemType Directory "$tree\A\B", "$tree\Prot\Sub\Prot2\Deep", "$root\Outside" -Force | Out-Null
        'x' | Set-Content "$tree\A\B\x.bak"
        'y' | Set-Content "$tree\Prot\Sub\Prot2\Deep\y.mdf"
        'p' | Set-Content "$tree\A\pf.txt"
        & icacls "$tree\Prot" /inheritance:d | Out-Null
        & icacls "$tree\Prot\Sub\Prot2" /inheritance:d | Out-Null
        & icacls "$tree\A\pf.txt" /inheritance:d | Out-Null
        & cmd.exe /c mklink /J "$tree\Junction" "$root\Outside" | Out-Null
        [PSCustomObject]@{ Tree = $tree; Outside = "$root\Outside" }
    }
}

AfterAll {
    $env:SystemDrive = $script:realSystemDrive
    if (Get-Module sqmSQLTool) { Remove-Module sqmSQLTool -Force }
    $env:SQMSQLTOOL_SKIP_AUTO_UPDATE = $null
}

Describe 'Grant-sqmVolumeAccess' {
    BeforeEach {
        Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
        $env:SystemDrive = 'Z:'
    }
    AfterEach {
        $env:SystemDrive = $script:realSystemDrive
    }

    Context 'Parameter' {
        It 'verlangt -Principal oder -Account' {
            { Grant-sqmVolumeAccess -Path $TestDrive } | Should -Throw '*-Principal oder -Account*'
        }
        It 'bricht bei nicht aufloesbarem Konto ab' {
            { Grant-sqmVolumeAccess -Path $TestDrive -Account 'GIBTS-NICHT\Niemand' } | Should -Throw '*nicht aufloesen*'
        }
        It 'bietet nur die beiden bekannten Gruppen an' {
            (Get-Command Grant-sqmVolumeAccess).Parameters['Principal'].Attributes.ValidValues |
                Should -Be @('AuthenticatedUsers', 'Administrators')
        }
    }

    Context 'Ziele' {
        It 'fasst das Systemlaufwerk nie an' {
            $env:SystemDrive = $script:realSystemDrive
            $r = Grant-sqmVolumeAccess -Path $TestDrive -Principal AuthenticatedUsers -ExcludeDrive @() -Confirm:$false
            $r.Action | Should -Be 'Skipped'
            $r.Message | Should -BeLike '*Systemlaufwerk*'
        }
        It 'ueberspringt ausgeschlossene und fehlende Laufwerke' {
            $r = Grant-sqmVolumeAccess -Path 'S:\', 'Q:\gibts\nicht' -Principal AuthenticatedUsers -Confirm:$false
            $r.Action | Should -Be @('Skipped', 'Skipped')
            $r[0].Message | Should -BeLike '*ausgeschlossen*'
        }
    }

    Context 'Berechtigen' {
        BeforeAll {
            $env:SystemDrive = 'Z:'
            $script:t = New-Tree
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            $script:whatIf = Grant-sqmVolumeAccess -Path $script:t.Tree -Principal AuthenticatedUsers -ExcludeDrive @() -WhatIf
            $script:first = Grant-sqmVolumeAccess -Path $script:t.Tree -Principal AuthenticatedUsers -ExcludeDrive @() -Confirm:$false
            $script:second = Grant-sqmVolumeAccess -Path $script:t.Tree -Principal AuthenticatedUsers -ExcludeDrive @() -Confirm:$false
            $env:SystemDrive = $script:realSystemDrive
        }

        It '-WhatIf meldet Wurzel und beide Ordner mit unterbrochener Vererbung' {
            $script:whatIf.Action | Should -Be @('WhatIf', 'WhatIf', 'WhatIf')
            ($script:whatIf | Where-Object Scope -eq 'ProtectedFolder').Path | Split-Path -Leaf | Sort-Object |
                Should -Be @('Prot', 'Prot2')
        }
        It 'setzt Wurzel und geschuetzte Ordner und liest nach' {
            $script:first.Action | Should -Be @('Granted', 'Granted', 'Granted')
        }
        It 'erreicht alles unterhalb, auch unter verschachtelt geschuetzten Ordnern' {
            foreach ($p in 'A', 'A\B', 'A\B\x.bak', 'Prot', 'Prot\Sub', 'Prot\Sub\Prot2', 'Prot\Sub\Prot2\Deep', 'Prot\Sub\Prot2\Deep\y.mdf')
            {
                (Get-SidAces (Join-Path $script:t.Tree $p) 'S-1-5-11').Count | Should -Be 1 -Because $p
            }
        }
        It 'hinterlaesst auf normalen Unterordnern keine doppelte explizite ACE' {
            $aces = Get-SidAces (Join-Path $script:t.Tree 'A\B') 'S-1-5-11'
            $aces.Count | Should -Be 1
            $aces[0].IsInherited | Should -BeTrue
        }
        It 'folgt keiner Junction' {
            (Get-SidAces $script:t.Outside 'S-1-5-11').Count | Should -Be 0
        }
        It 'laesst geschuetzte Dateien ohne -IncludeFiles aus' {
            (Get-SidAces (Join-Path $script:t.Tree 'A\pf.txt') 'S-1-5-11').Count | Should -Be 0
        }
        It 'aendert beim zweiten Lauf nichts' {
            $script:second.Action | Should -Be @('AlreadyPresent', 'AlreadyPresent', 'AlreadyPresent')
        }
        It 'berechtigt mit -IncludeFiles auch geschuetzte Dateien, mehrere Principals, -Account per SID' {
            $env:SystemDrive = 'Z:'
            $r = Grant-sqmVolumeAccess -Path $script:t.Tree -Principal AuthenticatedUsers -Account 'S-1-5-32-545' -Rights Read -IncludeFiles -ExcludeDrive @() -Confirm:$false
            $file = $r | Where-Object Scope -eq 'ProtectedFile'
            ($file | Where-Object Sid -eq 'S-1-5-32-545').Action | Should -Be 'Granted'
            # RX deckt Read ab
            ($file | Where-Object Sid -eq 'S-1-5-11').Action | Should -Be 'Granted'
            ($r | Where-Object { $_.Scope -eq 'Root' -and $_.Sid -eq 'S-1-5-11' }).Action | Should -Be 'AlreadyPresent'
            (Get-SidAces (Join-Path $script:t.Tree 'A\pf.txt') 'S-1-5-32-545').Count | Should -Be 1
        }
    }

    Context 'Vererbung aus' {
        BeforeAll {
            $env:SystemDrive = 'Z:'
            $script:o = New-Tree
            Mock -ModuleName sqmSQLTool Invoke-sqmLogging { }
            $script:off = Grant-sqmVolumeAccess -Path $script:o.Tree -Principal AuthenticatedUsers -Inheritance Off -IncludeFiles -ExcludeDrive @() -Confirm:$false -WarningAction SilentlyContinue
            $env:SystemDrive = $script:realSystemDrive
        }

        It 'setzt nur die Wurzel, sucht keine Unterordner' {
            @($script:off).Count | Should -Be 1
            $script:off.Scope | Should -Be 'Root'
            $script:off.Action | Should -Be 'Granted'
            $script:off.Inheritance | Should -Be 'Off'
        }
        It 'setzt die ACE ohne Vererbungsflags' {
            $aces = Get-SidAces $script:o.Tree 'S-1-5-11'
            $aces.Count | Should -Be 1
            $aces[0].InheritanceFlags | Should -Be ([System.Security.AccessControl.InheritanceFlags]::None)
        }
        It 'aendert nichts unterhalb' {
            foreach ($p in 'A', 'A\B\x.bak', 'Prot', 'Prot\Sub')
            {
                (Get-SidAces (Join-Path $script:o.Tree $p) 'S-1-5-11').Count | Should -Be 0 -Because $p
            }
        }
        It 'zaehlt eine nicht vererbbare ACE bei -Inheritance On nicht als vorhanden' {
            $env:SystemDrive = 'Z:'
            $r = Grant-sqmVolumeAccess -Path $script:o.Tree -Principal AuthenticatedUsers -ExcludeDrive @() -Confirm:$false
            ($r | Where-Object Scope -eq 'Root').Action | Should -Be 'Granted'
            (Get-SidAces (Join-Path $script:o.Tree 'A\B') 'S-1-5-11').Count | Should -Be 1
        }
        It 'meldet AlreadyPresent fuer Off, wenn eine vererbbare ACE schon da ist' {
            $env:SystemDrive = 'Z:'
            $r = Grant-sqmVolumeAccess -Path $script:o.Tree -Principal AuthenticatedUsers -Inheritance Off -ExcludeDrive @() -Confirm:$false
            $r.Action | Should -Be 'AlreadyPresent'
        }
    }

    Context 'Fehler' {
        It 'meldet Failed, wenn icacls trotz Exitcode 0 nicht setzen durfte' {
            $env:SystemDrive = 'Z:'
            $root = Join-Path $TestDrive 'fail'
            New-Item -ItemType Directory "$root\Locked" -Force | Out-Null
            # OWNER RIGHTS nur Lesen: der Besitzer verliert sein implizites WRITE_DAC
            & icacls "$root\Locked" /inheritance:r /grant '*S-1-3-4:(OI)(CI)R' '*S-1-5-32-544:(OI)(CI)R' | Out-Null
            try
            {
                # Probe: greift die Sperre ueberhaupt? Auf dem GitHub-Runner (erhoehter Admin) setzt
                # icacls die ACE trotzdem - dann ist der Fehlerpfad hier nicht herstellbar.
                & icacls "$root\Locked" /grant '*S-1-5-19:R' 2>&1 | Out-Null
                if (Get-SidAces "$root\Locked" 'S-1-5-19')
                {
                    Set-ItResult -Skipped -Because 'ACL-Sperre greift in diesem Kontext nicht (erhoehter Prozess darf WRITE_DAC trotzdem)'
                    return
                }
                $r = Grant-sqmVolumeAccess -Path $root -Principal AuthenticatedUsers -ExcludeDrive @() -Confirm:$false
                ($r | Where-Object Scope -eq 'Root').Action | Should -Be 'Granted'
                $locked = $r | Where-Object Scope -eq 'ProtectedFolder'
                $locked.Action | Should -Be 'Failed'
                $locked.Message | Should -BeLike 'icacls:*'
            }
            finally
            {
                # Leerer Ordner: ueber FILE_DELETE_CHILD des Elternordners loeschbar
                [System.IO.Directory]::Delete("$root\Locked")
            }
        }
    }
}