From 8577272eb4b2a6e55ae3e7e71ab68216afa954be Mon Sep 17 00:00:00 2001 From: Anthony Martin Date: Wed, 10 Mar 2021 09:31:11 -0500 Subject: [PATCH] Add some assertions for parent/nested resources & scopes --- .../NestedResourceTests.cs | 176 +++++++++++++++++- .../ParentPropertyResourceTests.cs | 175 ++++++++++++++++- .../Assertions/JTokenAssertionsExtensions.cs | 16 +- 3 files changed, 362 insertions(+), 5 deletions(-) diff --git a/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs b/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs index a1fa4a00d09..ec1d2fdeafb 100644 --- a/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs +++ b/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs @@ -585,7 +585,7 @@ public void NestedResources_provides_correct_error_for_resource_access_with_brok } [TestMethod] - public void NestedResources_formats_names_and_dependsOn_correctly() + public void Nested_resource_formats_names_and_dependsOn_correctly() { var (template, diags, _) = CompilationHelper.Compile(@" resource vnet 'Microsoft.Network/virtualNetworks@2020-06-01' = { @@ -643,5 +643,179 @@ public void NestedResources_formats_names_and_dependsOn_correctly() template.Should().HaveValueAtPath("$.outputs['subnet1id'].value", "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'myVnet', 'subnet1')]"); } } + + [TestMethod] + public void Nested_resource_works_with_extension_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' = { + name: 'res1' + + resource child 'child1' = { + name: 'child1' + } +} + +resource res2 'Microsoft.Rp2/resource2@2020-06-01' = { + scope: res1:child + name: 'res2' + + resource child 'child2' = { + name: 'child2' + } +} + +output res2childprop string = res2:child.properties.someProp +output res2childname string = res2:child.name +output res2childtype string = res2:child.type +output res2childid string = res2:child.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + // res1 + template.Should().HaveValueAtPath("$.resources[2].name", "res1"); + template.Should().NotHaveValueAtPath("$.resources[2].dependsOn"); + + // res1::child1 + template.Should().HaveValueAtPath("$.resources[0].name", "[format('{0}/{1}', 'res1', 'child1')]"); + template.Should().HaveValueAtPath("$.resources[0].dependsOn", new JArray { "[resourceId('Microsoft.Rp1/resource1', 'res1')]" }); + + // res2 + template.Should().HaveValueAtPath("$.resources[3].name", "res2"); + // TODO: fix this to depend on the child (not parent id) + template.Should().HaveValueAtPath("$.resources[3].dependsOn", new JArray { "[resourceId('Microsoft.Rp1/resource1', 'res1')]" }); + + // res2::child2 + template.Should().HaveValueAtPath("$.resources[1].name", "[format('{0}/{1}', 'res2', 'child2')]"); + template.Should().HaveValueAtPath("$.resources[1].dependsOn", new JArray { "[extensionResourceId(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1'), 'Microsoft.Rp2/resource2', 'res2')]" }); + + // TODO: fix this to use an extension resourceId + template.Should().HaveValueAtPath("$.outputs['res2childprop'].value", "[reference(resourceId('Microsoft.Rp2/resource2/child2', 'res2', 'child2')).someProp]"); + template.Should().HaveValueAtPath("$.outputs['res2childname'].value", "child2"); + template.Should().HaveValueAtPath("$.outputs['res2childtype'].value", "Microsoft.Rp2/resource2/child2"); + // TODO: fix this to use an extension resourceId + template.Should().HaveValueAtPath("$.outputs['res2childid'].value", "[resourceId('Microsoft.Rp2/resource2/child2', 'res2', 'child2')]"); + } + } + + [TestMethod] + public void Nested_resource_works_with_existing_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + name: 'res1' + + resource child 'child1' = { + name: 'child1' + } +} + +output res1childprop string = res1:child.properties.someProp +output res1childname string = res1:child.name +output res1childtype string = res1:child.type +output res1childid string = res1:child.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + // res1:child1 + template.Should().HaveValueAtPath("$.resources[0].name", "[format('{0}/{1}', 'res1', 'child1')]"); + template.Should().HaveValueAtPath("$.resources[0].dependsOn", new JArray()); + + template.Should().NotHaveValueAtPath("$.resources[1]"); + + template.Should().HaveValueAtPath("$.outputs['res1childprop'].value", "[reference(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')).someProp]"); + template.Should().HaveValueAtPath("$.outputs['res1childname'].value", "child1"); + template.Should().HaveValueAtPath("$.outputs['res1childtype'].value", "Microsoft.Rp1/resource1/child1"); + template.Should().HaveValueAtPath("$.outputs['res1childid'].value", "[resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')]"); + } + } + + [TestMethod] + public void Nested_resource_formats_references_correctly_for_existing_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + scope: tenant() + name: 'res1' + + resource child 'child1' existing = { + name: 'child1' + } +} + +output res1childprop string = res1:child.properties.someProp +output res1childname string = res1:child.name +output res1childtype string = res1:child.type +output res1childid string = res1:child.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + template.Should().NotHaveValueAtPath("$.resources[0]"); + + // TODO: this should be a tenant resource reference + template.Should().HaveValueAtPath("$.outputs['res1childprop'].value", "[reference(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1'), '2020-06-01').someProp]"); + template.Should().HaveValueAtPath("$.outputs['res1childname'].value", "child1"); + template.Should().HaveValueAtPath("$.outputs['res1childtype'].value", "Microsoft.Rp1/resource1/child1"); + // TODO: this should be a tenant resourceId + template.Should().HaveValueAtPath("$.outputs['res1childid'].value", "[resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')]"); + } + } + + [TestMethod] + public void Nested_resource_blocks_existing_parents_at_different_scopes() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + scope: tenant() + name: 'res1' + + resource child 'child1' = { + name: 'child1' + } +} +"); + + using (new AssertionScope()) + { + // TODO: this should raise an error as cross-scope deployment should be blocked + template.Should().NotHaveValue(); + diags.Where(x => x.Code != "BCP081").Should().NotBeEmpty(); + } + } + + [TestMethod] + public void Nested_resource_blocks_scope_on_child_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' = { + name: 'res1' +} + +resource res2 'Microsoft.Rp2/resource2@2020-06-01' = { + name: 'res2' + + resource child 'child2' = { + scope: res1 + name: 'child2' + } +} +"); + + using (new AssertionScope()) + { + // TODO: this should raise an error as setting scope + parent should be blocked + template.Should().NotHaveValue(); + diags.Where(x => x.Code != "BCP081").Should().NotBeEmpty(); + } + } } } \ No newline at end of file diff --git a/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs b/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs index 276eb7eaf68..13c6e15d7a1 100644 --- a/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParentPropertyResourceTests.cs @@ -62,7 +62,7 @@ public void Parent_property_formats_names_and_dependsOn_correctly() template.Should().HaveValueAtPath("$.resources[1].dependsOn", new JArray { "[resourceId('Microsoft.Network/virtualNetworks', 'myVnet')]" }); template.Should().HaveValueAtPath("$.resources[2].name", "[format('{0}/{1}', 'myVnet', 'subnet2')]"); - template.Should().HaveValueAtPath("$.resources[1].dependsOn", new JArray { "[resourceId('Microsoft.Network/virtualNetworks', 'myVnet')]" }); + template.Should().HaveValueAtPath("$.resources[2].dependsOn", new JArray { "[resourceId('Microsoft.Network/virtualNetworks', 'myVnet')]" }); template.Should().HaveValueAtPath("$.outputs['subnet1prefix'].value", "[reference(resourceId('Microsoft.Network/virtualNetworks/subnets', 'myVnet', 'subnet1')).addressPrefix]"); template.Should().HaveValueAtPath("$.outputs['subnet1name'].value", "subnet1"); @@ -70,5 +70,178 @@ public void Parent_property_formats_names_and_dependsOn_correctly() template.Should().HaveValueAtPath("$.outputs['subnet1id'].value", "[resourceId('Microsoft.Network/virtualNetworks/subnets', 'myVnet', 'subnet1')]"); } } + + [TestMethod] + public void Parent_property_works_with_extension_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' = { + name: 'res1' +} + +resource res1child 'Microsoft.Rp1/resource1/child1@2020-06-01' = { + parent: res1 + name: 'child1' +} + +resource res2 'Microsoft.Rp2/resource2@2020-06-01' = { + scope: res1child + name: 'res2' +} + +resource res2child 'Microsoft.Rp2/resource2/child2@2020-06-01' = { + parent: res2 + name: 'child2' +} + +output res2childprop string = res2child.properties.someProp +output res2childname string = res2child.name +output res2childtype string = res2child.type +output res2childid string = res2child.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + template.Should().HaveValueAtPath("$.resources[0].name", "res1"); + template.Should().NotHaveValueAtPath("$.resources[0].dependsOn"); + + template.Should().HaveValueAtPath("$.resources[1].name", "[format('{0}/{1}', 'res1', 'child1')]"); + template.Should().HaveValueAtPath("$.resources[1].dependsOn", new JArray { "[resourceId('Microsoft.Rp1/resource1', 'res1')]" }); + + template.Should().HaveValueAtPath("$.resources[2].name", "res2"); + template.Should().HaveValueAtPath("$.resources[2].dependsOn", new JArray { "[resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')]" }); + + template.Should().HaveValueAtPath("$.resources[3].name", "[format('{0}/{1}', 'res2', 'child2')]"); + template.Should().HaveValueAtPath("$.resources[3].dependsOn", new JArray { "[extensionResourceId(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1'), 'Microsoft.Rp2/resource2', 'res2')]" }); + + template.Should().HaveValueAtPath("$.outputs['res2childprop'].value", "[reference(resourceId('Microsoft.Rp2/resource2/child2', 'res2', 'child2')).someProp]"); + template.Should().HaveValueAtPath("$.outputs['res2childname'].value", "child2"); + template.Should().HaveValueAtPath("$.outputs['res2childtype'].value", "Microsoft.Rp2/resource2/child2"); + template.Should().HaveValueAtPath("$.outputs['res2childid'].value", "[resourceId('Microsoft.Rp2/resource2/child2', 'res2', 'child2')]"); + } + } + + [TestMethod] + public void Parent_property_works_with_existing_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + name: 'res1' +} + +resource child1 'Microsoft.Rp1/resource1/child1@2020-06-01' = { + parent: res1 + name: 'child1' +} + +output res1childprop string = child1.properties.someProp +output res1childname string = child1.name +output res1childtype string = child1.type +output res1childid string = child1.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + // child1 + template.Should().HaveValueAtPath("$.resources[0].name", "[format('{0}/{1}', 'res1', 'child1')]"); + template.Should().HaveValueAtPath("$.resources[0].dependsOn", new JArray()); + + template.Should().NotHaveValueAtPath("$.resources[1]"); + + template.Should().HaveValueAtPath("$.outputs['res1childprop'].value", "[reference(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')).someProp]"); + template.Should().HaveValueAtPath("$.outputs['res1childname'].value", "child1"); + template.Should().HaveValueAtPath("$.outputs['res1childtype'].value", "Microsoft.Rp1/resource1/child1"); + template.Should().HaveValueAtPath("$.outputs['res1childid'].value", "[resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')]"); + } + } + + [TestMethod] + public void Parent_property_formats_references_correctly_for_existing_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + scope: tenant() + name: 'res1' +} + +resource child1 'Microsoft.Rp1/resource1/child1@2020-06-01' existing = { + parent: res1 + name: 'child1' +} + +output res1childprop string = child1.properties.someProp +output res1childname string = child1.name +output res1childtype string = child1.type +output res1childid string = child1.id +"); + + using (new AssertionScope()) + { + diags.Where(x => x.Code != "BCP081").Should().BeEmpty(); + + template.Should().NotHaveValueAtPath("$.resources[0]"); + + // TODO: this should be a tenant resource reference + template.Should().HaveValueAtPath("$.outputs['res1childprop'].value", "[reference(resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1'), '2020-06-01').someProp]"); + template.Should().HaveValueAtPath("$.outputs['res1childname'].value", "child1"); + template.Should().HaveValueAtPath("$.outputs['res1childtype'].value", "Microsoft.Rp1/resource1/child1"); + // TODO: this should be a tenant resourceId + template.Should().HaveValueAtPath("$.outputs['res1childid'].value", "[resourceId('Microsoft.Rp1/resource1/child1', 'res1', 'child1')]"); + } + } + + [TestMethod] + public void Parent_property_blocks_existing_parents_at_different_scopes() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' existing = { + scope: tenant() + name: 'res1' +} + +resource child1 'Microsoft.Rp1/resource1/child1@2020-06-01' = { + parent: res1 + name: 'child1' +} +"); + + using (new AssertionScope()) + { + // TODO: this should raise an error as cross-scope deployment should be blocked + template.Should().NotHaveValue(); + diags.Where(x => x.Code != "BCP081").Should().NotBeEmpty(); + } + } + + [TestMethod] + public void Parent_property_blocks_scope_on_child_resources() + { + var (template, diags, _) = CompilationHelper.Compile(@" +resource res1 'Microsoft.Rp1/resource1@2020-06-01' = { + name: 'res1' +} + +resource res2 'Microsoft.Rp2/resource2@2020-06-01' = { + name: 'res2' +} + +resource res2child 'Microsoft.Rp2/resource2/child2@2020-06-01' = { + scope: res1 + parent: res2 + name: 'child2' +} +"); + + using (new AssertionScope()) + { + // TODO: this should raise an error as setting scope + parent should be blocked + template.Should().NotHaveValue(); + diags.Where(x => x.Code != "BCP081").Should().NotBeEmpty(); + } + } } } \ No newline at end of file diff --git a/src/Bicep.Core.UnitTests/Assertions/JTokenAssertionsExtensions.cs b/src/Bicep.Core.UnitTests/Assertions/JTokenAssertionsExtensions.cs index 4ebc16a1e68..19030581de0 100644 --- a/src/Bicep.Core.UnitTests/Assertions/JTokenAssertionsExtensions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/JTokenAssertionsExtensions.cs @@ -57,7 +57,7 @@ public static AndConstraint DeepEqual(this JTokenAssertions in Execute.Assertion .BecauseOf(because, becauseArgs) .ForCondition(JToken.DeepEquals(instance.Subject, expected)) - .FailWith("Expected {0} but got {1}", expected?.ToString(), instance.Subject?.ToString()); + .FailWith("Expected {0} but got {1}", expected.ToString(), instance.Subject?.ToString()); return new AndConstraint(instance); } @@ -69,12 +69,12 @@ public static AndConstraint HaveValueAtPath(this JTokenAsserti Execute.Assertion .BecauseOf(because, becauseArgs) .ForCondition(valueAtPath is not null) - .FailWith("Expected {0} at path {1} but it was null", expected.ToString(), jtokenPath); + .FailWith("Expected value at path {0} to be {1} but it was null", jtokenPath, expected.ToString()); Execute.Assertion .BecauseOf(because, becauseArgs) .ForCondition(JToken.DeepEquals(valueAtPath, expected)) - .FailWith("Expected {0} at path {1} but got {2}", expected.ToString(), jtokenPath, valueAtPath?.ToString()); + .FailWith("Expected value at path {0} to be {1} but it was {2}", jtokenPath, expected.ToString(), valueAtPath?.ToString()); return new AndConstraint(instance); } @@ -90,5 +90,15 @@ public static AndConstraint NotHaveValueAtPath(this JTokenAsse return new AndConstraint(instance); } + + public static AndConstraint NotHaveValue(this JTokenAssertions instance, string because = "", params object[] becauseArgs) + { + Execute.Assertion + .BecauseOf(because, becauseArgs) + .ForCondition(instance.Subject is null) + .FailWith("Expected value to be null, but it was {0}", instance.Subject?.ToString()); + + return new AndConstraint(instance); + } } } \ No newline at end of file