diff --git a/docs/changelog.md b/docs/changelog.md index 183a98f925..7a5c63810a 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -46,6 +46,8 @@ What's changed since pre-release v1.48.0-B0228: [#3907](https://github.com/Azure/PSRule.Rules.Azure/issues/3907) - Fixed `tryGet` throwing instead of returning `null` for a property lookup against an array by @oWretch. [#3907](https://github.com/Azure/PSRule.Rules.Azure/issues/3907) + - Fixed cross-scope `existing` resource reference via symbolic name resolving to a malformed mock, breaking `concat()`/`map()` during pre-flight expansion. + [#3920](https://github.com/Azure/PSRule.Rules.Azure/issues/3920) - Engineering: - Bump YamlDotNet to 11.2.5. diff --git a/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs b/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs index feba3dcda8..468c93f910 100644 --- a/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs +++ b/src/PSRule.Rules.Azure/Arm/Expressions/Functions.cs @@ -289,21 +289,19 @@ internal static object Concat(ITemplateContext context, object[] args) return result.ToString(); } // Array - else if (args[0] is Array || args[0] is JArray) + else if (ExpressionHelpers.TryArray(args[0], out var firstArray)) { var result = new List(); - for (var i = 0; i < args.Length; i++) + for (var j = 0; j < firstArray.Length; j++) + result.Add(firstArray.GetValue(j)); + + for (var i = 1; i < args.Length; i++) { - if (args[i] is Array array) + if (ExpressionHelpers.TryArray(args[i], out var array)) { for (var j = 0; j < array.Length; j++) result.Add(array.GetValue(j)); } - else if (args[i] is JArray jArray) - { - for (var j = 0; j < jArray.Count; j++) - result.Add(jArray[j]); - } } return result.ToArray(); } diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj index 2c2e399493..66a4057764 100644 --- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj +++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj @@ -320,6 +320,12 @@ PreserveNewest + + PreserveNewest + + + PreserveNewest + PreserveNewest diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs index 04678391fc..26d2b940cd 100644 --- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs +++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs @@ -1417,4 +1417,31 @@ public void ProcessTemplate_WhenNullableIndexFromEnd_ShouldReturnExpectedValue() var secondLastPart = secondLastPartOutput["value"].Value(); Assert.Equal("eastus", secondLastPart); } + + /// + /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3920 + /// + [Fact] + public void ProcessTemplate_WhenExistingResourceReferencedBySymbolicName_ShouldReturnMock() + { + _ = ProcessTemplate(GetSourcePath("Tests.Bicep.44.json"), null, out var templateContext); + + Assert.True(templateContext.RootDeployment.TryOutput("addressPrefixes", out JObject addressPrefixesOutput)); + Assert.Equal(JTokenType.Array, addressPrefixesOutput["value"].Type); + + Assert.True(templateContext.RootDeployment.TryOutput("combinedAddressPrefixes", out JObject combinedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, combinedAddressPrefixesOutput["value"].Type); + + Assert.True(templateContext.RootDeployment.TryOutput("mappedAddressPrefixes", out JObject mappedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, mappedAddressPrefixesOutput["value"].Type); + } + + [Fact] + public void ProcessTemplate_WhenExistingResourceReferencedByResourceId_ShouldReturnMock() + { + _ = ProcessTemplate(GetSourcePath("Tests.Bicep.45.json"), null, out var templateContext); + + Assert.True(templateContext.RootDeployment.TryOutput("combinedAddressPrefixes", out JObject combinedAddressPrefixesOutput)); + Assert.Equal(JTokenType.Array, combinedAddressPrefixesOutput["value"].Type); + } } diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json new file mode 100644 index 0000000000..636be037fe --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.44.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "languageVersion": "2.0", + "contentVersion": "1.0.0.0", + "resources": { + "vnet": { + "existing": true, + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2023-11-01", + "subscriptionId": "[subscription().subscriptionId]", + "resourceGroup": "other-rg", + "name": "vnet1" + } + }, + "outputs": { + "addressPrefixes": { + "type": "array", + "value": "[reference('vnet').addressSpace.addressPrefixes]" + }, + "combinedAddressPrefixes": { + "type": "array", + "value": "[concat(reference('vnet').addressSpace.addressPrefixes, createArray())]" + }, + "mappedAddressPrefixes": { + "type": "array", + "value": "[map(reference('vnet').addressSpace.addressPrefixes, lambda('prefix', lambdaVariables('prefix')))]" + } + } +} diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json new file mode 100644 index 0000000000..9154ef5f62 --- /dev/null +++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.45.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [], + "outputs": { + "combinedAddressPrefixes": { + "type": "array", + "value": "[concat(reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, 'other-rg'), 'Microsoft.Network/virtualNetworks', 'vnet1'), '2023-11-01').addressSpace.addressPrefixes, createArray())]" + } + } +}