diff --git a/.autover/changes/976aa8d4-d143-4f7b-9bf5-762b166d7fb5.json b/.autover/changes/976aa8d4-d143-4f7b-9bf5-762b166d7fb5.json new file mode 100644 index 0000000..d0aa4fa --- /dev/null +++ b/.autover/changes/976aa8d4-d143-4f7b-9bf5-762b166d7fb5.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Extensions.Configuration.SystemsManager", + "Type": "Patch", + "ChangelogMessages": [ + "Removed content type check for app config. We now always try to parse json first" + ] + } + ] +} \ No newline at end of file diff --git a/src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj b/src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj index fb57492..9f53018 100644 --- a/src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj +++ b/src/Amazon.Extensions.Configuration.SystemsManager/Amazon.Extensions.Configuration.SystemsManager.csproj @@ -38,7 +38,11 @@ - + + + + + true ..\..\public.snk diff --git a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs index 094fde8..a1a2f82 100644 --- a/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs +++ b/src/Amazon.Extensions.Configuration.SystemsManager/AppConfig/AppConfigProcessor.cs @@ -17,6 +17,7 @@ using System.Collections.Generic; using System.IO; using System.Net.Http; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Amazon.AppConfigData; @@ -162,20 +163,17 @@ private async Task GetInitialConfigurationTokenAsync(IAmazonAppConfigDat return (await appConfigClient.StartConfigurationSessionAsync(request).ConfigureAwait(false)).InitialConfigurationToken; } - private static IDictionary ParseConfig(string contentType, Stream configuration) + internal static IDictionary ParseConfig(string contentType, Stream configuration) { - // Content-Type has format "media-type; charset" or "media-type; boundary" (for multipart entities). - if (contentType != null) + try { - contentType = contentType.Split(';')[0]; + return JsonConfigurationParser.Parse(configuration); } - - switch (contentType) + catch (JsonException ex) { - case "application/json": - return JsonConfigurationParser.Parse(configuration); - default: - throw new NotImplementedException($"Not implemented AppConfig type: {contentType}"); + throw new InvalidOperationException( + $"Failed to parse AppConfig content as JSON. Content-Type was '{contentType}'. {ex.Message}", + ex); } } } diff --git a/test/Amazon.Extensions.Configuration.SystemsManager.Integ/AppConfigEndToEndTests.cs b/test/Amazon.Extensions.Configuration.SystemsManager.Integ/AppConfigEndToEndTests.cs index 2610f98..0f680c9 100644 --- a/test/Amazon.Extensions.Configuration.SystemsManager.Integ/AppConfigEndToEndTests.cs +++ b/test/Amazon.Extensions.Configuration.SystemsManager.Integ/AppConfigEndToEndTests.cs @@ -84,6 +84,42 @@ public async Task JsonWithCharsetConfiguration() } } + [Fact] + public async Task AppConfigWithOctetStreamContentType() + { + var configSettings = new Dictionary + { + { "key1", "value1" }, + { "key2", "value2" }, + { "database:connectionString", "server=localhost;database=test" }, + { "api:timeout", "30" } + }; + + // Create AppConfig with application/octet-stream content type (simulates Parameter Store backend) + (string applicationId, string environmentId, string configProfileId) = + await CreateAppConfigResourcesAsync("OctetStreamTest", configSettings, "application/octet-stream"); + + try + { + var builder = new ConfigurationBuilder() + .AddAppConfig(applicationId, environmentId, configProfileId, + new AWSOptions { Region = RegionEndpoint.USWest2 }, + TimeSpan.FromSeconds(5)); + + var configuration = builder.Build(); + + // Verify configuration loads correctly despite application/octet-stream content type + Assert.Equal("value1", configuration["key1"]); + Assert.Equal("value2", configuration["key2"]); + Assert.Equal("server=localhost;database=test", configuration["database:connectionString"]); + Assert.Equal("30", configuration["api:timeout"]); + } + finally + { + await CleanupAppConfigResourcesAsync(applicationId, environmentId, configProfileId); + } + } + private async Task CleanupAppConfigResourcesAsync(string applicationId, string environmentId, string configProfileId) { await _appConfigClient.DeleteEnvironmentAsync(new DeleteEnvironmentRequest {ApplicationId = applicationId, EnvironmentId = environmentId }); diff --git a/test/Amazon.Extensions.Configuration.SystemsManager.Tests/Amazon.Extensions.Configuration.SystemsManager.Tests.csproj b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/Amazon.Extensions.Configuration.SystemsManager.Tests.csproj index c0efff9..c222faf 100644 --- a/test/Amazon.Extensions.Configuration.SystemsManager.Tests/Amazon.Extensions.Configuration.SystemsManager.Tests.csproj +++ b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/Amazon.Extensions.Configuration.SystemsManager.Tests.csproj @@ -4,6 +4,8 @@ net8.0 true false + true + ..\..\public.snk diff --git a/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs new file mode 100644 index 0000000..5dcef29 --- /dev/null +++ b/test/Amazon.Extensions.Configuration.SystemsManager.Tests/AppConfigProcessorTests.cs @@ -0,0 +1,148 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Amazon.Extensions.Configuration.SystemsManager.AppConfig; +using Xunit; + +namespace Amazon.Extensions.Configuration.SystemsManager.Tests +{ + public class AppConfigProcessorTests + { + [Fact] + public void ParseConfig_ApplicationJson_ParsesSuccessfully() + { + var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/json", stream); + + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + } + + [Fact] + public void ParseConfig_ApplicationOctetStream_ParsesJsonSuccessfully() + { + var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/octet-stream", stream); + + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + } + + [Fact] + public void ParseConfig_ApplicationJsonWithCharset_ParsesSuccessfully() + { + var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/json; charset=utf-8", stream); + + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + } + + [Fact] + public void ParseConfig_UnknownContentType_ParsesJsonSuccessfully() + { + var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/unknown", stream); + + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + } + + [Fact] + public void ParseConfig_NullContentType_ParsesJsonSuccessfully() + { + var jsonContent = "{\"key1\":\"value1\",\"key2\":\"value2\"}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig(null, stream); + + Assert.Equal("value1", result["key1"]); + Assert.Equal("value2", result["key2"]); + } + + [Fact] + public void ParseConfig_NestedJson_ParsesSuccessfully() + { + var jsonContent = "{\"section1\":{\"key1\":\"value1\",\"key2\":\"value2\"},\"section2\":{\"key3\":\"value3\"}}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/octet-stream", stream); + + Assert.Equal("value1", result["section1:key1"]); + Assert.Equal("value2", result["section1:key2"]); + Assert.Equal("value3", result["section2:key3"]); + } + + [Fact] + public void ParseConfig_InvalidJson_ThrowsInvalidOperationException() + { + var invalidJsonContent = "{ invalid json content }"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidJsonContent)); + + var exception = Assert.Throws(() => + AppConfigProcessor.ParseConfig("application/json", stream)); + + Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); + Assert.Contains("Content-Type was 'application/json'", exception.Message); + } + + [Fact] + public void ParseConfig_InvalidJsonWithOctetStream_ThrowsInvalidOperationException() + { + var invalidJsonContent = "not json at all"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(invalidJsonContent)); + + var exception = Assert.Throws(() => + AppConfigProcessor.ParseConfig("application/octet-stream", stream)); + + Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); + Assert.Contains("Content-Type was 'application/octet-stream'", exception.Message); + } + + [Fact] + public void ParseConfig_EmptyJson_ReturnsEmptyDictionary() + { + var jsonContent = "{}"; + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonContent)); + + var result = AppConfigProcessor.ParseConfig("application/json", stream); + + Assert.Empty(result); + } + + [Fact] + public void ParseConfig_EmptyStream_ThrowsInvalidOperationException() + { + using var stream = new MemoryStream(); + + var exception = Assert.Throws(() => + AppConfigProcessor.ParseConfig("application/json", stream)); + + Assert.Contains("Failed to parse AppConfig content as JSON", exception.Message); + } + } +}