tests/Readme.Tests.ps1

#Requires -Module Pester
#
# The README's Commands section is the only index of what this module does, and it drifts
# silently: a new command ships, the help audit passes, the docs page generates, and the one
# place a person actually browses never mentions it. Grant-MsecAzureDevOpsPermission reached a
# release candidate that way - it appeared in the setup examples, so a search for its name found
# it, while the command list did not have it at all.

BeforeAll {
    Import-Module (Join-Path $PSScriptRoot '..' 'Msec.psm1') -Force -ErrorAction Stop
    $script:RepoRoot = Join-Path $PSScriptRoot '..' '..'
    $script:Readme   = Join-Path $script:RepoRoot 'README.md'

    # Only bullet links of the documented form count. A mention in prose or an example is not
    # an index entry - that distinction is exactly what hid the gap.
    $lines = Get-Content $script:Readme
    $start = ($lines | Select-String -Pattern '^## Commands$').LineNumber
    $after = $lines[$start..($lines.Count - 1)]
    $end   = ($after | Select-String -Pattern '^## ').LineNumber | Select-Object -First 1
    $block = if ($end) { $after[0..($end - 2)] } else { $after }

    $script:Listed = @($block | ForEach-Object {
        if ($_ -match '^- \[([A-Za-z]+-Msec[A-Za-z]*)\]\(\./docs/commands/') { $Matches[1] }
    } | Sort-Object -Unique)

    $script:Exported = @((Get-Command -Module msec).Name)
}
AfterAll { Remove-Module msec -Force -ErrorAction SilentlyContinue }

Describe 'README command index' {

    It 'lists every exported command' {
        $missing = @($script:Exported | Where-Object { $_ -notin $script:Listed })
        $missing -join ', ' | Should -BeNullOrEmpty
    }

    It 'lists nothing the module does not export' {
        # A renamed or removed command leaves a link that 404s on the docs site.
        $ghost = @($script:Listed | Where-Object { $_ -notin $script:Exported })
        $ghost -join ', ' | Should -BeNullOrEmpty
    }

    It 'points every link at a docs page that exists' {
        $broken = @($script:Listed | Where-Object {
            -not (Test-Path (Join-Path $script:RepoRoot "docs/commands/$_.md"))
        })
        $broken -join ', ' | Should -BeNullOrEmpty
    }

    It 'has no heading that swallowed its first bullet' {
        # '### Entra ID- [Get-Msec...' renders as neither a heading nor a list item, and is what
        # a string-replace insert produces when it eats the newline.
        $offenders = @(Get-Content $script:Readme | Where-Object { $_ -match '^#{1,4} .+- \[' })
        $offenders -join ' | ' | Should -BeNullOrEmpty
    }
}