AnthonyCMartin.BicepTesting.psm1

Set-StrictMode -Version Latest
$script:BicepTestSessionType = $null
$script:LiveBicepTestSessionType = $null

function Import-BicepTestAssembly {
    if ($script:BicepTestSessionType -and $script:LiveBicepTestSessionType) {
        return
    }

    $loadedAssembly = [AppDomain]::CurrentDomain.GetAssemblies() |
        Where-Object { $_.GetName().Name -eq 'AnthonyCMartin.BicepTesting' } |
        Select-Object -First 1
    if ($loadedAssembly) {
        $script:BicepTestSessionType = $loadedAssembly.GetType('AnthonyCMartin.BicepTesting.BicepTestSession', $true)
        $script:LiveBicepTestSessionType = $loadedAssembly.GetType('AnthonyCMartin.BicepTesting.LiveBicepTestSession', $true)
        return
    }

    $libraryPath = Join-Path $PSScriptRoot 'lib/net10.0'
    $assemblyPath = Join-Path $libraryPath 'AnthonyCMartin.BicepTesting.dll'
    if (-not (Test-Path -LiteralPath $assemblyPath)) {
        throw "The BicepTest runtime has not been built. Run packages/powershell/build.ps1."
    }

    Get-ChildItem -LiteralPath $libraryPath -Filter '*.dll' |
        Where-Object Name -ne 'AnthonyCMartin.BicepTesting.dll' |
        ForEach-Object {
            try {
                [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromAssemblyPath($_.FullName) | Out-Null
            }
            catch [System.IO.FileLoadException] {
                # The PowerShell host may already provide the same framework assembly.
            }
        }
    $assembly = [System.Runtime.Loader.AssemblyLoadContext]::Default.LoadFromAssemblyPath($assemblyPath)
    $script:BicepTestSessionType = $assembly.GetType('AnthonyCMartin.BicepTesting.BicepTestSession', $true)
        $script:LiveBicepTestSessionType = $assembly.GetType('AnthonyCMartin.BicepTesting.LiveBicepTestSession', $true)
}

function New-BicepTestSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $BicepVersion
    )

    Import-BicepTestAssembly
    $method = $script:BicepTestSessionType.GetMethod('CreateAsync')
    $task = $method.Invoke($null, @($BicepVersion, [Threading.CancellationToken]::None))
    $task.GetAwaiter().GetResult()
}

function New-BicepLiveTestSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [ValidateNotNullOrEmpty()]
        [string] $BicepVersion,

        [Parameter(Mandatory)]
        [ValidateNotNull()]
        [object] $Credential
    )

    Import-BicepTestAssembly
    $method = $script:LiveBicepTestSessionType.GetMethod('CreateAsync')
    $task = $method.Invoke($null, @($BicepVersion, $Credential, [Threading.CancellationToken]::None))
    $task.GetAwaiter().GetResult()
}

function ConvertTo-BicepJsonElementDictionary {
    param(
        [AllowNull()]
        [hashtable] $ParameterOverrides
    )

    $overrides = [Collections.Generic.Dictionary[string, Text.Json.JsonElement]]::new()
    if ($null -eq $ParameterOverrides) {
        return $overrides
    }

    foreach ($item in $ParameterOverrides.GetEnumerator()) {
        $valueType = if ($null -eq $item.Value) { [object] } else { $item.Value.GetType() }
        $overrides.Add(
            [string] $item.Key,
            [Text.Json.JsonSerializer]::SerializeToElement(
                [object] $item.Value,
                $valueType,
                [Text.Json.JsonSerializerOptions] $null))
    }

    return $overrides
}

function New-BicepDeployOptions {
    param(
        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(Mandatory)]
        [string] $ParameterSetName,

        [string] $SubscriptionId,

        [string] $ResourceGroup,

        [string] $ManagementGroupId,

        [string] $Location,

        [AllowNull()]
        [string] $StackName,

        [AllowNull()]
        [hashtable] $ParameterOverrides
    )

    Import-BicepTestAssembly
    $properties = [ordered] @{
        FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
        ParameterOverrides = ConvertTo-BicepJsonElementDictionary -ParameterOverrides $ParameterOverrides
    }

    switch ($ParameterSetName) {
        'ResourceGroup' {
            $properties.SubscriptionId = $SubscriptionId
            $properties.ResourceGroup = $ResourceGroup
            if (-not [string]::IsNullOrWhiteSpace($Location)) {
                $properties.Location = $Location
            }
        }
        'Subscription' {
            $properties.SubscriptionId = $SubscriptionId
            $properties.Location = $Location
        }
        'ManagementGroup' {
            $properties.ManagementGroupId = $ManagementGroupId
            $properties.Location = $Location
        }
        default {
            throw "Unsupported parameter set '$ParameterSetName'."
        }
    }

    if (-not [string]::IsNullOrWhiteSpace($StackName)) {
        $properties.StackName = $StackName
    }

    return [AnthonyCMartin.BicepTesting.DeployOptions] $properties
}

function Get-BicepSnapshot {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateNotNull()]
        [object] $Session,

        [Parameter(Mandatory, Position = 1)]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [string] $TenantId,

        [string] $SubscriptionId,

        [string] $ResourceGroup,

        [string] $Location,

        [string] $DeploymentName
    )

    process {
        Import-BicepTestAssembly
        $properties = [ordered] @{
            FilePath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path)
        }

        if ($PSBoundParameters.ContainsKey('TenantId')) {
            $properties.TenantId = $TenantId
        }
        if ($PSBoundParameters.ContainsKey('SubscriptionId')) {
            $properties.SubscriptionId = $SubscriptionId
        }
        if ($PSBoundParameters.ContainsKey('ResourceGroup')) {
            $properties.ResourceGroup = $ResourceGroup
        }
        if ($PSBoundParameters.ContainsKey('Location')) {
            $properties.Location = $Location
        }
        if ($PSBoundParameters.ContainsKey('DeploymentName')) {
            $properties.DeploymentName = $DeploymentName
        }
        $options = [AnthonyCMartin.BicepTesting.SnapshotOptions] $properties

        $task = $Session.SnapshotAsync(
            $options,
            [Threading.CancellationToken]::None)
        $task.GetAwaiter().GetResult()
    }
}

function Remove-BicepTestSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateNotNull()]
        [object] $Session
    )

    process {
        $Session.Dispose()
    }
}

function Start-BicepTestDeployment {
    [CmdletBinding(DefaultParameterSetName = 'ResourceGroup', SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNull()]
        [object] $Session,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [ValidateNotNullOrEmpty()]
        [string] $SubscriptionId,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $ResourceGroup,

        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $ManagementGroupId,

        [Parameter(ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $Location,

        [ValidateNotNullOrEmpty()]
        [string] $StackName,

        [hashtable] $ParameterOverrides
    )

    process {
        $options = New-BicepDeployOptions `
            -Path $Path `
            -ParameterSetName $PSCmdlet.ParameterSetName `
            -SubscriptionId $SubscriptionId `
            -ResourceGroup $ResourceGroup `
            -ManagementGroupId $ManagementGroupId `
            -Location $Location `
            -StackName $StackName `
            -ParameterOverrides $ParameterOverrides

        $scope = if ($PSCmdlet.ParameterSetName -eq 'ManagementGroup') {
            "managementGroups/$($options.ManagementGroupId)"
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'Subscription') {
            "subscriptions/$($options.SubscriptionId)"
        }
        else {
            "subscriptions/$($options.SubscriptionId)/resourceGroups/$($options.ResourceGroup)"
        }
        $target = "$scope/stacks/$($options.StackName)"
        if (-not $PSCmdlet.ShouldProcess($target, 'Create or update Bicep test deployment')) {
            return
        }

        $task = $Session.DeployAsync(
            $options,
            [Threading.CancellationToken]::None)
        $task.GetAwaiter().GetResult()
    }
}

function Test-BicepTestDeployment {
    [CmdletBinding(DefaultParameterSetName = 'ResourceGroup')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNull()]
        [object] $Session,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $Path,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [ValidateNotNullOrEmpty()]
        [string] $SubscriptionId,

        [Parameter(Mandatory, ParameterSetName = 'ResourceGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $ResourceGroup,

        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $ManagementGroupId,

        [Parameter(ParameterSetName = 'ResourceGroup')]
        [Parameter(Mandatory, ParameterSetName = 'Subscription')]
        [Parameter(Mandatory, ParameterSetName = 'ManagementGroup')]
        [ValidateNotNullOrEmpty()]
        [string] $Location,

        [ValidateNotNullOrEmpty()]
        [string] $StackName,

        [hashtable] $ParameterOverrides
    )

    process {
        $options = New-BicepDeployOptions `
            -Path $Path `
            -ParameterSetName $PSCmdlet.ParameterSetName `
            -SubscriptionId $SubscriptionId `
            -ResourceGroup $ResourceGroup `
            -ManagementGroupId $ManagementGroupId `
            -Location $Location `
            -StackName $StackName `
            -ParameterOverrides $ParameterOverrides

        $task = $Session.ValidateAsync(
            $options,
            [Threading.CancellationToken]::None)
        $task.GetAwaiter().GetResult()
    }
}

function Remove-BicepTestDeployment {
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateNotNull()]
        [object] $Deployment
    )

    process {
        if (-not $PSCmdlet.ShouldProcess('Bicep test deployment', 'Remove deployment stack and managed resources')) {
            return
        }

        $task = $Deployment.TeardownAsync([Threading.CancellationToken]::None)
        $task.GetAwaiter().GetResult()
    }
}

Export-ModuleMember -Function Get-BicepSnapshot, New-BicepLiveTestSession, New-BicepTestSession, Remove-BicepTestDeployment, Remove-BicepTestSession, Start-BicepTestDeployment, Test-BicepTestDeployment