|
9 | 9 | using System.Linq; |
10 | 10 | using System.Threading.Tasks; |
11 | 11 | using Azure.DataApiBuilder.Config; |
| 12 | +using Azure.DataApiBuilder.Config.Converters; |
12 | 13 | using Azure.DataApiBuilder.Config.ObjectModel; |
13 | 14 | using Microsoft.VisualStudio.TestTools.UnitTesting; |
14 | 15 | using Newtonsoft.Json.Linq; |
@@ -131,4 +132,154 @@ public async Task CanLoadValidMultiSourceConfigWithAutoentities(string configPat |
131 | 132 | Assert.IsTrue(runtimeConfig.SqlDataSourceUsed, "Should have Sql data source"); |
132 | 133 | Assert.AreEqual(expectedEntities, runtimeConfig.Entities.Entities.Count, "Number of entities is not what is expected."); |
133 | 134 | } |
| 135 | + |
| 136 | + /// <summary> |
| 137 | + /// Validates that when a child config contains @env('...') references to environment variables |
| 138 | + /// that do not exist, the config still loads successfully because the child config uses |
| 139 | + /// EnvironmentVariableReplacementFailureMode.Ignore (matching the parent config behavior). |
| 140 | + /// Regression test for https://github.com/Azure/data-api-builder/issues/3271 |
| 141 | + /// </summary> |
| 142 | + [TestMethod] |
| 143 | + public async Task ChildConfigWithMissingEnvVarsLoadsSuccessfully() |
| 144 | + { |
| 145 | + string parentConfig = await File.ReadAllTextAsync("Multidab-config.MsSql.json"); |
| 146 | + |
| 147 | + // Child config references env vars that do not exist in the environment. |
| 148 | + string childConfig = @"{ |
| 149 | + ""$schema"": ""https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch/dab.draft.schema.json"", |
| 150 | + ""data-source"": { |
| 151 | + ""database-type"": ""mssql"", |
| 152 | + ""connection-string"": ""Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;"" |
| 153 | + }, |
| 154 | + ""runtime"": { |
| 155 | + ""rest"": { ""enabled"": true }, |
| 156 | + ""graphql"": { ""enabled"": true }, |
| 157 | + ""host"": { |
| 158 | + ""cors"": { ""origins"": [] }, |
| 159 | + ""authentication"": { ""provider"": ""StaticWebApps"" } |
| 160 | + }, |
| 161 | + ""telemetry"": { |
| 162 | + ""open-telemetry"": { |
| 163 | + ""enabled"": true, |
| 164 | + ""endpoint"": ""@env('NONEXISTENT_OTEL_ENDPOINT')"", |
| 165 | + ""headers"": ""@env('NONEXISTENT_OTEL_HEADERS')"", |
| 166 | + ""service-name"": ""@env('NONEXISTENT_OTEL_SERVICE_NAME')"" |
| 167 | + } |
| 168 | + } |
| 169 | + }, |
| 170 | + ""entities"": { |
| 171 | + ""ChildEntity"": { |
| 172 | + ""source"": ""dbo.ChildTable"", |
| 173 | + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""read""] }] |
| 174 | + } |
| 175 | + } |
| 176 | + }"; |
| 177 | + |
| 178 | + // Save original env var values and clear them to ensure they don't exist. |
| 179 | + string? origEndpoint = Environment.GetEnvironmentVariable("NONEXISTENT_OTEL_ENDPOINT"); |
| 180 | + string? origHeaders = Environment.GetEnvironmentVariable("NONEXISTENT_OTEL_HEADERS"); |
| 181 | + string? origServiceName = Environment.GetEnvironmentVariable("NONEXISTENT_OTEL_SERVICE_NAME"); |
| 182 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_ENDPOINT", null); |
| 183 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_HEADERS", null); |
| 184 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_SERVICE_NAME", null); |
| 185 | + |
| 186 | + // Write the child config to a unique temp file because the RuntimeConfig |
| 187 | + // constructor creates a real FileSystem to load child data-source-files. |
| 188 | + string childFilePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".json"); |
| 189 | + try |
| 190 | + { |
| 191 | + await File.WriteAllTextAsync(childFilePath, childConfig); |
| 192 | + |
| 193 | + JObject parentJson = JObject.Parse(parentConfig); |
| 194 | + parentJson.Add("data-source-files", new JArray(childFilePath)); |
| 195 | + string parentJsonStr = parentJson.ToString(); |
| 196 | + |
| 197 | + MockFileSystem fs = new(new Dictionary<string, MockFileData>() |
| 198 | + { |
| 199 | + { "dab-config.json", new MockFileData(parentJsonStr) } |
| 200 | + }); |
| 201 | + |
| 202 | + FileSystemRuntimeConfigLoader loader = new(fs); |
| 203 | + |
| 204 | + DeserializationVariableReplacementSettings replacementSettings = new( |
| 205 | + azureKeyVaultOptions: null, |
| 206 | + doReplaceEnvVar: true, |
| 207 | + doReplaceAkvVar: false, |
| 208 | + envFailureMode: EnvironmentVariableReplacementFailureMode.Ignore); |
| 209 | + |
| 210 | + Assert.IsTrue( |
| 211 | + loader.TryLoadConfig("dab-config.json", out RuntimeConfig runtimeConfig, replacementSettings: replacementSettings), |
| 212 | + "Config should load successfully even when child config has missing env vars."); |
| 213 | + |
| 214 | + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("ChildEntity"), "Child config entity should be merged into the parent config."); |
| 215 | + } |
| 216 | + finally |
| 217 | + { |
| 218 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_ENDPOINT", origEndpoint); |
| 219 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_HEADERS", origHeaders); |
| 220 | + Environment.SetEnvironmentVariable("NONEXISTENT_OTEL_SERVICE_NAME", origServiceName); |
| 221 | + |
| 222 | + if (File.Exists(childFilePath)) |
| 223 | + { |
| 224 | + File.Delete(childFilePath); |
| 225 | + } |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + /// <summary> |
| 230 | + /// Validates that when a child config file exists but contains invalid content, |
| 231 | + /// the parent config loading fails instead of silently skipping the child. |
| 232 | + /// Non-existent child files are intentionally skipped to support late-configured scenarios. |
| 233 | + /// Regression test for https://github.com/Azure/data-api-builder/issues/3271 |
| 234 | + /// </summary> |
| 235 | + [TestMethod] |
| 236 | + public async Task ChildConfigLoadFailureHaltsParentConfigLoading() |
| 237 | + { |
| 238 | + string parentConfig = await File.ReadAllTextAsync("Multidab-config.MsSql.json"); |
| 239 | + |
| 240 | + // Use a real temp file with invalid JSON so the file exists but fails to parse. |
| 241 | + string invalidChildPath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".json"); |
| 242 | + |
| 243 | + try |
| 244 | + { |
| 245 | + await File.WriteAllTextAsync(invalidChildPath, "{ this is not valid json }"); |
| 246 | + |
| 247 | + JObject parentJson = JObject.Parse(parentConfig); |
| 248 | + parentJson.Add("data-source-files", new JArray(invalidChildPath)); |
| 249 | + string parentJsonStr = parentJson.ToString(); |
| 250 | + |
| 251 | + MockFileSystem fs = new(new Dictionary<string, MockFileData>() |
| 252 | + { |
| 253 | + { "dab-config.json", new MockFileData(parentJsonStr) } |
| 254 | + }); |
| 255 | + |
| 256 | + FileSystemRuntimeConfigLoader loader = new(fs); |
| 257 | + |
| 258 | + TextWriter originalError = Console.Error; |
| 259 | + StringWriter sw = new(); |
| 260 | + |
| 261 | + try |
| 262 | + { |
| 263 | + Console.SetError(sw); |
| 264 | + |
| 265 | + bool loaded = loader.TryLoadConfig("dab-config.json", out RuntimeConfig _); |
| 266 | + string error = sw.ToString(); |
| 267 | + |
| 268 | + Assert.IsFalse(loaded, "Config loading should fail when a child config file exists but cannot be parsed."); |
| 269 | + Assert.IsTrue(error.Contains("Failed to load datasource file"), "Error message should indicate the child config file that failed to load."); |
| 270 | + } |
| 271 | + finally |
| 272 | + { |
| 273 | + Console.SetError(originalError); |
| 274 | + sw.Dispose(); |
| 275 | + } |
| 276 | + } |
| 277 | + finally |
| 278 | + { |
| 279 | + if (File.Exists(invalidChildPath)) |
| 280 | + { |
| 281 | + File.Delete(invalidChildPath); |
| 282 | + } |
| 283 | + } |
| 284 | + } |
134 | 285 | } |
0 commit comments