From 1ebdfb910ed7976b24b6c0d7786bf73009d01cf4 Mon Sep 17 00:00:00 2001 From: Nanook Date: Sat, 6 Jun 2026 03:50:09 +0000 Subject: [PATCH] fix: replace env defaults for stored procedures --- .../EntitySourceConverterFactory.cs | 13 +- ...untimeConfigLoaderJsonDeserializerTests.cs | 56 +++++++++ .../UnitTests/SqlExecuteStructureUnitTests.cs | 114 ++++++++++++++++++ 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/Service.Tests/UnitTests/SqlExecuteStructureUnitTests.cs diff --git a/src/Config/Converters/EntitySourceConverterFactory.cs b/src/Config/Converters/EntitySourceConverterFactory.cs index 2edafe31e1..453d107a2f 100644 --- a/src/Config/Converters/EntitySourceConverterFactory.cs +++ b/src/Config/Converters/EntitySourceConverterFactory.cs @@ -65,7 +65,7 @@ public EntitySourceConverter(DeserializationVariableReplacementSettings? replace List paramList = []; foreach (JsonProperty prop in parametersElement.EnumerateObject()) { - string? defaultValue = GetClrValue(prop.Value)?.ToString(); + string? defaultValue = GetClrValue(prop.Value, _replacementSettings)?.ToString(); paramList.Add(new ParameterMetadata { Name = prop.Name, @@ -112,11 +112,11 @@ public EntitySourceConverter(DeserializationVariableReplacementSettings? replace } } - private static object GetClrValue(JsonElement element) + private static object GetClrValue(JsonElement element, DeserializationVariableReplacementSettings? replacementSettings) { return element.ValueKind switch { - JsonValueKind.String => element.GetString() ?? string.Empty, + JsonValueKind.String => DeserializeStringValue(element, replacementSettings) ?? string.Empty, JsonValueKind.Number => GetNumberValue(element), JsonValueKind.True => true, JsonValueKind.False => false, @@ -124,6 +124,13 @@ private static object GetClrValue(JsonElement element) }; } + private static string? DeserializeStringValue(JsonElement element, DeserializationVariableReplacementSettings? replacementSettings) + { + Utf8JsonReader reader = new(JsonSerializer.SerializeToUtf8Bytes(element.GetString())); + reader.Read(); + return reader.DeserializeString(replacementSettings); + } + /// /// Attempts to get the correct numeric value from the . /// If all possible numeric values are exhausted, the raw text is returned. diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 3dfaf71b2e..222a992631 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -165,6 +165,62 @@ public void TestConfigParsingWithEnvVarReplacement(bool replaceEnvVar, string da ClearEnvironmentVariablesFromDictionary(_environmentFileContentDict); } + [DataTestMethod] + [DataRow(false, "@env('year-end')", DisplayName = "Do not replace stored procedure parameter default environment variable.")] + [DataRow(true, "2024-06-30", DisplayName = "Replace stored procedure parameter default environment variable.")] + public void TestStoredProcedureParameterDefaultEnvVarReplacement(bool replaceEnvVar, string expectedDefaultValue) + { + const string envVarName = "year-end"; + Environment.SetEnvironmentVariable(envVarName, "2024-06-30"); + + try + { + string configJson = @" + { + ""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;"" + }, + ""entities"": { + ""GetRecordsByDate"": { + ""source"": { + ""object"": ""get_records_by_date"", + ""type"": ""stored-procedure"", + ""parameters"": { + ""YearEndDate"": ""@env('year-end')"" + } + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [ + { + ""action"": ""execute"" + } + ] + } + ] + } + } + }"; + + bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig runtimeConfig, + replacementSettings: new DeserializationVariableReplacementSettings( + azureKeyVaultOptions: null, + doReplaceEnvVar: replaceEnvVar, + doReplaceAkvVar: false)); + + Assert.IsTrue(isParsingSuccessful); + Assert.AreEqual(expectedDefaultValue, runtimeConfig.Entities["GetRecordsByDate"].Source.Parameters[0].Default); + } + finally + { + Environment.SetEnvironmentVariable(envVarName, null); + } + } + /// /// Test the parsing of DataSource section in runtime config for cosmosdb_nosql /// where under cosmosDb options has database as null, container is not provided, and schema is an empty string. diff --git a/src/Service.Tests/UnitTests/SqlExecuteStructureUnitTests.cs b/src/Service.Tests/UnitTests/SqlExecuteStructureUnitTests.cs new file mode 100644 index 0000000000..e98041b9ef --- /dev/null +++ b/src/Service.Tests/UnitTests/SqlExecuteStructureUnitTests.cs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Data; +using Azure.DataApiBuilder.Auth; +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Models; +using Azure.DataApiBuilder.Core.Resolvers; +using Azure.DataApiBuilder.Core.Services; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; + +namespace Azure.DataApiBuilder.Service.Tests.UnitTests +{ + [TestClass] + public class SqlExecuteStructureUnitTests + { + [TestMethod] + public void ConfigDefaultDateTimeParameterIsResolvedAsDateTime() + { + const string entityName = "GetRecordsByDate"; + const string parameterName = "YearEndDate"; + const string envVarName = "year-end"; + DateTime expectedDate = new(year: 2024, month: 6, day: 30, hour: 0, minute: 0, second: 0, kind: DateTimeKind.Utc); + + Environment.SetEnvironmentVariable(envVarName, "2024-06-30"); + try + { + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + GetStoredProcedureConfigWithEnvDefault(), + out RuntimeConfig runtimeConfig, + replacementSettings: new DeserializationVariableReplacementSettings( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAkvVar: false))); + + StoredProcedureDefinition storedProcedureDefinition = new(); + storedProcedureDefinition.Parameters.Add( + parameterName, + new ParameterDefinition + { + SystemType = typeof(DateTime), + DbType = DbType.DateTime, + HasConfigDefault = true, + ConfigDefaultValue = runtimeConfig.Entities[entityName].Source.Parameters[0].Default + }); + + DatabaseStoredProcedure storedProcedure = new(schemaName: "dbo", tableName: "get_records_by_date") + { + SourceType = EntitySourceType.StoredProcedure, + StoredProcedureDefinition = storedProcedureDefinition + }; + + Mock metadataProvider = new(); + metadataProvider.SetupGet(x => x.EntityToDatabaseObject).Returns(new Dictionary + { + { entityName, storedProcedure } + }); + metadataProvider.Setup(x => x.GetStoredProcedureDefinition(entityName)).Returns(storedProcedureDefinition); + metadataProvider.Setup(x => x.GetSourceDefinition(entityName)).Returns(storedProcedureDefinition); + + SqlExecuteStructure executeStructure = new( + entityName, + metadataProvider.Object, + Mock.Of(), + null, + new Dictionary()); + + string dbConnectionParameterName = (string)executeStructure.ProcedureParameters[parameterName]; + Assert.AreEqual(expectedDate, executeStructure.Parameters[dbConnectionParameterName].Value); + } + finally + { + Environment.SetEnvironmentVariable(envVarName, null); + } + } + + private static string GetStoredProcedureConfigWithEnvDefault() + { + return @" + { + ""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;"" + }, + ""entities"": { + ""GetRecordsByDate"": { + ""source"": { + ""object"": ""get_records_by_date"", + ""type"": ""stored-procedure"", + ""parameters"": { + ""YearEndDate"": ""@env('year-end')"" + } + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [ + { + ""action"": ""execute"" + } + ] + } + ] + } + } + }"; + } + } +}