diff --git a/Elsa.sln b/Elsa.sln index a6d64300a..f5a389155 100644 --- a/Elsa.sln +++ b/Elsa.sln @@ -1,4 +1,4 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.7.34003.232 MinimumVisualStudioVersion = 10.0.40219.1 @@ -262,6 +262,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Expressions.Python", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.SasTokens", "src\modules\Elsa.SasTokens\Elsa.SasTokens.csproj", "{A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elsa.Http.UnitTests", "test\unit\Elsa.Http.UnitTests\Elsa.Http.UnitTests.csproj", "{66ACA2B3-DB48-4F68-B26D-62555F3CE69B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -450,6 +452,10 @@ Global {A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Debug|Any CPU.Build.0 = Debug|Any CPU {A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Release|Any CPU.ActiveCfg = Release|Any CPU {A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24}.Release|Any CPU.Build.0 = Release|Any CPU + {66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {66ACA2B3-DB48-4F68-B26D-62555F3CE69B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -527,6 +533,7 @@ Global {6AF53651-99F0-1DE0-D37B-4FF6B0348DBB} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {9D8FB664-88B4-10BE-58A2-D9A1644AD2E4} = {6EF07978-A6D2-40EB-891D-7D70C5F37E76} {A7DE02E3-405B-B6BA-7E47-9E27D4BEFB24} = {5948B0A5-7873-4DBB-BA03-EB283D6EA91B} + {66ACA2B3-DB48-4F68-B26D-62555F3CE69B} = {18453B51-25EB-4317-A4B3-B10518252E92} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D4B5CEAA-7D70-4FCB-A68E-B03FBE5E0E5E} diff --git a/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs index 2523b553a..d3c45da4e 100644 --- a/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs +++ b/src/modules/Elsa.Http/ContentWriters/JsonContentFactory.cs @@ -22,6 +22,6 @@ public class JsonContentFactory : IHttpContentFactory if (string.IsNullOrWhiteSpace(contentType)) contentType = MediaTypeNames.Application.Json; - return new StringContent(text, Encoding.UTF8, contentType); + return new RawStringContent(text, Encoding.UTF8, contentType); } } \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/RawStringContent.cs b/src/modules/Elsa.Http/ContentWriters/RawStringContent.cs new file mode 100644 index 000000000..358131ddf --- /dev/null +++ b/src/modules/Elsa.Http/ContentWriters/RawStringContent.cs @@ -0,0 +1,48 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; + +namespace Elsa.Http.ContentWriters; + +/// +/// A implementation that allows setting the content type without automatically appending charset information. +/// +public class RawStringContent : HttpContent +{ + private readonly string _content; + private readonly Encoding _encoding; + + /// + /// Creates a new instance of the class. + /// + /// The content to send. + /// The encoding to use when sending the content. + /// The media type to use for the content. + public RawStringContent(string content, Encoding encoding, string mediaType) + { + _content = content; + _encoding = encoding; + + // Set the media type exactly as provided without appending charset information + Headers.ContentType = new MediaTypeHeaderValue(mediaType); + } + + /// + protected override Task SerializeToStreamAsync(Stream stream, TransportContext? context) => + SerializeToStreamAsync(stream, context, CancellationToken.None); + + /// + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken) + { + using var writer = new StreamWriter(stream, _encoding, leaveOpen: true); + await writer.WriteAsync(_content.AsMemory(), cancellationToken); + await writer.FlushAsync(); + } + + /// + protected override bool TryComputeLength(out long length) + { + length = _encoding.GetByteCount(_content); + return true; + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs index d1c14167e..7b6532877 100644 --- a/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs +++ b/src/modules/Elsa.Http/ContentWriters/TextContentFactory.cs @@ -24,6 +24,6 @@ public class TextContentFactory : IHttpContentFactory if (string.IsNullOrWhiteSpace(contentType)) contentType = MediaTypeNames.Text.Plain; - return new StringContent(text!, Encoding.UTF8, contentType); + return new RawStringContent(text!, Encoding.UTF8, contentType); } } \ No newline at end of file diff --git a/src/modules/Elsa.Http/ContentWriters/XmlContentFactory.cs b/src/modules/Elsa.Http/ContentWriters/XmlContentFactory.cs index d45ebd61a..1d65abc6d 100644 --- a/src/modules/Elsa.Http/ContentWriters/XmlContentFactory.cs +++ b/src/modules/Elsa.Http/ContentWriters/XmlContentFactory.cs @@ -21,7 +21,7 @@ public class XmlContentFactory : IHttpContentFactory public HttpContent CreateHttpContent(object content, string contentType) { var text = content as string ?? Serialize(content); - return new StringContent(text, Encoding.UTF8, contentType); + return new RawStringContent(text, Encoding.UTF8, contentType); } private string Serialize(object value) diff --git a/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs b/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs new file mode 100644 index 000000000..e8d923bf1 --- /dev/null +++ b/test/unit/Elsa.Http.UnitTests/ContentWriters/ContentFactoryTests.cs @@ -0,0 +1,67 @@ +using Elsa.Http.ContentWriters; +using Xunit; + +namespace Elsa.Http.UnitTests.ContentWriters; + +/// +/// Tests for the implementations. +/// +public class ContentFactoryTests +{ + /// + /// Tests that doesn't append charset to content type. + /// + [Fact] + public void JsonContentFactory_ShouldNotAppendCharset() + { + // Arrange + const string contentType = "application/json"; + const string content = "{\"test\": \"value\"}"; + var factory = new JsonContentFactory(); + + // Act + var httpContent = factory.CreateHttpContent(content, contentType); + + // Assert + Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType); + Assert.Null(httpContent.Headers.ContentType?.CharSet); + } + + /// + /// Tests that doesn't append charset to content type. + /// + [Fact] + public void XmlContentFactory_ShouldNotAppendCharset() + { + // Arrange + const string contentType = "text/xml"; + const string content = "test"; + var factory = new XmlContentFactory(); + + // Act + var httpContent = factory.CreateHttpContent(content, contentType); + + // Assert + Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType); + Assert.Null(httpContent.Headers.ContentType?.CharSet); + } + + /// + /// Tests that doesn't append charset to content type. + /// + [Fact] + public void TextContentFactory_ShouldNotAppendCharset() + { + // Arrange + const string contentType = "text/html"; + const string content = "test"; + var factory = new TextContentFactory(); + + // Act + var httpContent = factory.CreateHttpContent(content, contentType); + + // Assert + Assert.Equal(contentType, httpContent.Headers.ContentType?.MediaType); + Assert.Null(httpContent.Headers.ContentType?.CharSet); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Http.UnitTests/ContentWriters/RawStringContentTests.cs b/test/unit/Elsa.Http.UnitTests/ContentWriters/RawStringContentTests.cs new file mode 100644 index 000000000..308f22c37 --- /dev/null +++ b/test/unit/Elsa.Http.UnitTests/ContentWriters/RawStringContentTests.cs @@ -0,0 +1,55 @@ +using System.Net.Http.Headers; +using System.Text; +using Elsa.Http.ContentWriters; +using Xunit; + +namespace Elsa.Http.UnitTests.ContentWriters; + +/// +/// Tests for the class. +/// +public class RawStringContentTests +{ + /// + /// Tests that the content type is set exactly as provided without appending charset information. + /// + [Fact] + public void ContentType_ShouldNotAppendCharset() + { + // Arrange + const string contentType = "text/xml"; + const string content = "test"; + + // Act + var rawContent = new RawStringContent(content, Encoding.UTF8, contentType); + + // Assert + Assert.Equal(contentType, rawContent.Headers.ContentType?.MediaType); + Assert.Null(rawContent.Headers.ContentType?.CharSet); + } + + /// + /// Tests that the content type with parameters is preserved exactly as provided. + /// + [Fact] + public void ContentType_WithParameters_ShouldPreserveParameters() + { + // Arrange + const string contentType = "application/json; custom-param=value"; + var expectedMediaType = new MediaTypeHeaderValue(contentType); + const string content = "{\"test\": \"value\"}"; + + // Act + var rawContent = new RawStringContent(content, Encoding.UTF8, contentType); + + // Assert + Assert.Equal(expectedMediaType.MediaType, rawContent.Headers.ContentType?.MediaType); + Assert.Equal(expectedMediaType.Parameters.Count(), rawContent.Headers.ContentType?.Parameters.Count()); + + var expectedParam = expectedMediaType.Parameters.First(); + var actualParam = rawContent.Headers.ContentType?.Parameters.First(); + + Assert.Equal(expectedParam.Name, actualParam?.Name); + Assert.Equal(expectedParam.Value, actualParam?.Value); + } +} \ No newline at end of file diff --git a/test/unit/Elsa.Http.UnitTests/Elsa.Http.UnitTests.csproj b/test/unit/Elsa.Http.UnitTests/Elsa.Http.UnitTests.csproj new file mode 100644 index 000000000..457f65e7f --- /dev/null +++ b/test/unit/Elsa.Http.UnitTests/Elsa.Http.UnitTests.csproj @@ -0,0 +1,30 @@ + + + + net8.0 + enable + enable + latest + false + true + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file