|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Text.Json.Nodes;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.Tests.Helpers;
using Aspire.Hosting.Utils;
using Azure.Provisioning.KeyVault;
using Json.Schema;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Aspire.Hosting.Tests.Schema;
public class SchemaTests
{
public static TheoryData<string, Action<IDistributedApplicationBuilder>> ApplicationSamples
{
get
{
var data = new TheoryData<string, Action<IDistributedApplicationBuilder>>
{
{ "BasicParameter", (IDistributedApplicationBuilder builder) =>
{
builder.AddParameter("foo");
}
},
{ "BasicSecretParameter", (IDistributedApplicationBuilder builder) =>
{
builder.AddParameter("foo", secret: true);
}
},
{ "ConnectionStringParameter", (IDistributedApplicationBuilder builder) =>
{
builder.AddConnectionString("foo");
}
},
{ "BasicContainer", (IDistributedApplicationBuilder builder) =>
{
builder.AddRedis("redis");
}
},
{ "ContainerWithBuild", (IDistributedApplicationBuilder builder) =>
{
var tempPath = Path.GetTempPath();
var tempContextPath = Path.Combine(tempPath, Path.GetRandomFileName());
Directory.CreateDirectory(tempContextPath);
var tempDockerfilePath = Path.Combine(tempContextPath, "Dockerfile");
File.WriteAllText(tempDockerfilePath, "does not need to be valid dockerfile content here");
builder.AddContainer("mycontainer", "myimage").WithDockerfile(tempContextPath);
}
},
{ "ContainerWithBuildAndBuildArgs", (IDistributedApplicationBuilder builder) =>
{
var tempPath = Path.GetTempPath();
var tempContextPath = Path.Combine(tempPath, Path.GetRandomFileName());
Directory.CreateDirectory(tempContextPath);
var tempDockerfilePath = Path.Combine(tempContextPath, "Dockerfile");
File.WriteAllText(tempDockerfilePath, "does not need to be valid dockerfile content here");
var p = builder.AddParameter("p");
builder.AddContainer("mycontainer", "myimage")
.WithDockerfile(tempContextPath)
.WithBuildArg("stringArg", "a string")
.WithBuildArg("intArg", 42)
.WithBuildArg("boolArg", true)
.WithBuildArg("parameterArg", p);
}
},
{ "ContainerWithBuildAndSecretBuildArgs", (IDistributedApplicationBuilder builder) =>
{
var tempPath = Path.GetTempPath();
var tempContextPath = Path.Combine(tempPath, Path.GetRandomFileName());
Directory.CreateDirectory(tempContextPath);
var tempDockerfilePath = Path.Combine(tempContextPath, "Dockerfile");
File.WriteAllText(tempDockerfilePath, "does not need to be valid dockerfile content here");
var p = builder.AddParameter("p", secret: true);
builder.AddContainer("mycontainer", "myimage")
.WithDockerfile(tempContextPath)
.WithBuildSecret("secretArg", p);
}
},
{ "ContainerWithVolume", (IDistributedApplicationBuilder builder) =>
{
builder.AddRedis("redis").WithDataVolume();
}
},
{ "ContainerWithBindMount", (IDistributedApplicationBuilder builder) =>
{
builder.AddRedis("redis").WithBindMount("source", "target");
}
},
{ "BasicContainerWithConnectionString", (IDistributedApplicationBuilder builder) =>
{
builder.AddPostgres("postgres");
}
},
{ "BasicDockerfile", (IDistributedApplicationBuilder builder) =>
{
builder.AddExecutable("foo", "bar", "baz", "one", "two", "three").PublishAsDockerFile();
}
},
{ "ContainerWithContainerRuntimeArgs", (IDistributedApplicationBuilder builder) =>
{
builder.AddContainer("foo", "bar").WithContainerRuntimeArgs("one", "two", "three");
}
},
{ "BasicProject", (IDistributedApplicationBuilder builder) =>
{
builder.AddProject<Projects.ServiceA>("project");
}
},
{ "BasicExecutable", (IDistributedApplicationBuilder builder) =>
{
builder.AddExecutable("executable", "hellworld", "foo", "arg1", "arg2");
}
},
{ "VanillaProjectBasedContainerApp", (IDistributedApplicationBuilder builder) =>
{
builder.AddProject<Projects.ServiceA>("project")
.PublishAsAzureContainerApp((_, _) => { });
}
},
{ "CustomizedProjectBasedContainerApp", (IDistributedApplicationBuilder builder) =>
{
var minReplicas = builder.AddParameter("minReplicas");
builder.AddProject<Projects.ServiceA>("project")
.PublishAsAzureContainerApp((infrastructure, app) =>
{
app.Template.Scale.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure);
});
}
},
{ "VanillaContainerBasedContainerApp", (IDistributedApplicationBuilder builder) =>
{
builder.AddContainer("mycontainer", "myimage")
.PublishAsAzureContainerApp((_, _) => { });
}
},
{ "CustomizedContainerBasedContainerApp", (IDistributedApplicationBuilder builder) =>
{
var minReplicas = builder.AddParameter("minReplicas");
builder.AddContainer("mycontainer", "myimage")
.PublishAsAzureContainerApp((infrastructure, app) =>
{
app.Template.Scale.MinReplicas = minReplicas.AsProvisioningParameter(infrastructure);
});
}
},
{ "VanillaBicepResource", (IDistributedApplicationBuilder builder) =>
{
builder.AddAzureInfrastructure("infrastructure", infrastructure =>
{
var kv = KeyVaultService.FromExisting("doesnotexist");
infrastructure.Add(kv);
});
}
},
};
return data;
}
}
private static JsonSchema? s_schema;
private static JsonSchema GetSchema()
{
if (s_schema == null)
{
var relativePath = Path.Combine("Schema", "aspire-8.0.json");
var schemaPath = Path.GetFullPath(relativePath);
s_schema = JsonSchema.FromFile(schemaPath);
}
return s_schema;
}
[Theory]
[MemberData(nameof(ApplicationSamples))]
public void ValidateApplicationSamples(string testCaseName, Action<IDistributedApplicationBuilder> configurator)
{
string manifestDir = Directory.CreateTempSubdirectory(testCaseName).FullName;
var builder = TestDistributedApplicationBuilder.Create(["--publisher", "manifest", "--output-path", Path.Combine(manifestDir, "not-used.json")]);
builder.Services.AddKeyedSingleton<IDistributedApplicationPublisher, JsonDocumentManifestPublisher>("manifest");
configurator(builder);
using var program = builder.Build();
var publisher = program.Services.GetManifestPublisher();
program.Run();
var manifestText = publisher.ManifestDocument.RootElement.ToString();
AssertValid(manifestText);
}
[Fact]
public void SchemaRejectsEmptyManifest()
{
var manifestText = """
{
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void ManifestAcceptsUnknownResourceType()
{
var manifestText = """
{
"resources": {
"aresource": {
"type": "not.a.valid.resource"
}
}
}
"""{
"resources": {
"aresource": {
"type": "not.a.valid.resource"
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithContainerResourceWithMissingImageIsRejected()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v0"
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v0"
}
}
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void ManifestWithValue0ResourceWithConnectionStringAndValueIsRejectedIsRejected()
{
var manifestText = """
{
"resources": {
"valueresource": {
"type": "value.v0",
"connectionString": "{valueresource.value}",
"value": "this.should.not.be.here"
}
}
}
"""{
"resources": {
"valueresource": {
"type": "value.v0",
"connectionString": "{valueresource.value}",
"value": "this.should.not.be.here"
}
}
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void InvalidBicepResourceFailsValidationToProveItIsntBeingIgnored()
{
var manifestText = """
{
"resources": {
"invalidbicepresource": {
"type": "azure.bicep.v0",
"invalidproperty": "invalidvalue"
}
}
}
"""{
"resources": {
"invalidbicepresource": {
"type": "azure.bicep.v0",
"invalidproperty": "invalidvalue"
}
}
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void ManifestWithContainerResourceAndNoEnvOrBindingsIsAccepted()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v0",
"image": "myimage:latest"
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v0",
"image": "myimage:latest"
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithContainerV0ResourceAndBuildFieldIsRejected()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v0",
"image": "myimage:latest",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v0",
"image": "myimage:latest",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void ManifestWithContainerV1ResourceWithImageAndBuildFieldIsRejected()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v1",
"image": "myimage:latest",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v1",
"image": "myimage:latest",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
""";
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
Assert.False(schema.Evaluate(manifestJson).IsValid);
}
[Fact]
public void ManifestWithContainerV1ResourceAndBuildFieldIsAccepted()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v1",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v1",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile"
}
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithDockerfileV0ResourceAndBuildFieldAndArgsIsAccepted()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "dockerfile.v0",
"context": "relativepath",
"path": "relativepath/Dockerfile",
"buildArgs": {
"ARG1": "an arg"
}
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "dockerfile.v0",
"context": "relativepath",
"path": "relativepath/Dockerfile",
"buildArgs": {
"ARG1": "an arg"
}
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithContainerV1ResourceAndBuildFieldAndArgsIsAccepted()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v1",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile",
"args": {
"ARG1": "an arg"
}
}
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v1",
"build": {
"context": "relativepath",
"dockerfile": "relativepath/Dockerfile",
"args": {
"ARG1": "an arg"
}
}
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithContainerV1ResourceAndImageFieldIsAccepted()
{
var manifestText = """
{
"resources": {
"mycontainer": {
"type": "container.v1",
"image": "myimage:latest"
}
}
}
"""{
"resources": {
"mycontainer": {
"type": "container.v1",
"image": "myimage:latest"
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void ManifestWithProjectResourceAndNoEnvOrBindingsIsAccepted()
{
var manifestText = """
{
"resources": {
"myapp": {
"type": "project.v0",
"path": "path/to/project/project.csproj"
}
}
}
"""{
"resources": {
"myapp": {
"type": "project.v0",
"path": "path/to/project/project.csproj"
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void BicepManifestIsAccepted()
{
// The reason this large test is here is that when submitting the positive test cases to SchemaStore.org
// I found this one was failing. So I'm including it here.
var manifestText = """
{
"resources": {
"administratorLogin": {
"inputs": {
"value": {
"type": "string"
}
},
"type": "parameter.v0",
"value": "{administratorLogin.inputs.value}"
},
"administratorLoginPassword": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"generate": {
"minLength": 16,
"lower": false,
"upper": false,
"numeric": false,
"special": false,
"minLower": 1,
"minUpper": 2,
"minNumeric": 3,
"minSpecial": 4
}
}
}
},
"type": "parameter.v0",
"value": "{administratorLoginPassword.inputs.value}"
},
"administratorName": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"value": "David"
}
}
},
"type": "parameter.v0",
"value": "{administratorName.inputs.value}"
},
"ai": {
"connectionString": "{ai.outputs.appInsightsConnectionString}",
"params": {
"logAnalyticsWorkspaceId": "{lawkspc.outputs.logAnalyticsWorkspaceId}"
},
"path": "ai.module.bicep",
"type": "azure.bicep.v0"
},
"aiwithoutlaw": {
"connectionString": "{aiwithoutlaw.outputs.appInsightsConnectionString}",
"params": {
"logAnalyticsWorkspaceId": ""
},
"path": "aiwithoutlaw.module.bicep",
"type": "azure.bicep.v0"
},
"api": {
"bindings": {
"http": {
"external": true,
"protocol": "tcp",
"scheme": "http",
"transport": "http"
},
"https": {
"external": true,
"protocol": "tcp",
"scheme": "https",
"transport": "http"
}
},
"env": {
"APPLICATIONINSIGHTS_CONNECTION_STRING": "{ai.connectionString}",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"ConnectionStrings__appConfig": "{appConfig.connectionString}",
"ConnectionStrings__blob": "{blob.connectionString}",
"ConnectionStrings__cosmos": "{cosmos.connectionString}",
"ConnectionStrings__db": "{db.connectionString}",
"ConnectionStrings__db2": "{db2.connectionString}",
"ConnectionStrings__kv3": "{kv3.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"ConnectionStrings__redis": "{redis.connectionString}",
"ConnectionStrings__sb": "{sb.connectionString}",
"ConnectionStrings__signalr": "{signalr.connectionString}",
"ConnectionStrings__table": "{table.connectionString}",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"bicepValue0": "{test.outputs.val0}",
"bicepValue1": "{test.outputs.val1}",
"bicepValue_test": "{test.outputs.test}"
},
"path": "../BicepSample.ApiService/BicepSample.ApiService.csproj",
"type": "project.v0"
},
"appConfig": {
"connectionString": "{appConfig.outputs.appConfigEndpoint}",
"params": {
"principalId": "",
"principalType": "",
"sku": "standard"
},
"path": "appConfig.module.bicep",
"type": "azure.bicep.v0"
},
"blob": {
"connectionString": "{storage.outputs.blobEndpoint}",
"type": "value.v0"
},
"cosmos": {
"connectionString": "{cosmos.secretOutputs.connectionString}",
"params": {
"keyVaultName": ""
},
"path": "cosmos.module.bicep",
"type": "azure.bicep.v0"
},
"db": {
"connectionString": "{sql.connectionString};Database=db",
"type": "value.v0"
},
"db2": {
"connectionString": "{postgres2.connectionString};Database=db2",
"type": "value.v0"
},
"kv3": {
"connectionString": "{kv3.outputs.vaultUri}",
"params": {
"principalId": "",
"principalType": ""
},
"path": "kv3.module.bicep",
"type": "azure.bicep.v0"
},
"lawkspc": {
"path": "lawkspc.module.bicep",
"type": "azure.bicep.v0"
},
"postgres2": {
"connectionString": "{postgres2.secretOutputs.connectionString}",
"params": {
"administratorLogin": "{administratorLogin.value}",
"administratorLoginPassword": "{administratorLoginPassword.value}",
"keyVaultName": ""
},
"path": "postgres2.module.bicep",
"type": "azure.bicep.v0"
},
"queue": {
"connectionString": "{storage.outputs.queueEndpoint}",
"type": "value.v0"
},
"redis": {
"connectionString": "{redis.secretOutputs.connectionString}",
"params": {
"keyVaultName": ""
},
"path": "redis.module.bicep",
"type": "azure.bicep.v0"
},
"sb": {
"connectionString": "{sb.outputs.serviceBusEndpoint}",
"params": {
"principalId": "",
"principalType": ""
},
"path": "sb.module.bicep",
"type": "azure.bicep.v0"
},
"signalr": {
"connectionString": "Endpoint=https://{signalr.outputs.hostName};AuthType=azure",
"params": {
"principalId": "",
"principalType": ""
},
"path": "signalr.module.bicep",
"type": "azure.bicep.v0"
},
"sql": {
"connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
"params": {
"principalId": "",
"principalName": ""
},
"path": "sql.module.bicep",
"type": "azure.bicep.v0"
},
"storage": {
"params": {
"principalId": "",
"principalType": ""
},
"path": "storage.module.bicep",
"type": "azure.bicep.v0"
},
"table": {
"connectionString": "{storage.outputs.tableEndpoint}",
"type": "value.v0"
},
"test": {
"params": {
"p2": "{test0.outputs.val0}",
"test": "{val.value}",
"values": ["one", "two"]
},
"path": "test.bicep",
"type": "azure.bicep.v0"
},
"test0": {
"path": "test0.module.bicep",
"type": "azure.bicep.v0"
},
"val": {
"inputs": {
"value": {
"type": "string"
}
},
"type": "parameter.v0",
"value": "{val.inputs.value}"
}
}
}
"""{
"resources": {
"administratorLogin": {
"inputs": {
"value": {
"type": "string"
}
},
"type": "parameter.v0",
"value": "{administratorLogin.inputs.value}"
},
"administratorLoginPassword": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"generate": {
"minLength": 16,
"lower": false,
"upper": false,
"numeric": false,
"special": false,
"minLower": 1,
"minUpper": 2,
"minNumeric": 3,
"minSpecial": 4
}
}
}
},
"type": "parameter.v0",
"value": "{administratorLoginPassword.inputs.value}"
},
"administratorName": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"value": "David"
}
}
},
"type": "parameter.v0",
"value": "{administratorName.inputs.value}"
},
"ai": {
"connectionString": "{ai.outputs.appInsightsConnectionString}",
"params": {
"logAnalyticsWorkspaceId": "{lawkspc.outputs.logAnalyticsWorkspaceId}"
},
"path": "ai.module.bicep",
"type": "azure.bicep.v0"
},
"aiwithoutlaw": {
"connectionString": "{aiwithoutlaw.outputs.appInsightsConnectionString}",
"params": {
"logAnalyticsWorkspaceId": ""
},
"path": "aiwithoutlaw.module.bicep",
"type": "azure.bicep.v0"
},
"api": {
"bindings": {
"http": {
"external": true,
"protocol": "tcp",
"scheme": "http",
"transport": "http"
},
"https": {
"external": true,
"protocol": "tcp",
"scheme": "https",
"transport": "http"
}
},
"env": {
"APPLICATIONINSIGHTS_CONNECTION_STRING": "{ai.connectionString}",
"ASPNETCORE_FORWARDEDHEADERS_ENABLED": "true",
"ConnectionStrings__appConfig": "{appConfig.connectionString}",
"ConnectionStrings__blob": "{blob.connectionString}",
"ConnectionStrings__cosmos": "{cosmos.connectionString}",
"ConnectionStrings__db": "{db.connectionString}",
"ConnectionStrings__db2": "{db2.connectionString}",
"ConnectionStrings__kv3": "{kv3.connectionString}",
"ConnectionStrings__queue": "{queue.connectionString}",
"ConnectionStrings__redis": "{redis.connectionString}",
"ConnectionStrings__sb": "{sb.connectionString}",
"ConnectionStrings__signalr": "{signalr.connectionString}",
"ConnectionStrings__table": "{table.connectionString}",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
"OTEL_DOTNET_EXPERIMENTAL_OTLP_RETRY": "in_memory",
"bicepValue0": "{test.outputs.val0}",
"bicepValue1": "{test.outputs.val1}",
"bicepValue_test": "{test.outputs.test}"
},
"path": "../BicepSample.ApiService/BicepSample.ApiService.csproj",
"type": "project.v0"
},
"appConfig": {
"connectionString": "{appConfig.outputs.appConfigEndpoint}",
"params": {
"principalId": "",
"principalType": "",
"sku": "standard"
},
"path": "appConfig.module.bicep",
"type": "azure.bicep.v0"
},
"blob": {
"connectionString": "{storage.outputs.blobEndpoint}",
"type": "value.v0"
},
"cosmos": {
"connectionString": "{cosmos.secretOutputs.connectionString}",
"params": {
"keyVaultName": ""
},
"path": "cosmos.module.bicep",
"type": "azure.bicep.v0"
},
"db": {
"connectionString": "{sql.connectionString};Database=db",
"type": "value.v0"
},
"db2": {
"connectionString": "{postgres2.connectionString};Database=db2",
"type": "value.v0"
},
"kv3": {
"connectionString": "{kv3.outputs.vaultUri}",
"params": {
"principalId": "",
"principalType": ""
},
"path": "kv3.module.bicep",
"type": "azure.bicep.v0"
},
"lawkspc": {
"path": "lawkspc.module.bicep",
"type": "azure.bicep.v0"
},
"postgres2": {
"connectionString": "{postgres2.secretOutputs.connectionString}",
"params": {
"administratorLogin": "{administratorLogin.value}",
"administratorLoginPassword": "{administratorLoginPassword.value}",
"keyVaultName": ""
},
"path": "postgres2.module.bicep",
"type": "azure.bicep.v0"
},
"queue": {
"connectionString": "{storage.outputs.queueEndpoint}",
"type": "value.v0"
},
"redis": {
"connectionString": "{redis.secretOutputs.connectionString}",
"params": {
"keyVaultName": ""
},
"path": "redis.module.bicep",
"type": "azure.bicep.v0"
},
"sb": {
"connectionString": "{sb.outputs.serviceBusEndpoint}",
"params": {
"principalId": "",
"principalType": ""
},
"path": "sb.module.bicep",
"type": "azure.bicep.v0"
},
"signalr": {
"connectionString": "Endpoint=https://{signalr.outputs.hostName};AuthType=azure",
"params": {
"principalId": "",
"principalType": ""
},
"path": "signalr.module.bicep",
"type": "azure.bicep.v0"
},
"sql": {
"connectionString": "Server=tcp:{sql.outputs.sqlServerFqdn},1433;Encrypt=True;Authentication=\u0022Active Directory Default\u0022",
"params": {
"principalId": "",
"principalName": ""
},
"path": "sql.module.bicep",
"type": "azure.bicep.v0"
},
"storage": {
"params": {
"principalId": "",
"principalType": ""
},
"path": "storage.module.bicep",
"type": "azure.bicep.v0"
},
"table": {
"connectionString": "{storage.outputs.tableEndpoint}",
"type": "value.v0"
},
"test": {
"params": {
"p2": "{test0.outputs.val0}",
"test": "{val.value}",
"values": ["one", "two"]
},
"path": "test.bicep",
"type": "azure.bicep.v0"
},
"test0": {
"path": "test0.module.bicep",
"type": "azure.bicep.v0"
},
"val": {
"inputs": {
"value": {
"type": "string"
}
},
"type": "parameter.v0",
"value": "{val.inputs.value}"
}
}
}
""";
AssertValid(manifestText);
}
[Fact]
public void BothDefaultGenerateAndValueAreMutuallyExclusive()
{
// Trying to us both 'generate' and 'value' in the same parameter should be rejected.
var manifestText = """
{
"resources": {
"foo": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"generate": {
"minLength": 16
},
"value": "some value"
}
}
},
"type": "parameter.v0",
"value": "{foo.inputs.value}"
}
}
}
"""{
"resources": {
"foo": {
"inputs": {
"value": {
"secret": true,
"type": "string",
"default": {
"generate": {
"minLength": 16
},
"value": "some value"
}
}
},
"type": "parameter.v0",
"value": "{foo.inputs.value}"
}
}
}
""";
AssertInvalid(manifestText);
}
private static void AssertValid(string manifestText)
{
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
var results = schema.Evaluate(manifestJson);
if (!results.IsValid)
{
var errorMessages = results.Details.Where(x => x.HasErrors).SelectMany(e => e.Errors!).Select(e => e.Value);
Assert.True(results.IsValid, string.Join(Environment.NewLine, errorMessages ?? ["Schema failed validation with no errors"]));
}
}
private static void AssertInvalid(string manifestText)
{
var manifestJson = JsonNode.Parse(manifestText);
var schema = GetSchema();
var results = schema.Evaluate(manifestJson);
Assert.False(results.IsValid);
}
}
|