testcases/deploymentTemplate/ResourceIds-should-not-contain.test.ps1

<#
.Synopsis
    Ensures that there is no function used for a default parameter in the resourceId function (e.g. resourceGroup().namd, subscription().subscriptionId).
.Description
    Ensures that there is no function used for a default parameter in the resourceId function (e.g. resourceGroup().namd, subscription().subscriptionId).
.Example
    Test-AzTemplate -TemplatePath .\100-marketplace-sample\ -Test ResourceId-should-not-contain-resourcegroup-function
.Example
    .\ResourceIds-should-not-contain.test.ps1 -TemplateText (Get-Content -path ..\..\..\unit-tests\ResourceIds-should-not-contain.json -Raw)
    
#>

param(
    [Parameter(Mandatory = $true)]
    [string]
    $TemplateText
)

$exprStrOrQuote = [Regex]::new('(?<!\\)[\[\"]', 'RightToLeft') # use this to go backwards from a match below and ensure we are in an expression

# Check for any functions as parameters - PowerShell handles empty differently in objects so check the JSON source (i.e. text)
# note this regex allows for any chars to preceed the function to check for nesting, but it will also flag UDFs as written (which are not common)
$items = @([Regex]::Matches($TemplateText, "resourceId\s{0,}\(\s{0,}resourceGroup\(")) +                 # resourceId(resourceGroup(
         @([Regex]::Matches($TemplateText, "resourceId\s{0,}\(\s{0,}subscription\(")) +                  # resourceId(subscription(
         @([Regex]::Matches($TemplateText, "resourceId\s{0,}\(\s{0,}(concat|format)\s{0,}\(")) +             # resourceId(concat( (or format)
         @([Regex]::Matches($TemplateText, "tenantResourceId\s{0,}\(\s{0,}(concat|format)\s{0,}\(")) +       # tenantResourceId(concat(
         @([Regex]::Matches($TemplateText, "extensionResourceId\s{0,}\(\s{0,}(concat|format)\s{0,}\(")) +    # extensionResourceId(concat(
         @([Regex]::Matches($TemplateText, "subscriptionResourceId\s{0,}\(\s{0,}subscription\(")) +      # subscriptionResourceId(subscription(
         @([Regex]::Matches($TemplateText, "subscriptionResourceId\s{0,}\(\s{0,}(concat|format)\s{0,}\(")) + # subscriptionResourceId(concat(
         @([Regex]::Matches($TemplateText, "reference\s{0,}\(\s{0,}(concat|format)\s{0,}\(")) +              # reference(concat(
         @([Regex]::Matches($TemplateText, "\s{0,}list\w{1,}\s{0,}\(\s{0,}(concat|format)\s{0,}\("))  # list*(concat( - the preceeding part of the expression ensures that we don't find a UDF named something like myListOfSomething


$lineBreaks = [Regex]::Matches($TemplateText, "`n|$([Environment]::NewLine)")

# this just gets the line number and property name for the error message
if ($items) {
    $sortedItems = @()
    foreach ($item in $items) {
        $findExpr = $exprStrOrQuote.Match($TemplateText, $item.Index) # make sure we hit a [ before a quote
        if ($findExpr.Value -eq '[') {
            # we found the expression opening, so we need to throw the error
            $nearbyContext = [Regex]::new('"(?<PropertyName>[^"]{1,})"\s{0,}:', "RightToLeft").Match($TemplateText, $item.Index)
            if ($nearbyContext -and $nearbyContext.Success) {
                $PropertyName = $nearbyContext.Groups["PropertyName"].Value
                $lineNumber = @($lineBreaks | ? { $_.Index -lt $item.Index }).Count + 1
                $obj = New-Object -TypeName psobject -Property @{item = $item; lineNumber = $lineNumber; PropertyName = $PropertyName }
                $sortedItems += $obj
            }
        } 
    }
    #sort the error output by line number
    $sortedItems = $sortedItems | Sort-Object -Property lineNumber
    foreach ($item in $sortedItems) {
        Write-Error "Using `"$($item.item)`" is not allowed - found on line: $($item.lineNumber) for property: $($item.PropertyName)" `
            -TargetObject $item -ErrorId ResourceId.Should.Not.Contain.Function

    }
}