Skip to content

Commit e6a1c32

Browse files
committed
SIANXSVC-1193: dotnet-e5e does not handle binary requests as expected
Closes SIANXSVC-1193
1 parent 7bd1753 commit e6a1c32

15 files changed

+398
-14
lines changed

src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj

+10
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<Nullable>enable</Nullable>
66
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
77
<IsPackable>false</IsPackable>
8+
<LangVersion>12</LangVersion>
89
</PropertyGroup>
910

1011
<ItemGroup>
@@ -28,4 +29,13 @@
2829
<ProjectReference Include="..\Anexia.E5E\Anexia.E5E.csproj"/>
2930
</ItemGroup>
3031

32+
<ItemGroup>
33+
<None Update="TestData\binary_request_with_multiple_files.json">
34+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
35+
</None>
36+
<None Update="TestData\binary_request_unknown_content_type.json">
37+
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
38+
</None>
39+
</ItemGroup>
40+
3141
</Project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// --------------------------------------------------------------------------------------------
2+
// <copyright file = "BinaryRequestIntegrationTests.cs" company = "ANEXIA® Internetdienstleistungs GmbH">
3+
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
4+
// </copyright>
5+
// --------------------------------------------------------------------------------------------
6+
7+
using System.Threading.Tasks;
8+
9+
using Anexia.E5E.Exceptions;
10+
using Anexia.E5E.Functions;
11+
using Anexia.E5E.Tests.TestHelpers;
12+
13+
using static Anexia.E5E.Tests.TestData.TestData;
14+
15+
using Xunit;
16+
using Xunit.Abstractions;
17+
18+
namespace Anexia.E5E.Tests.Integration;
19+
20+
public sealed class BinaryRequestIntegrationTests(ITestOutputHelper outputHelper) : IntegrationTestBase(outputHelper)
21+
{
22+
[Fact]
23+
public async Task DecodeToBytesThrowsForMultipleFiles()
24+
{
25+
await Host.StartWithTestEntrypointAsync(request =>
26+
{
27+
Assert.Throws<E5EMultipleFilesInFormDataException>(() => request.Event.AsBytes());
28+
return null!;
29+
});
30+
await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles);
31+
}
32+
33+
[Fact]
34+
public async Task MultipleFilesAreProperlyDecoded()
35+
{
36+
await Host.StartWithTestEntrypointAsync(request =>
37+
{
38+
var files = request.Event.AsFiles();
39+
Assert.Collection(files, first =>
40+
{
41+
Assert.NotNull(first);
42+
Assert.Equal(first, new E5EFileData
43+
{
44+
Base64Encoded = "SGVsbG8gd29ybGQh",
45+
FileSizeInBytes = 12,
46+
Filename = "my-file-1.name",
47+
ContentType = "application/my-content-type-1",
48+
Charset = "utf-8",
49+
});
50+
}, second =>
51+
{
52+
Assert.NotNull(second);
53+
Assert.Equal(second, new E5EFileData
54+
{
55+
Base64Encoded = "SGVsbG8gd29ybGQh",
56+
FileSizeInBytes = 12,
57+
Filename = "my-file-2.name",
58+
ContentType = "application/my-content-type-2",
59+
Charset = "utf-8",
60+
});
61+
});
62+
return null!;
63+
});
64+
await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles);
65+
}
66+
67+
[Fact]
68+
public async Task UnknownContentType()
69+
{
70+
await Host.StartWithTestEntrypointAsync(request =>
71+
{
72+
Assert.Equal("SGVsbG8gd29ybGQh"u8.ToArray(), request.Event.AsBytes());
73+
return null!;
74+
});
75+
await Host.WriteOnceAsync(BinaryRequestWithUnknownContentType);
76+
}
77+
78+
[Fact]
79+
public async Task FallbackForByteArrayReturnsValidResponse()
80+
{
81+
// act
82+
#pragma warning disable CS0618 // Type or member is obsolete
83+
await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From("SGVsbG8gd29ybGQh"u8.ToArray()));
84+
#pragma warning restore CS0618 // Type or member is obsolete
85+
var response = await Host.WriteOnceAsync(x => x.WithData("test"));
86+
87+
// assert
88+
const string expected =
89+
"""
90+
{"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"}
91+
""";
92+
Assert.Contains(expected, response.Stdout);
93+
}
94+
95+
[Fact]
96+
public async Task FileDataReturnsValidResponse()
97+
{
98+
// act
99+
await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From(new E5EFileData
100+
{
101+
Base64Encoded = "SGVsbG8gd29ybGQh",
102+
Type = "binary",
103+
FileSizeInBytes = 16,
104+
Filename = "hello-world.txt",
105+
ContentType = "text/plain",
106+
Charset = "utf-8",
107+
}));
108+
var response = await Host.WriteOnceAsync(x => x.WithData("test"));
109+
110+
// assert
111+
const string expected =
112+
"""
113+
{"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":16,"name":"hello-world.txt","content_type":"text/plain","charset":"utf-8"},"type":"binary"}
114+
""";
115+
Assert.Contains(expected, response.Stdout);
116+
}
117+
}

src/Anexia.E5E.Tests/Serialization/SerializationTests.cs

+12-4
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,11 @@ public void ResponseSerializationWorksBidirectional(string _, E5EResponse input)
9999
public void ResponseSerializationRecognisesCorrectType()
100100
{
101101
Assert.Equal(E5EResponseType.Text, E5EResponse.From("test").Type);
102-
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test")).Type);
103-
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test").AsEnumerable()).Type);
102+
#pragma warning disable CS0618 // Type or member is obsolete
103+
Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray()).Type);
104+
Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray().AsEnumerable()).Type);
105+
#pragma warning restore CS0618 // Type or member is obsolete
106+
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(new E5EFileData()).Type);
104107
Assert.Equal(E5EResponseType.StructuredObject, E5EResponse.From(new E5ERuntimeMetadata()).Type);
105108
}
106109

@@ -180,7 +183,10 @@ private class RequestSerializationTestsData : IEnumerable<object[]>
180183
private readonly Dictionary<string, E5EEvent> _tests = new()
181184
{
182185
{ "simple text request", new TestRequestBuilder().WithData("test").BuildEvent() },
183-
{ "simple binary request", new TestRequestBuilder().WithData(Encoding.UTF8.GetBytes("test")).BuildEvent() },
186+
{
187+
"simple binary request",
188+
new TestRequestBuilder().WithData(new E5EFileData { Base64Encoded = "aGVsbG8=" }).BuildEvent()
189+
},
184190
{
185191
"simple object request",
186192
new TestRequestBuilder().WithData(new Dictionary<string, string> { { "test", "value" } }).BuildEvent()
@@ -211,7 +217,9 @@ private class ResponseSerializationTestsData : IEnumerable<object[]>
211217
private readonly Dictionary<string, E5EResponse> _tests = new()
212218
{
213219
{ "simple text response", E5EResponse.From("test") },
214-
{ "simple binary response", E5EResponse.From(Encoding.UTF8.GetBytes("test")) },
220+
#pragma warning disable CS0618 // Type or member is obsolete
221+
{ "simple binary response", E5EResponse.From("aGVsbG8="u8.ToArray()) },
222+
#pragma warning restore CS0618 // Type or member is obsolete
215223
{ "simple object response", E5EResponse.From(new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }) },
216224
{
217225
"text response with headers and status code", E5EResponse.From("test", HttpStatusCode.Moved,
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"type":"binary","data":"dGVzdA=="}
1+
{"type":"binary","data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":null,"content_type":null,"charset":null}}
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"data":"dGVzdA==","type":"binary"}
1+
{"data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"}
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// --------------------------------------------------------------------------------------------
2+
// <copyright file = "TestData.cs" company = "ANEXIA® Internetdienstleistungs GmbH">
3+
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
4+
// </copyright>
5+
// --------------------------------------------------------------------------------------------
6+
7+
using System.IO;
8+
9+
namespace Anexia.E5E.Tests.TestData;
10+
11+
internal static class TestData
12+
{
13+
private static string ReadTestDataFile(string path) => File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "TestData", path));
14+
15+
internal static string BinaryRequestWithMultipleFiles => ReadTestDataFile("binary_request_with_multiple_files.json");
16+
internal static string BinaryRequestWithUnknownContentType => ReadTestDataFile("binary_request_unknown_content_type.json");
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"context": {
3+
"type": "integration-test",
4+
"async": true,
5+
"date": "2024-01-01T00:00:00Z"
6+
},
7+
"event": {
8+
"type": "binary",
9+
"data": {
10+
"binary": "SGVsbG8gd29ybGQh",
11+
"type": "binary",
12+
"name": "my-file-1.name",
13+
"size": 12,
14+
"content_type": "application/my-content-type-1",
15+
"charset": "utf-8"
16+
}
17+
}
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"context": {
3+
"type": "integration-test",
4+
"async": true,
5+
"date": "2024-01-01T00:00:00Z"
6+
},
7+
"event": {
8+
"type": "binary",
9+
"data": [
10+
{
11+
"binary": "SGVsbG8gd29ybGQh",
12+
"type": "binary",
13+
"name": "my-file-1.name",
14+
"size": 12,
15+
"content_type": "application/my-content-type-1",
16+
"charset": "utf-8"
17+
},
18+
{
19+
"binary": "SGVsbG8gd29ybGQh",
20+
"type": "binary",
21+
"name": "my-file-2.name",
22+
"size": 12,
23+
"content_type": "application/my-content-type-2",
24+
"charset": "utf-8"
25+
}
26+
]
27+
}
28+
}

src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Text.Json;
34

@@ -17,7 +18,9 @@ public TestRequestBuilder WithData<T>(T data)
1718
_requestType = data switch
1819
{
1920
string => E5ERequestDataType.Text,
20-
IEnumerable<byte> => E5ERequestDataType.Binary,
21+
IEnumerable<byte> => throw new InvalidOperationException(
22+
$"E5E does not compose binary requests just from the bytes. Please convert this call to use {nameof(E5EFileData)} instead."),
23+
E5EFileData => E5ERequestDataType.Binary,
2124
_ => E5ERequestDataType.StructuredObject,
2225
};
2326
_data = JsonSerializer.SerializeToElement(data);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// --------------------------------------------------------------------------------------------
2+
// <copyright file = "E5EMultipleFilesInFormDataException.cs" company = "ANEXIA® Internetdienstleistungs GmbH">
3+
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
4+
// </copyright>
5+
// --------------------------------------------------------------------------------------------
6+
7+
using Anexia.E5E.Functions;
8+
9+
namespace Anexia.E5E.Exceptions;
10+
11+
/// <summary>
12+
/// Exception that is thrown if the <see cref="E5EEvent.AsBytes"/> method is called, but there is more than one
13+
/// file attached to the request.
14+
/// </summary>
15+
public sealed class E5EMultipleFilesInFormDataException : E5EException
16+
{
17+
internal E5EMultipleFilesInFormDataException() : base(
18+
$"There were multiple files attached to this request, so there's no unique binary data. Please use the {nameof(E5EEvent)}.{nameof(E5EEvent.AsFiles)} method instead.")
19+
{
20+
}
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// --------------------------------------------------------------------------------------------
2+
// <copyright file = "E5EObsoleteCodeInUseException.cs" company = "ANEXIA® Internetdienstleistungs GmbH">
3+
// Copyright (c) ANEXIA® Internetdienstleistungs GmbH. All rights reserved.
4+
// </copyright>
5+
// --------------------------------------------------------------------------------------------
6+
7+
namespace Anexia.E5E.Exceptions;
8+
9+
/// <summary>
10+
/// Exception that is thrown if obsolete code is in usage.
11+
/// Usually this shouldn't not happen, as the APIs are backwards compatible. If it gets thrown nonetheless, this indicates a bug in the library.
12+
/// </summary>
13+
public sealed class E5EObsoleteCodeInUseException : E5EException
14+
{
15+
internal E5EObsoleteCodeInUseException(string message) : base(message)
16+
{
17+
}
18+
}

src/Anexia.E5E/Functions/E5EEvent.cs

+35-6
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ namespace Anexia.E5E.Functions;
1414
/// <param name="Data">The data, not processed.</param>
1515
/// <param name="RequestHeaders">The request headers, if any.</param>
1616
/// <param name="Params">The request parameters, if any.</param>
17-
public record E5EEvent(E5ERequestDataType Type,
17+
public record E5EEvent(
18+
E5ERequestDataType Type,
1819
JsonElement? Data = null,
1920
E5EHttpHeaders? RequestHeaders = null,
2021
E5ERequestParameters? Params = null)
@@ -63,15 +64,43 @@ public record E5EEvent(E5ERequestDataType Type,
6364
}
6465

6566
/// <summary>
66-
/// Returns the value as byte enumerable.
67+
/// Returns the bytes of the attached file.
6768
/// </summary>
6869
/// <exception cref="E5EInvalidConversionException">
69-
/// Thrown if <see cref="Type" /> is not
70-
/// <see cref="E5ERequestDataType.Binary" />.
70+
/// Thrown if <see cref="Type" /> is not <see cref="E5ERequestDataType.Binary" />.
7171
/// </exception>
72-
public byte[]? AsBytes()
72+
/// <exception cref="E5EMultipleFilesInFormDataException">Thrown if there are multiple files attached to this request.</exception>
73+
public IEnumerable<byte> AsBytes()
74+
{
75+
E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type);
76+
if (Data.GetValueOrDefault().ValueKind == JsonValueKind.Array)
77+
throw new E5EMultipleFilesInFormDataException();
78+
79+
#if NET8_0_OR_GREATER
80+
return As(E5ESerializationContext.Default.E5EFileData)?.GetBytes() ?? Enumerable.Empty<byte>();
81+
#else
82+
return As<E5EFileData>()?.GetBytes() ?? Enumerable.Empty<byte>();
83+
#endif
84+
}
85+
86+
/// <summary>
87+
/// If this request is a multipart/form-data request, all files attached to this request are deserialized.
88+
/// </summary>
89+
/// <returns></returns>
90+
/// <exception cref="NotImplementedException"></exception>
91+
public IEnumerable<E5EFileData> AsFiles()
7392
{
7493
E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type);
75-
return As(E5ESerializationContext.Default.ByteArray);
94+
return Data.GetValueOrDefault().ValueKind switch
95+
{
96+
#if NET8_0_OR_GREATER
97+
JsonValueKind.Object => new[] { As(E5ESerializationContext.Default.E5EFileData)! },
98+
JsonValueKind.Array => As(E5ESerializationContext.Default.IEnumerableE5EFileData),
99+
#else
100+
JsonValueKind.Object => new[] { As<E5EFileData>()! },
101+
JsonValueKind.Array => As<IEnumerable<E5EFileData>>(),
102+
#endif
103+
_ => null,
104+
} ?? Enumerable.Empty<E5EFileData>();
76105
}
77106
}

0 commit comments

Comments
 (0)