tests/Get-MsecAzureDevOpsPipelineSetting.Tests.ps1

#Requires -Module Pester
#
# Tests for Get-MsecAzureDevOpsPipelineSetting.
#
# Two things worth protecting.
#
# SecretsWithheldFromForks is named for the SAFE state while the API field it comes from,
# enforceNoAccessToSecretsFromForks, is a double negative. Getting that backwards would report an
# organization as safe when it is not, so the mapping is asserted rather than assumed.
#
# OtherSettings must name what has no column. On the first run against a live organization it
# surfaced enforceReferencedRepoScopedToken, which varied between projects and has since been
# promoted - that is the mechanism working, and it only works if unknown keys survive.

BeforeAll {
    $modulePath = Join-Path $PSScriptRoot '..' 'msec.psm1'
    Import-Module $modulePath -Force -ErrorAction Stop
}

AfterAll {
    Remove-Module msec -Force -ErrorAction SilentlyContinue
}

Describe 'Get-MsecAzureDevOpsPipelineSetting' {

    BeforeEach {
        InModuleScope msec {
            $script:MsecSession = @{ TenantId = 't'; ClientId = 'c'; Tokens = @{} }

            function script:Set-SettingsMock {
                param([hashtable] $Settings = @{}, [switch] $Fail)
                $script:MockSettings = [pscustomobject]$Settings
                $script:MockFail = [bool] $Fail
                Mock Invoke-MsecAzureDevOpsRequest -MockWith {
                    if ($Path -eq '_apis/projects') { return @([pscustomobject]@{ id = 'p1'; name = 'Platform' }) }
                    if ($Path -match '/generalsettings$') {
                        if ($script:MockFail) { throw 'Response status code does not indicate success: 403 (Forbidden).' }
                        return $script:MockSettings
                    }
                    return @()
                }
            }
        }
    }

    It 'reports the fork settings separately, not as one verdict' {
        $rows = InModuleScope msec {
            . Set-SettingsMock -Settings @{
                forkProtectionEnabled             = $true
                buildsEnabledForForks             = $true
                enforceNoAccessToSecretsFromForks = $true
                enforceJobAuthScopeForForks       = $false
            }
            Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso'
        }

        # Builds of forks being enabled is only dangerous if secrets are also available to them.
        # Collapsing the two into one column would lose which half to fix.
        $rows.BuildsEnabledForForks    | Should -BeTrue
        $rows.SecretsWithheldFromForks | Should -BeTrue
        $rows.ForkJobAuthScopeLimited  | Should -BeFalse
    }

    It 'maps the double-negative fork secret field to the safe-state name' {
        $rows = InModuleScope msec {
            # enforceNoAccessToSecretsFromForks = false means secrets ARE reachable from forks.
            . Set-SettingsMock -Settings @{ buildsEnabledForForks = $true; enforceNoAccessToSecretsFromForks = $false }
            Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso'
        }

        # Reading this backwards would report the dangerous configuration as the safe one.
        $rows.SecretsWithheldFromForks | Should -BeFalse
    }

    It 'names settings it has no column for, with their values' {
        $rows = InModuleScope msec {
            . Set-SettingsMock -Settings @{
                enforceJobAuthScope = $true
                somethingAddedLastWeek = $true
                anotherNewKnob = $false
            }
            Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso'
        }

        # Azure DevOps adds settings here. A column-per-known-key report loses them silently.
        $rows.OtherSettings | Should -Match 'somethingAddedLastWeek=True'
        $rows.OtherSettings | Should -Match 'anotherNewKnob=False'
        # Something with a column of its own must not also appear.
        $rows.OtherSettings | Should -Not -Match 'enforceJobAuthScope'
    }

    It 'reports the job token scoping settings' {
        $rows = InModuleScope msec {
            . Set-SettingsMock -Settings @{
                enforceJobAuthScope              = $true
                enforceJobAuthScopeForReleases   = $false
                enforceReferencedRepoScopedToken = $true
            }
            Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso'
        }

        $rows.JobAuthScopeLimited             | Should -BeTrue
        $rows.JobAuthScopeLimitedForReleases  | Should -BeFalse
        $rows.ReferencedRepoScopedToken       | Should -BeTrue
    }

    It 'reports a project whose settings could not be read' {
        $warnings = @()
        $rows = InModuleScope msec {
            . Set-SettingsMock -Fail
            Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso'
        } -WarningVariable warnings -WarningAction SilentlyContinue

        # A project that refused must not be absent without explanation - and must certainly not
        # read as configured safely.
        @($rows).Count | Should -Be 0
        ($warnings -join ' ') | Should -Match 'not as configured safely'
    }

    It 'throws a clear error when not connected' {
        InModuleScope msec {
            $script:MsecSession = $null
            { Get-MsecAzureDevOpsPipelineSetting -Organization 'contoso' } | Should -Throw '*Connect-Msec*'
        }
    }
}