Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
*.verified.txt text eol=lf working-tree-encoding=UTF-8
*.verified.xml text eol=lf working-tree-encoding=UTF-8
*.verified.json text eol=lf working-tree-encoding=UTF-8

*.cs text eol=lf
24 changes: 16 additions & 8 deletions schemas/dab.draft.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"description": "Enable health check endpoint",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint for something",
"default": true,
"additionalProperties": false
},
Expand Down Expand Up @@ -186,7 +186,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling REST requests for all entities."
},
"request-body-strict": {
Expand All @@ -210,7 +210,7 @@
"type": "string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling GraphQL requests for all entities."
},
"depth-limit": {
Expand Down Expand Up @@ -438,7 +438,7 @@
"description": "Application Insights connection string"
},
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Application Insights telemetry.",
"default": true
}
Expand Down Expand Up @@ -481,7 +481,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Allow enabling/disabling Azure Log Analytics.",
"default": false
},
Expand Down Expand Up @@ -618,7 +618,7 @@
"additionalProperties": false,
"properties": {
"enabled": {
"type": "boolean",
"$ref": "#/$defs/boolean-or-string",
"description": "Enable health check endpoint globally",
"default": true,
"additionalProperties": false
Expand Down Expand Up @@ -1391,7 +1391,15 @@
"type": "string"
}
},
"required": ["singular"]
"required": [ "singular" ]
}
]
},
"boolean-or-string": {
"oneOf":[
{
"type": [ "boolean", "string" ],
"pattern": "^(?:true|false|1|0|@.{3}\\('.*'\\))$"
}
]
},
Expand Down
61 changes: 61 additions & 0 deletions src/Config/Converters/BooleanJsonConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace Azure.DataApiBuilder.Config.Converters;

/// <summary>
/// JSON converter for boolean values that also supports string representations such as
/// "true", "false", "1", and "0". Any environment variable replacement is handled by
/// other converters (for example, the string converter) before the value is parsed here.
public class BoolJsonConverter : JsonConverter<bool>
{

public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.Null)
{

throw new JsonException("Unexpected null JSON token. Expected a boolean literal or a valid @expression.");
}

if (reader.TokenType == JsonTokenType.String)
{

string? tempBoolean = JsonSerializer.Deserialize<string>(ref reader, options);

bool result = tempBoolean?.ToLower() switch
{
//numeric values have to be checked here as they may come from string replacement
"true" or "1" => true,
"false" or "0" => false,
_ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or false."),
};

return result;
}
else if (reader.TokenType == JsonTokenType.Number)
{
bool result = reader.GetInt32() switch
{
1 => true,
0 => false,
_ => throw new JsonException($"Invalid boolean value. Specify either true or false."),
};
return result;
}
else
{
return reader.GetBoolean();
}

throw new JsonException("Invalid JSON value. Expected a boolean literal or a valid @expression.");
}

public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options)
{
writer.WriteBooleanValue(value);
}
}
1 change: 1 addition & 0 deletions src/Config/RuntimeConfigLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ public static JsonSerializerOptions GetSerializationOptions(
options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings));
options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings));
options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings));
options.Converters.Add(new BoolJsonConverter());
options.Converters.Add(new FileSinkConverter(replacementSettings));

// Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly
Expand Down
80 changes: 78 additions & 2 deletions src/Service.Tests/Configuration/ConfigurationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,46 @@ type Moon {
""entities"":{ }
}";

public const string CONFIG_FILE_WITH_BOOLEAN_AS_ENV = @"{
// Link for latest draft schema.
""$schema"":""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json"",
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""sample-conn-string"",
""health"": {
""enabled"": <REPLACE_VALUE>
}
},
""runtime"": {
""health"": {
""enabled"": <REPLACE_VALUE>
},
""rest"": {
""enabled"": <REPLACE_VALUE>,
""path"": ""/api""
},
""graphql"": {
""enabled"": <REPLACE_VALUE>,
""path"": ""/graphql"",
""allow-introspection"": true
},
""host"": {
""authentication"": {
""provider"": ""AppService""
}
},
""telemetry"": {
""application-insights"":{
""enabled"": <REPLACE_VALUE>,
""connection-string"":""sample-ai-connection-string""
}

}

},
""entities"":{ }
}";

[TestCleanup]
public void CleanupAfterEachTest()
{
Expand Down Expand Up @@ -1820,8 +1860,44 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData)
JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem());

JsonSchemaValidationResult result = jsonSchemaValidator.ValidateJsonConfigWithSchema(jsonSchema, jsonData);
Assert.IsTrue(result.IsValid);
Assert.AreEqual("", String.Join('\n', result.ValidationErrors?.Select(s => $"{s.Message} at {s.Path} {s.LineNumber} {s.LinePosition}") ?? []), "Expected no validation errors.");
Assert.IsTrue(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors));

Assert.IsTrue(result.IsValid);
schemaValidatorLogger.Verify(
x => x.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((o, t) => o.ToString()!.Contains($"The config satisfies the schema requirements.")),
It.IsAny<Exception>(),
(Func<It.IsAnyType, Exception, string>)It.IsAny<object>()),
Times.Once);
}

[DataTestMethod]
[DataRow("true", DisplayName = "Validates variable boolean schema for true value")]
[DataRow("false", DisplayName = "Validates variable boolean schema for false value.")]
[DataRow("\"true\"", DisplayName = "Validates variable boolean schema for true as string.")]
[DataRow("\"false\"", DisplayName = "Validates variable boolean schema for false as string.")]
[DataRow("\"1\"", DisplayName = "Validates variable boolean schema for 1 as string.")]
[DataRow("\"0\"", DisplayName = "Validates variable boolean schema for 0as string.")]
[DataRow("\"@env('SAMPLE')\"", DisplayName = "Validates variable boolean schema for environment variables.")]
[DataRow("\"@akv('SAMPLE')\"", DisplayName = "Validates variable boolean schema for keyvaul variables.")]
public void TestBasicConfigSchemaWithFlexibleBoolean(string Value)
{
Mock<ILogger<JsonConfigSchemaValidator>> schemaValidatorLogger = new();

string jsonSchema = File.ReadAllText("dab.draft.schema.json");

JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem());

string jsonData = CONFIG_FILE_WITH_BOOLEAN_AS_ENV.Replace("<REPLACE_VALUE>", Value);
JsonSchemaValidationResult result = jsonSchemaValidator.ValidateJsonConfigWithSchema(jsonSchema, jsonData);
Assert.AreEqual("", String.Join('\n', result.ValidationErrors?.Select(s => $"{s.Message} at {s.Path} {s.LineNumber} {s.LinePosition}") ?? []), "Expected no validation errors.");

Assert.IsTrue(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors), "Validation Erros null of empty");

Assert.IsTrue(result.IsValid, "Result should be valid");
schemaValidatorLogger.Verify(
x => x.Log(
LogLevel.Information,
Expand Down Expand Up @@ -3368,7 +3444,7 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr
HttpMethod httpMethod = SqlTestHelper.ConvertRestMethodToHttpMethod(SupportedHttpVerb.Post);
string requestBody = @"{
""title"": ""Harry Potter and the Order of Phoenix"",
""publisher_id"": 1234";
""publisher_id"": 1234 }";

if (includeExtraneousFieldInRequestBody)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
using Azure.DataApiBuilder.Config.ObjectModel;
using Azure.DataApiBuilder.Service.Exceptions;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;

namespace Azure.DataApiBuilder.Service.Tests.UnitTests
{
Expand Down Expand Up @@ -213,6 +215,138 @@ private static string GetExpectedPropertyValue(string envVarName, bool replaceEn
}
}

/// <summary>
/// Test method to validate that environment variable replacement works correctly
/// for the telemetry.application-insights.enabled property when set through config
/// or through environment variables
/// </summary>
[TestMethod]
[DataRow(true, DisplayName = "ApplicationInsights.Enabled set to true (literal bool)")]
[DataRow(false, DisplayName = "ApplicationInsights.Enabled set to false (literal bool)")]
public void TestTelemetryApplicationInsightsEnabled(bool expected)
{
TestTelemetryApplicationInsightsEnabledInternal(expected.ToString().ToLower(), expected);
}

[TestMethod]
[DataRow("true", true, DisplayName = "ApplicationInsights.Enabled from string 'true'")]
[DataRow("false", false, DisplayName = "ApplicationInsights.Enabled from string 'false'")]
[DataRow("1", true, DisplayName = "ApplicationInsights.Enabled from string '1'")]
[DataRow("0", false, DisplayName = "ApplicationInsights.Enabled from string '0'")]
public void TestTelemetryApplicationInsightsEnabledFromString(string configSetting, bool expected)
{

TestTelemetryApplicationInsightsEnabledInternal($"\"{configSetting}\"", expected);
}

[TestMethod]
[DataRow("true", true, DisplayName = "ApplicationInsights.Enabled from environment 'true'")]
[DataRow("false", false, DisplayName = "ApplicationInsights.Enabled from environment 'false'")]
[DataRow("1", true, DisplayName = "ApplicationInsights.Enabled from environment '1'")]
[DataRow("0", false, DisplayName = "ApplicationInsights.Enabled from environment '0'")]
public void TestTelemetryApplicationInsightsEnabledFromEnvironment(string configSetting, bool expected)
{
// Arrange
const string envVarName = "APP_INSIGHTS_ENABLED";
string envVarValue = configSetting;
// Set up the environment variable
Environment.SetEnvironmentVariable(envVarName, envVarValue);

try
{
TestTelemetryApplicationInsightsEnabledInternal("\"@env('APP_INSIGHTS_ENABLED')\"", expected);
}
finally
{
// Cleanup
Environment.SetEnvironmentVariable(envVarName, null);
}

}
public static void TestTelemetryApplicationInsightsEnabledInternal(string configValue, bool expected)
{
string configJson = @"{
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json"",
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;""
},
""runtime"": {
""telemetry"": {
""application-insights"": {
""enabled"": " + configValue + @",
""connection-string"": ""InstrumentationKey=test-key""
}
}
},
""entities"": { }
}";

// Act
bool IsParsed = RuntimeConfigLoader.TryParseConfig(
configJson,
out RuntimeConfig runtimeConfig,
replacementSettings: new DeserializationVariableReplacementSettings(
azureKeyVaultOptions: null,
doReplaceEnvVar: true,
doReplaceAkvVar: false));

// Assert
Assert.IsTrue(IsParsed);
Assert.AreEqual("InstrumentationKey=test-key", runtimeConfig.Runtime.Telemetry.ApplicationInsights.ConnectionString, "Connection string should be preserved");
Assert.AreEqual(expected, runtimeConfig.Runtime.Telemetry.ApplicationInsights.Enabled, "ApplicationInsights enabled value should match expected value");
}

/// <summary>
///
/// </summary>
/// <param name="configValue">Value to set in the config to cause error</param>
/// <param name="message">Error message</param>
[TestMethod]
[DataRow("somenonboolean", "Invalid boolean value: somenonboolean. Specify either true or false.", DisplayName = "ApplicationInsights.Enabled invalid value should error")]
public void TestTelemetryApplicationInsightsEnabledShouldError(string configValue, string message)
{
string configJson = @"{
""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json"",
""data-source"": {
""database-type"": ""mssql"",
""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;""
},
""runtime"": {
""telemetry"": {
""application-insights"": {
""enabled"": """ + configValue + @""",
""connection-string"": ""InstrumentationKey=test-key""
}
}
},
""entities"": { }
}";

// Arrange
Mock<ILogger> mockLogger = new();

// Act
bool isParsed = RuntimeConfigLoader.TryParseConfig(
configJson,
out RuntimeConfig runtimeConfig,
replacementSettings: new DeserializationVariableReplacementSettings(
azureKeyVaultOptions: null,
doReplaceEnvVar: true,
doReplaceAkvVar: false),
logger: mockLogger.Object);

// Assert
Assert.IsFalse(isParsed);
Assert.IsNull(runtimeConfig);

Assert.AreEqual(1, mockLogger.Invocations.Count, "Should raise 1 exception");
Assert.AreEqual(5, mockLogger.Invocations[0].Arguments.Count, "Log should have 4 arguments");
var ConfigException = mockLogger.Invocations[0].Arguments[3] as JsonException;
Assert.IsInstanceOfType(ConfigException, typeof(JsonException), "Should have raised a Json Exception");
Assert.AreEqual(ConfigException.Message, message);
}

/// <summary>
/// Method to validate that comments are skipped in config file (and are ignored during deserialization).
/// </summary>
Expand Down
Loading