From add53edf49f291aca86b0c8fd12c34a5cc3384be Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Tue, 13 Jan 2026 22:44:07 +0000 Subject: [PATCH 01/19] #3053 Fix by using custom bool parsing that if a string is found then it uses the environment replacement and looks for a true false 1 or 0. --- .../Converters/BooleanJsonConverterFactory.cs | 50 ++++++++++++++ src/Config/RuntimeConfigLoader.cs | 1 + ...untimeConfigLoaderJsonDeserializerTests.cs | 65 +++++++++++++++++++ 3 files changed, 116 insertions(+) create mode 100644 src/Config/Converters/BooleanJsonConverterFactory.cs diff --git a/src/Config/Converters/BooleanJsonConverterFactory.cs b/src/Config/Converters/BooleanJsonConverterFactory.cs new file mode 100644 index 0000000000..9f9f5e059b --- /dev/null +++ b/src/Config/Converters/BooleanJsonConverterFactory.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// Custom string json converter factory to replace environment variables and other variable patterns +/// during deserialization using the DeserializationVariableReplacementSettings. +/// +public class BoolJsonConverter : JsonConverter +{ + + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) + { + + throw new JsonException(); + } + + if (reader.TokenType == JsonTokenType.String) + { + + string? tempBoolean = JsonSerializer.Deserialize(ref reader, options); + + bool result = tempBoolean?.ToLower() switch + { + "true" or "1" => true, + "false" or "0" => false, + _ => throw new JsonException($"Invalid enabled value: {tempBoolean}. Specify either true or false."), + }; + + return result; + } + else + { + return reader.GetBoolean(); + } + + throw new JsonException(); + } + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } +} diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 5996a902ed..9a54d09d8e 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -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 diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 2a0e49cf00..1559f5d3df 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -213,6 +213,71 @@ private static string GetExpectedPropertyValue(string envVarName, bool replaceEn } } + /// + /// 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 + /// + [TestMethod] + [DataRow(true, true, DisplayName = "Config with enabled set to true ")] + [DataRow(true, false, DisplayName = "Config with enabled set to false")] + [DataRow(false, true, DisplayName = "Replace environment variables containing boolean value with true")] + [DataRow(false, false, DisplayName = "Replace environment variables containing boolean value with false")] + + public void TestTelemetryApplicationInsightsEnabledWithEnvironmentVariable(bool hardcoded, bool expected) + { + // Arrange + const string envVarName = "APP_INSIGHTS_ENABLED"; + string envVarValue = expected.ToString(); + + + // Set up the environment variable + Environment.SetEnvironmentVariable(envVarName, envVarValue); + string configValue = hardcoded ? expected.ToString().ToLower() : "\"@env('APP_INSIGHTS_ENABLED')\""; + try + { + 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 isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig runtimeConfig, + replacementSettings: new DeserializationVariableReplacementSettings( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAkvVar: false)); + + // Assert + Assert.IsTrue(isParsingSuccessful, "Config parsing should succeed"); + Assert.IsNotNull(runtimeConfig, "Runtime config should not be null"); + Assert.IsNotNull(runtimeConfig.Runtime, "Runtime section should not be null"); + Assert.IsNotNull(runtimeConfig.Runtime.Telemetry, "Telemetry section should not be null"); + Assert.IsNotNull(runtimeConfig.Runtime.Telemetry.ApplicationInsights, "ApplicationInsights section should not be null"); + 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"); + } + finally + { + // Cleanup + Environment.SetEnvironmentVariable(envVarName, null); + } + } + /// /// Method to validate that comments are skipped in config file (and are ignored during deserialization). /// From 05b35868da48b844bef88e3cda0cb9df2dc2c4a5 Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:12:00 +0000 Subject: [PATCH 02/19] Update src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 1559f5d3df..f37f40b2a1 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -228,7 +228,7 @@ public void TestTelemetryApplicationInsightsEnabledWithEnvironmentVariable(bool { // Arrange const string envVarName = "APP_INSIGHTS_ENABLED"; - string envVarValue = expected.ToString(); + string envVarValue = expected.ToString(); // Set up the environment variable From 0b6e7f3102d472e3fa72ac25601fa3c0bc5242d4 Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:12:18 +0000 Subject: [PATCH 03/19] Update src/Config/Converters/BooleanJsonConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/BooleanJsonConverterFactory.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Config/Converters/BooleanJsonConverterFactory.cs b/src/Config/Converters/BooleanJsonConverterFactory.cs index 9f9f5e059b..66179bb7f3 100644 --- a/src/Config/Converters/BooleanJsonConverterFactory.cs +++ b/src/Config/Converters/BooleanJsonConverterFactory.cs @@ -7,9 +7,9 @@ namespace Azure.DataApiBuilder.Config.Converters; /// -/// Custom string json converter factory to replace environment variables and other variable patterns -/// during deserialization using the DeserializationVariableReplacementSettings. -/// +/// 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 { From c1759afdd7f5034b77c10d197129f20a713e3f6d Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:12:25 +0000 Subject: [PATCH 04/19] Update src/Config/Converters/BooleanJsonConverterFactory.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Config/Converters/BooleanJsonConverterFactory.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/BooleanJsonConverterFactory.cs b/src/Config/Converters/BooleanJsonConverterFactory.cs index 66179bb7f3..6338ff7b58 100644 --- a/src/Config/Converters/BooleanJsonConverterFactory.cs +++ b/src/Config/Converters/BooleanJsonConverterFactory.cs @@ -30,7 +30,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer { "true" or "1" => true, "false" or "0" => false, - _ => throw new JsonException($"Invalid enabled value: {tempBoolean}. Specify either true or false."), + _ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or false."), }; return result; From 36621c699049e30c2cef9a77e9cac868b6da3b99 Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Tue, 13 Jan 2026 23:12:43 +0000 Subject: [PATCH 05/19] Update src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index f37f40b2a1..09da8e2ed3 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -219,7 +219,7 @@ private static string GetExpectedPropertyValue(string envVarName, bool replaceEn /// or through environment variables /// [TestMethod] - [DataRow(true, true, DisplayName = "Config with enabled set to true ")] + [DataRow(true, true, DisplayName = "Config with enabled set to true")] [DataRow(true, false, DisplayName = "Config with enabled set to false")] [DataRow(false, true, DisplayName = "Replace environment variables containing boolean value with true")] [DataRow(false, false, DisplayName = "Replace environment variables containing boolean value with false")] From 76b0c41499cc3bb10195027a12998c76cd86b3ad Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Wed, 14 Jan 2026 01:00:32 +0000 Subject: [PATCH 06/19] Extra handling of numeric values and test coverage --- .../Converters/BooleanJsonConverterFactory.cs | 13 +- ...untimeConfigLoaderJsonDeserializerTests.cs | 137 +++++++++++++----- 2 files changed, 115 insertions(+), 35 deletions(-) diff --git a/src/Config/Converters/BooleanJsonConverterFactory.cs b/src/Config/Converters/BooleanJsonConverterFactory.cs index 6338ff7b58..df4a69c01b 100644 --- a/src/Config/Converters/BooleanJsonConverterFactory.cs +++ b/src/Config/Converters/BooleanJsonConverterFactory.cs @@ -28,13 +28,24 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer bool result = tempBoolean?.ToLower() switch { - "true" or "1" => true, + //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(); diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 09da8e2ed3..e2fa4d839f 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -15,6 +15,8 @@ using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.Data.SqlClient; using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Extensions.Logging; +using Moq; namespace Azure.DataApiBuilder.Service.Tests.UnitTests { @@ -219,24 +221,51 @@ private static string GetExpectedPropertyValue(string envVarName, bool replaceEn /// or through environment variables /// [TestMethod] - [DataRow(true, true, DisplayName = "Config with enabled set to true")] - [DataRow(true, false, DisplayName = "Config with enabled set to false")] - [DataRow(false, true, DisplayName = "Replace environment variables containing boolean value with true")] - [DataRow(false, false, DisplayName = "Replace environment variables containing boolean value with false")] + [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); + } - public void TestTelemetryApplicationInsightsEnabledWithEnvironmentVariable(bool hardcoded, bool 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 = expected.ToString(); - - + string envVarValue = configSetting; // Set up the environment variable Environment.SetEnvironmentVariable(envVarName, envVarValue); - string configValue = hardcoded ? expected.ToString().ToLower() : "\"@env('APP_INSIGHTS_ENABLED')\""; + try { - string configJson = @"{ + 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"", @@ -252,30 +281,70 @@ public void TestTelemetryApplicationInsightsEnabledWithEnvironmentVariable(bool }, ""entities"": { } }"; - - // Act - bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configJson, - out RuntimeConfig runtimeConfig, - replacementSettings: new DeserializationVariableReplacementSettings( - azureKeyVaultOptions: null, - doReplaceEnvVar: true, - doReplaceAkvVar: false)); - - // Assert - Assert.IsTrue(isParsingSuccessful, "Config parsing should succeed"); - Assert.IsNotNull(runtimeConfig, "Runtime config should not be null"); - Assert.IsNotNull(runtimeConfig.Runtime, "Runtime section should not be null"); - Assert.IsNotNull(runtimeConfig.Runtime.Telemetry, "Telemetry section should not be null"); - Assert.IsNotNull(runtimeConfig.Runtime.Telemetry.ApplicationInsights, "ApplicationInsights section should not be null"); - 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"); - } - finally - { - // Cleanup - Environment.SetEnvironmentVariable(envVarName, null); - } + + // 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"); + } + + /// + /// + /// + /// Value to set in the config to cause error + /// Error message + [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 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); } /// From 5c7f38aab2f5111f18619a72cc6f02537f385d00 Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Wed, 14 Jan 2026 01:04:41 +0000 Subject: [PATCH 07/19] rename file to match class --- .../{BooleanJsonConverterFactory.cs => BooleanJsonConverter.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/Config/Converters/{BooleanJsonConverterFactory.cs => BooleanJsonConverter.cs} (100%) diff --git a/src/Config/Converters/BooleanJsonConverterFactory.cs b/src/Config/Converters/BooleanJsonConverter.cs similarity index 100% rename from src/Config/Converters/BooleanJsonConverterFactory.cs rename to src/Config/Converters/BooleanJsonConverter.cs From 7532d683c48eef804f864ac9b83076212f954b6f Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Wed, 14 Jan 2026 19:44:51 +0000 Subject: [PATCH 08/19] Add schema solution --- schemas/dab.draft.schema.json | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index b684cc28ac..58620a41bb 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -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 }, @@ -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 @@ -1391,10 +1391,18 @@ "type": "string" } }, - "required": ["singular"] + "required": [ "singular" ] } ] }, + "boolean-or-string": { + "oneOf":[ + { + "type": [ "boolean", "string" ], + "pattern": "^(true|false|1|0|@env('.?')|@akv('.?'))$" + } + ] + } "action": { "oneOf": [ { From e3eee4a49c021fdf8b978200c52bcab217c9850b Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Wed, 14 Jan 2026 22:47:39 +0000 Subject: [PATCH 09/19] Add test to validate the pattern matching for string booleans --- schemas/dab.draft.schema.json | 4 +- .../Configuration/ConfigurationTests.cs | 66 ++++++++++++++++++- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 58620a41bb..a2efc1d069 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -1399,10 +1399,10 @@ "oneOf":[ { "type": [ "boolean", "string" ], - "pattern": "^(true|false|1|0|@env('.?')|@akv('.?'))$" + "pattern": "^(true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$" } ] - } + }, "action": { "oneOf": [ { diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 1faa8eb08a..deee0e1aed 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -663,6 +663,34 @@ 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"" + }, + ""runtime"": { + ""health"": { + ""enabled"": + }, + ""rest"": { + ""enabled"": true, + ""path"": ""/api"" + }, + ""graphql"": { + ""enabled"": true, + ""path"": ""/graphql"", + ""allow-introspection"": true + }, + ""host"": { + ""authentication"": { + ""provider"": ""AppService"" + } + } + }, + ""entities"":{ } + }"; [TestCleanup] public void CleanupAfterEachTest() @@ -1816,12 +1844,46 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData) Mock> schemaValidatorLogger = new(); string jsonSchema = File.ReadAllText("dab.draft.schema.json"); - + JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem()); 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)); + Assert.IsTrue(result.IsValid); + schemaValidatorLogger.Verify( + x => x.Log( + LogLevel.Information, + It.IsAny(), + It.Is((o, t) => o.ToString()!.Contains($"The config satisfies the schema requirements.")), + It.IsAny(), + (Func)It.IsAny()), + Times.Once); + } + + [DataTestMethod] + [DataRow("true", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [DataRow("false", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [DataRow("\"true\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [DataRow("\"false\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [DataRow("\"@env('SAMPLE')\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [DataRow("\"@akv('SAMPLE')\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + public void TestBasicConfigSchemaWithFlexibleBoolean(string Value) + { + Mock> 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("", 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)); + + Assert.IsTrue(result.IsValid); schemaValidatorLogger.Verify( x => x.Log( LogLevel.Information, @@ -3368,7 +3430,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) { From 17f106754cd0f5989cbaa3b71579e9fd3e6d9b8b Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Wed, 14 Jan 2026 23:29:31 +0000 Subject: [PATCH 10/19] Apply flexible enabled to rest, graphql, health and application-insghts --- schemas/dab.draft.schema.json | 8 ++-- .../Configuration/ConfigurationTests.cs | 42 ++++++++++++------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index a2efc1d069..9ff5e771ff 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -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": { @@ -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": { @@ -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 } @@ -481,7 +481,7 @@ "additionalProperties": false, "properties": { "enabled": { - "type": "boolean", + "$ref": "#/$defs/boolean-or-string", "description": "Allow enabling/disabling Azure Log Analytics.", "default": false }, diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index deee0e1aed..06c3c215e9 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -663,23 +663,27 @@ type Moon { }, ""entities"":{ } }"; - public const string CONFIG_FILE_WITH_BOOLEAN_AS_ENV = @"{ + + 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"" + ""database-type"": ""mssql"", + ""connection-string"": ""sample-conn-string"", + ""health"": { + ""enabled"": + } }, ""runtime"": { ""health"": { ""enabled"": }, ""rest"": { - ""enabled"": true, + ""enabled"": , ""path"": ""/api"" }, ""graphql"": { - ""enabled"": true, + ""enabled"": , ""path"": ""/graphql"", ""allow-introspection"": true }, @@ -687,10 +691,18 @@ type Moon { ""authentication"": { ""provider"": ""AppService"" } + }, + ""telemetry"": { + ""application-insights"":{ + ""enabled"": , + ""connection-string"":""sample-ai-connection-string"" + } + } + }, ""entities"":{ } - }"; + }" ; [TestCleanup] public void CleanupAfterEachTest() @@ -1863,12 +1875,14 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData) } [DataTestMethod] - [DataRow("true", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] - [DataRow("false", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] - [DataRow("\"true\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] - [DataRow("\"false\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] - [DataRow("\"@env('SAMPLE')\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] - [DataRow("\"@akv('SAMPLE')\"", DisplayName = "Validates schema of the config file with boolean values set using environment variables.")] + [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> schemaValidatorLogger = new(); @@ -1881,9 +1895,9 @@ public void TestBasicConfigSchemaWithFlexibleBoolean(string 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)); + Assert.IsTrue(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors),"Validation Erros null of empty"); - Assert.IsTrue(result.IsValid); + Assert.IsTrue(result.IsValid,"Result should be valid"); schemaValidatorLogger.Verify( x => x.Log( LogLevel.Information, From 2edb0e4e4a98cb3ab22639df0705aaec8bf6f00f Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Thu, 15 Jan 2026 00:34:29 +0000 Subject: [PATCH 11/19] fix formatting --- src/Config/Converters/BooleanJsonConverter.cs | 6 +++--- .../Configuration/ConfigurationTests.cs | 16 ++++++++-------- .../RuntimeConfigLoaderJsonDeserializerTests.cs | 16 ++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/Config/Converters/BooleanJsonConverter.cs b/src/Config/Converters/BooleanJsonConverter.cs index df4a69c01b..d6f401853e 100644 --- a/src/Config/Converters/BooleanJsonConverter.cs +++ b/src/Config/Converters/BooleanJsonConverter.cs @@ -12,7 +12,7 @@ namespace Azure.DataApiBuilder.Config.Converters; /// other converters (for example, the string converter) before the value is parsed here. public class BoolJsonConverter : JsonConverter { - + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.Null) @@ -29,7 +29,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer bool result = tempBoolean?.ToLower() switch { //numeric values have to be checked here as they may come from string replacement - "true" or "1"=> true, + "true" or "1" => true, "false" or "0" => false, _ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or false."), }; @@ -42,7 +42,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer { 1 => true, 0 => false, - _ => throw new JsonException($"Invalid boolean value. Specify either true or false."), + _ => throw new JsonException($"Invalid boolean value. Specify either true or false."), }; return result; } diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 06c3c215e9..f6650dd349 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -664,7 +664,7 @@ type Moon { ""entities"":{ } }"; - public const string CONFIG_FILE_WITH_BOOLEAN_AS_ENV = @"{ + 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"": { @@ -702,7 +702,7 @@ type Moon { }, ""entities"":{ } - }" ; + }"; [TestCleanup] public void CleanupAfterEachTest() @@ -1856,13 +1856,13 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData) Mock> schemaValidatorLogger = new(); string jsonSchema = File.ReadAllText("dab.draft.schema.json"); - + JsonConfigSchemaValidator jsonSchemaValidator = new(schemaValidatorLogger.Object, new MockFileSystem()); 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.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( @@ -1874,7 +1874,7 @@ public void TestBasicConfigSchemaWithNoOptionalFieldsIsValid(string jsonData) Times.Once); } - [DataTestMethod] + [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.")] @@ -1895,9 +1895,9 @@ public void TestBasicConfigSchemaWithFlexibleBoolean(string 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(EnumerableUtilities.IsNullOrEmpty(result.ValidationErrors), "Validation Erros null of empty"); - Assert.IsTrue(result.IsValid,"Result should be valid"); + Assert.IsTrue(result.IsValid, "Result should be valid"); schemaValidatorLogger.Verify( x => x.Log( LogLevel.Information, diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index e2fa4d839f..1b06d3cea9 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -14,8 +14,8 @@ using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.Data.SqlClient; -using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; using Moq; namespace Azure.DataApiBuilder.Service.Tests.UnitTests @@ -223,7 +223,7 @@ private static string GetExpectedPropertyValue(string envVarName, bool replaceEn [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) + public void TestTelemetryApplicationInsightsEnabled(bool expected) { TestTelemetryApplicationInsightsEnabledInternal(expected.ToString().ToLower(), expected); } @@ -235,7 +235,7 @@ public void TestTelemetryApplicationInsightsEnabled( bool expected) [DataRow("0", false, DisplayName = "ApplicationInsights.Enabled from string '0'")] public void TestTelemetryApplicationInsightsEnabledFromString(string configSetting, bool expected) { - + TestTelemetryApplicationInsightsEnabledInternal($"\"{configSetting}\"", expected); } @@ -263,7 +263,7 @@ public void TestTelemetryApplicationInsightsEnabledFromEnvironment(string config } } - public static void TestTelemetryApplicationInsightsEnabledInternal(string configValue, bool expected) + 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"", @@ -324,7 +324,7 @@ public void TestTelemetryApplicationInsightsEnabledShouldError(string configValu }"; // Arrange - Mock mockLogger = new(); + Mock mockLogger = new(); // Act bool isParsed = RuntimeConfigLoader.TryParseConfig( @@ -340,11 +340,11 @@ public void TestTelemetryApplicationInsightsEnabledShouldError(string configValu 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"); + 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); + Assert.AreEqual(ConfigException.Message, message); } /// From 856052c7a3499bb528bd7f3b026b874eb59e71dc Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Thu, 15 Jan 2026 09:17:33 +0000 Subject: [PATCH 12/19] Normalise line endings --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index a224f6bcbb..8d16315cf8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 From 928bb0b63b35e01951b578d207b1a86e7d19552e Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Thu, 15 Jan 2026 12:09:16 +0000 Subject: [PATCH 13/19] fix pre commit hook docs on formatting --- CONTRIBUTING.md | 37 +++++++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ab98fbe8cf..a9fd633f27 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -95,7 +95,7 @@ We use `dotnet format` to enforce code conventions. It is run automatically in C #### Enforcing code style with git hooks -You can copy paste the following commands to install a git pre-commit hook. This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary. +You can copy paste the following commands to install a git pre-commit hook (creates a pre-commit file in your .git folder, which isn't shown in vs code). This will cause a commit to fail if you forgot to run `dotnet format`. If you have run on save enabled in your editor this is not necessary. ```bash cat > .git/hooks/pre-commit << __EOF__ @@ -112,17 +112,42 @@ if [ "\$(get_files)" = '' ]; then fi get_files | - xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\ - --check \\ - --fix-whitespace --fix-style warn --fix-analyzers warn \\ + xargs dotnet format src/Azure.DataApiBuilder.sln \\ + --verify-no-changes --include \\ || { get_files | - xargs dotnet format src/Azure.DataApiBuilder.Service.sln \\ - --fix-whitespace --fix-style warn --fix-analyzers warn \\ + xargs dotnet format src/Azure.DataApiBuilder.sln \\ --include exit 1 } __EOF__ chmod +x .git/hooks/pre-commit ``` + +The file should look like this + +``` bash +#!/bin/bash +set -euo pipefail + +get_files() { + git diff --cached --name-only --diff-filter=ACMR | \ + grep '\.cs$' +} + +if [ "$(get_files)" = '' ]; then + exit 0 +fi + +get_files | + xargs dotnet format src/Azure.DataApiBuilder.sln \ + --verify-no-changes \ + --include \ + || { + get_files | + xargs dotnet format src/Azure.DataApiBuilder.sln \ + --include + exit 1 +} +``` From 6b244045a1d9d32f8f6047b1ce20b880ff20e06f Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:00:59 +0000 Subject: [PATCH 14/19] update to all for any 3 letter prefix for substitution Co-authored-by: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> --- schemas/dab.draft.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 9ff5e771ff..b8ad1d5df4 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -1399,7 +1399,7 @@ "oneOf":[ { "type": [ "boolean", "string" ], - "pattern": "^(true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$" + "pattern": "^(?:true|false|1|0|@.{3}\\('.*'\\))$" } ] }, From af5dee45af3bb8d6598df0df70a34c45995c0455 Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:01:29 +0000 Subject: [PATCH 15/19] Better exception if null found Co-authored-by: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> --- src/Config/Converters/BooleanJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/BooleanJsonConverter.cs b/src/Config/Converters/BooleanJsonConverter.cs index d6f401853e..7e9bdc5f0e 100644 --- a/src/Config/Converters/BooleanJsonConverter.cs +++ b/src/Config/Converters/BooleanJsonConverter.cs @@ -51,7 +51,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer return reader.GetBoolean(); } - throw new JsonException(); + throw new JsonException("Invalid JSON value. Expected a boolean literal or a valid @expression."); } public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) From 2eaf189cc5a20f51c4a95d44a26607a80f60bc37 Mon Sep 17 00:00:00 2001 From: Simon Sabin <1209963+simonsabin@users.noreply.github.com> Date: Thu, 15 Jan 2026 18:13:21 +0000 Subject: [PATCH 16/19] Update src/Config/Converters/BooleanJsonConverter.cs Co-authored-by: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> --- src/Config/Converters/BooleanJsonConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/Converters/BooleanJsonConverter.cs b/src/Config/Converters/BooleanJsonConverter.cs index 7e9bdc5f0e..a85eb6fc74 100644 --- a/src/Config/Converters/BooleanJsonConverter.cs +++ b/src/Config/Converters/BooleanJsonConverter.cs @@ -18,7 +18,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer if (reader.TokenType is JsonTokenType.Null) { - throw new JsonException(); + throw new JsonException("Unexpected null JSON token. Expected a boolean literal or a valid @expression."); } if (reader.TokenType == JsonTokenType.String) From a90127530bae1f4df109dba9637cfebbfaecefe1 Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Thu, 15 Jan 2026 22:21:09 +0000 Subject: [PATCH 17/19] remove tests no longer valid, 1 and 0 are valid for booleans now --- src/Service.Tests/Caching/CachingConfigProcessingTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index 00729476cd..1294c009da 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -161,8 +161,6 @@ public void GlobalCacheOptionsDeserialization_ValidValues( [DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 0 }", DisplayName = "EntityCacheOptions.TtlSeconds set to zero is invalid configuration.")] [DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": -1 }", DisplayName = "EntityCacheOptions.TtlSeconds set to negative number is invalid configuration.")] [DataRow(@",""cache"": { ""enabled"": true, ""ttl-seconds"": 1.1 }", DisplayName = "EntityCacheOptions.TtlSeconds set to decimal is invalid configuration.")] - [DataRow(@",""cache"": { ""enabled"": 1 }", DisplayName = "EntityCacheOptions.Enabled property set to 1 should fail because not a boolean.")] - [DataRow(@",""cache"": { ""enabled"": 0 }", DisplayName = "EntityCacheOptions.Enabled property set to 0 should fail because not a boolean.")] [DataRow(@",""cache"": 1", DisplayName = "EntityCacheOptions property set to 1 should fail because it's not a JSON object.")] [DataRow(@",""cache"": 0", DisplayName = "EntityCacheOptions property set to 0 should fail because it's not a JSON object.")] [DataRow(@",""cache"": true", DisplayName = "EntityCacheOptions property set to true should fail because it's not a JSON object.")] From 17995ab455f16e06a3a54250d721cf3449a2483d Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Fri, 16 Jan 2026 00:21:08 +0000 Subject: [PATCH 18/19] revert back to @env\\('.*'\\)|@akv\\('.*'\\)| --- schemas/dab.draft.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index b8ad1d5df4..920c0a4da6 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -1399,7 +1399,7 @@ "oneOf":[ { "type": [ "boolean", "string" ], - "pattern": "^(?:true|false|1|0|@.{3}\\('.*'\\))$" + "pattern": "^(?:true|false|1|0|@env\\('.*'\\)|@akv\\('.*'\\))$" } ] }, From ab1c6e0e46bc367fce8e1e4820f08a97f9a7d831 Mon Sep 17 00:00:00 2001 From: Simon Sabin Date: Fri, 16 Jan 2026 13:06:14 +0000 Subject: [PATCH 19/19] Changes following review Added missing summary closing tag Removed redundant throw Clarified rules for booleans --- src/Config/Converters/BooleanJsonConverter.cs | 5 +- .../Configuration/ConfigurationTests.cs | 2 +- .../UnitTests/EnvironmentTests copy.cs | 100 ++++++++++++++++++ 3 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 src/Service.Tests/UnitTests/EnvironmentTests copy.cs diff --git a/src/Config/Converters/BooleanJsonConverter.cs b/src/Config/Converters/BooleanJsonConverter.cs index a85eb6fc74..9c4ce16a68 100644 --- a/src/Config/Converters/BooleanJsonConverter.cs +++ b/src/Config/Converters/BooleanJsonConverter.cs @@ -10,6 +10,7 @@ namespace Azure.DataApiBuilder.Config.Converters; /// 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 { @@ -31,7 +32,7 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer //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."), + _ => throw new JsonException($"Invalid boolean value: {tempBoolean}. Specify either true or 1 for true, false or 0 for false"), }; return result; @@ -50,8 +51,6 @@ public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSer { 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) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index f6650dd349..9df54be519 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -3444,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) { diff --git a/src/Service.Tests/UnitTests/EnvironmentTests copy.cs b/src/Service.Tests/UnitTests/EnvironmentTests copy.cs new file mode 100644 index 0000000000..b658e3283d --- /dev/null +++ b/src/Service.Tests/UnitTests/EnvironmentTests copy.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.UnitTests; + + +/// +/// Contains test involving environment variables. +/// +[TestClass] +public class EnvironmentTests3 +{ + private const string ASPNETCORE_URLS_NAME = "ASPNETCORE_URLS"; + + /// + /// Tests the behavior of the Main method when the ASPNETCORE_URLS environment variable is set to an invalid value. + /// + /// + /// This test sets the ASPNETCORE_URLS environment variable to an invalid value, invokes the Main method, + /// and verifies that the application exits with an error code of -1. Additionally, it checks if the error message + /// contains the name of the invalid environment variable. + /// + [TestMethod] + public void Main_WhenAspNetCoreUrlsInvalid_ShouldExitWithError() + { + const string ASPNETCORE_URLS_INVALID_VALUE = nameof(Main_WhenAspNetCoreUrlsInvalid_ShouldExitWithError); + string originalEnvValue = Environment.GetEnvironmentVariable(ASPNETCORE_URLS_NAME); + + // Arrange + Environment.SetEnvironmentVariable(ASPNETCORE_URLS_NAME, ASPNETCORE_URLS_INVALID_VALUE); + using StringWriter consoleOutput = new(); + Console.SetError(consoleOutput); + + // Act + Program.Main(Array.Empty()); + + // Assert + Assert.AreEqual(-1, Environment.ExitCode); + StringAssert.Contains(consoleOutput.ToString(), ASPNETCORE_URLS_NAME, StringComparison.Ordinal); + + // Cleanup + Environment.SetEnvironmentVariable(ASPNETCORE_URLS_NAME, originalEnvValue); + } + + /// + /// Tests the `ValidateAspNetCoreUrls` method with various inputs to ensure it correctly validates URLs. + /// + [DataTestMethod] + [DataRow(null, true, DisplayName = "null input")] + [DataRow("", true, DisplayName = "empty string")] + [DataRow(" ", false, DisplayName = "whitespace only")] + [DataRow("http://localhost", true, DisplayName = "valid URL")] + [DataRow("https://localhost", true, DisplayName = "valid secure URL")] + [DataRow("http://127.0.0.1:5000", true, DisplayName = "valid IP URL")] + [DataRow("https://127.0.0.1:5001", true, DisplayName = "valid secure IP URL")] + [DataRow("http://[::1]:80", true, DisplayName = "valid IPv6 URL")] + [DataRow("https://[::1]:443", true, DisplayName = "valid secure IPv6 URL")] + [DataRow("http://+:80/", true, DisplayName = "wildcard '+' host")] + [DataRow("https://+:443/", true, DisplayName = "secure wildcard '+' host")] + [DataRow("http://*:80/", true, DisplayName = "wildcard '*' host")] + [DataRow("https://*:443/", true, DisplayName = "secure wildcard '*' host")] + [DataRow("http://localhost:80/;https://localhost:443/", true, DisplayName = "semicolon-separated URLs")] + [DataRow("http://localhost:80/ https://localhost:443/", true, DisplayName = "space-separated URLs")] + [DataRow("http://localhost:80/,https://localhost:443/", true, DisplayName = "comma-separated URLs")] + [DataRow("ftp://localhost:21", false, DisplayName = "invalid scheme (ftp)")] + [DataRow("localhost:80", false, DisplayName = "missing scheme")] + [DataRow("http://", false, DisplayName = "incomplete URL")] + [DataRow("http://unix:/var/run/app.sock", true, DisplayName = "unix socket (Linux)")] + [DataRow("https://unix:/var/run/app.sock", true, DisplayName = "secure unix socket (Linux)")] + [DataRow("http://unix:var/run/app.sock", false, DisplayName = "unix socket missing slash")] + [DataRow("http://unix:", false, DisplayName = "unix socket missing path")] + [DataRow("http://unix:/var/run/app.sock;https://unix:/var/run/app2.sock", true, DisplayName = "multiple unix sockets (Linux)")] + [DataRow("http://localhost:80/;ftp://localhost:21", false, DisplayName = "mixed valid/invalid schemes")] + [DataRow(" http://localhost:80/ ", true, DisplayName = "trimmed whitespace")] + public void ValidateAspNetCoreUrls_Test(string input, bool expected) + { + // Arrange + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Linux) && + input is not null && + (input.StartsWith("http://unix:", StringComparison.OrdinalIgnoreCase) || + input.StartsWith("https://unix:", StringComparison.OrdinalIgnoreCase))) + { + expected = false; + } + + string originalEnvValue = Environment.GetEnvironmentVariable(ASPNETCORE_URLS_NAME); + Environment.SetEnvironmentVariable(ASPNETCORE_URLS_NAME, input); + + // Act + Assert.AreEqual(expected, Program.ValidateAspNetCoreUrls()); + + // Cleanup + Environment.SetEnvironmentVariable(ASPNETCORE_URLS_NAME, originalEnvValue); + } +}