Public/Test-SqlSpnPlan.ps1

# =============================================================================
# Script : Test-SqlSpnPlan.ps1
# Author : Keith Ramsey
# =============================================================================
# Change Log
# -----------------------------------------------------------------------------
# 2026-05-09 Keith Ramsey Phase 2 release polish - DR-202 standard header applied.
# 2026-05-13 Keith Ramsey Fix operator-precedence bug at the missing-fields check;
# '-not $expr -contains $x' parses as '($false) -contains $x'
# so the Invalid branch was dead code. Replace with -notcontains.
# =============================================================================
function Test-SqlSpnPlan {
    <#
    .SYNOPSIS
        Validates a plan object's shape before execution.
    .DESCRIPTION
        Confirms the plan carries the expected fields (AccountDn, ProposedSpns, Role)
        and that ProposedSpns is non-empty. Forest-wide duplicate detection runs
        inside Invoke-SqlSpnExecutionEngine (setspn -Q with optional -T) where it
        can surface conflicts as structured warnings; this command is the cheap
        local-shape check that runs before the expensive AD round-trip.
    .PARAMETER SpnPlan
        A plan object produced by New-SqlSpnPlan.
    .EXAMPLE
        $plan | Test-SqlSpnPlan
    .EXAMPLE
        $result = Test-SqlSpnPlan -SpnPlan $plan
        if ($result.Status -ne 'Clear') { throw "Plan invalid: $($result.Reason)" }
    .OUTPUTS
        PSCustomObject with Status (Clear / Invalid / Empty), Reason (when not Clear),
        and Spns (when Clear).
    .NOTES
        Status meanings:
          Clear - shape is valid and plan is non-empty; safe to pass to engine.
          Invalid - missing one or more required fields; plan was not built correctly.
          Empty - plan has zero proposed SPNs; nothing to register.
    #>

    [CmdletBinding()]
    param([Parameter(Mandatory=$true, ValueFromPipeline=$true)]$SpnPlan)
    process {
        $missing = @()
        foreach ($field in 'AccountDn', 'ProposedSpns', 'Role') {
            if ($SpnPlan.PSObject.Properties.Name -notcontains $field) { $missing += $field }
        }
        if ($missing.Count -gt 0) {
            return [PSCustomObject]@{ Status = 'Invalid'; Reason = "Missing fields: $($missing -join ', ')" }
        }
        if (-not $SpnPlan.ProposedSpns -or $SpnPlan.ProposedSpns.Count -eq 0) {
            return [PSCustomObject]@{ Status = 'Empty'; Reason = 'No SPNs proposed' }
        }
        return [PSCustomObject]@{ Status = 'Clear'; Spns = $SpnPlan.ProposedSpns }
    }
}