Skip to content

Commit

Permalink
SIANXSVC-1193: dotnet-e5e does not handle binary requests as expected
Browse files Browse the repository at this point in the history
Closes SIANXSVC-1193
  • Loading branch information
nachtjasmin committed Jan 26, 2024
1 parent 1d955c9 commit 1ff4c15
Show file tree
Hide file tree
Showing 15 changed files with 372 additions and 25 deletions.
10 changes: 10 additions & 0 deletions src/Anexia.E5E.Tests/Anexia.E5E.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<LangVersion>12</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand All @@ -27,4 +28,13 @@
<ProjectReference Include="..\Anexia.E5E\Anexia.E5E.csproj"/>
</ItemGroup>

<ItemGroup>
<None Update="TestData\binary_request_with_multiple_files.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="TestData\binary_request_unknown_content_type.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
107 changes: 107 additions & 0 deletions src/Anexia.E5E.Tests/Integration/BinaryRequestIntegrationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using System.Threading.Tasks;

using Anexia.E5E.Exceptions;
using Anexia.E5E.Functions;
using Anexia.E5E.Tests.TestHelpers;

using static Anexia.E5E.Tests.TestData.TestData;

using Xunit;
using Xunit.Abstractions;

namespace Anexia.E5E.Tests.Integration;

public sealed class BinaryRequestIntegrationTests(ITestOutputHelper outputHelper) : IntegrationTestBase(outputHelper)
{
[Fact]
public async Task DecodeToBytesThrowsForMultipleFiles()
{
await Host.StartWithTestEntrypointAsync(request =>
{
Assert.Throws<E5EMultipleFilesInFormDataException>(() => request.Event.AsBytes());
return null!;
});
await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles);
}

[Fact]
public async Task MultipleFilesAreProperlyDecoded()
{
await Host.StartWithTestEntrypointAsync(request =>
{
var content = "Hello world!"u8.ToArray();
var files = request.Event.AsFiles();
Assert.Collection(files, first =>
{
Assert.NotNull(first);
Assert.Equivalent(first, new E5EFileData(content)
{
FileSizeInBytes = 12,
Filename = "my-file-1.name",
ContentType = "application/my-content-type-1",
Charset = "utf-8",
});
}, second =>
{
Assert.NotNull(second);
Assert.Equivalent(second, new E5EFileData(content)
{
FileSizeInBytes = 12,
Filename = "my-file-2.name",
ContentType = "application/my-content-type-2",
Charset = "utf-8",
});
});
return null!;
});
await Host.WriteOnceAsync(BinaryRequestWithMultipleFiles);
}

[Fact]
public async Task UnknownContentType()
{
await Host.StartWithTestEntrypointAsync(request =>
{
Assert.Equal("Hello world!"u8.ToArray(), request.Event.AsBytes());
return null!;
});
await Host.WriteOnceAsync(BinaryRequestWithUnknownContentType);
}

[Fact]
public async Task FallbackForByteArrayReturnsValidResponse()
{
// act
await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From("Hello world!"u8.ToArray()));
var response = await Host.WriteOnceAsync(x => x.WithData("test"));

// assert
const string expected =
"""
{"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"}
""";
Assert.Contains(expected, response.Stdout);
}

[Fact]
public async Task FileDataReturnsValidResponse()
{
// act
await Host.StartWithTestEntrypointAsync(_ => E5EResponse.From(new E5EFileData("Hello world!"u8.ToArray())
{
Type = "binary",
FileSizeInBytes = 16,
Filename = "hello-world.txt",
ContentType = "text/plain",
Charset = "utf-8",
}));
var response = await Host.WriteOnceAsync(x => x.WithData("test"));

// assert
const string expected =
"""
{"data":{"binary":"SGVsbG8gd29ybGQh","type":"binary","size":16,"name":"hello-world.txt","content_type":"text/plain","charset":"utf-8"},"type":"binary"}
""";
Assert.Contains(expected, response.Stdout);
}
}
11 changes: 8 additions & 3 deletions src/Anexia.E5E.Tests/Integration/DocumentationTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

using Anexia.E5E.Functions;
Expand Down Expand Up @@ -42,7 +43,11 @@ await Host.StartWithTestEntrypointAsync(request =>
var data = request.Event.As<SumData>()!;
return E5EResponse.From(data.A + data.B);
});
var response = await Host.WriteRequestOnceAsync(x => x.WithData(new SumData { A = 3, B = 2 }));
var response = await Host.WriteRequestOnceAsync(x => x.WithData(new SumData
{
A = 3,
B = 2,
}));
Assert.Equal(5, response.As<int>());
}

Expand All @@ -55,8 +60,8 @@ await Host.StartWithTestEntrypointAsync(request =>
var resp = Encoding.UTF8.GetBytes($"Hello {name}");
return E5EResponse.From(resp);
});
var response = await Host.WriteRequestOnceAsync(x => x.WithData(Encoding.UTF8.GetBytes("Luna")));
Assert.Equal("\"SGVsbG8gTHVuYQ==\"", response.Data.GetRawText());
var response = await Host.WriteRequestOnceAsync(x => x.WithData(new E5EFileData("Luna"u8.ToArray())));
Assert.Equal("Hello Luna"u8.ToArray(), response.Data.Deserialize<E5EFileData>()!.Bytes);
Assert.Equal(E5EResponseType.Binary, response.Type);
}

Expand Down
25 changes: 13 additions & 12 deletions src/Anexia.E5E.Tests/Serialization/SerializationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

Expand Down Expand Up @@ -98,8 +97,9 @@ public void ResponseSerializationWorksBidirectional(string _, E5EResponse input)
public void ResponseSerializationRecognisesCorrectType()
{
Assert.Equal(E5EResponseType.Text, E5EResponse.From("test").Type);
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test")).Type);
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(Encoding.UTF8.GetBytes("test").AsEnumerable()).Type);
Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray()).Type);
Assert.Equal(E5EResponseType.Binary, E5EResponse.From("test"u8.ToArray().AsEnumerable()).Type);
Assert.Equal(E5EResponseType.Binary, E5EResponse.From(new E5EFileData("something"u8.ToArray())).Type);
Assert.Equal(E5EResponseType.StructuredObject, E5EResponse.From(new E5ERuntimeMetadata()).Type);
}

Expand Down Expand Up @@ -159,6 +159,7 @@ private class SerializationTestsData : IEnumerable<object[]>
new E5EContext("generic", DateTimeOffset.FromUnixTimeSeconds(0), true),
new E5ERequestParameters(),
new E5ERuntimeMetadata(),
new E5EFileData("data"u8.ToArray()),
};

private IEnumerable<object[]> Data => _objects.Select(obj => new[] { obj });
Expand All @@ -179,11 +180,8 @@ private class RequestSerializationTestsData : IEnumerable<object[]>
private readonly Dictionary<string, E5EEvent> _tests = new()
{
{ "simple text request", new TestRequestBuilder().WithData("test").BuildEvent() },
{ "simple binary request", new TestRequestBuilder().WithData(Encoding.UTF8.GetBytes("test")).BuildEvent() },
{
"simple object request",
new TestRequestBuilder().WithData(new Dictionary<string, string> { { "test", "value" } }).BuildEvent()
},
{ "simple binary request", new TestRequestBuilder().WithData(new E5EFileData("hello"u8.ToArray())).BuildEvent() },
{ "simple object request", new TestRequestBuilder().WithData(new Dictionary<string, string> { { "test", "value" } }).BuildEvent() },
{
"request with headers and parameters", new TestRequestBuilder().WithData("test")
.AddParam("param", "value")
Expand All @@ -210,12 +208,15 @@ private class ResponseSerializationTestsData : IEnumerable<object[]>
private readonly Dictionary<string, E5EResponse> _tests = new()
{
{ "simple text response", E5EResponse.From("test") },
{ "simple binary response", E5EResponse.From(Encoding.UTF8.GetBytes("test")) },
{ "simple object response", E5EResponse.From(new Dictionary<string, int> { { "a", 1 }, { "b", 2 } }) },
{ "simple binary response", E5EResponse.From("hello"u8.ToArray()) },
{
"text response with headers and status code", E5EResponse.From("test", HttpStatusCode.Moved,
new E5EHttpHeaders { { "Location", "https://example.com" } })
"simple object response", E5EResponse.From(new Dictionary<string, int>
{
{ "a", 1 },
{ "b", 2 },
})
},
{ "text response with headers and status code", E5EResponse.From("test", HttpStatusCode.Moved, new E5EHttpHeaders { { "Location", "https://example.com" } }) },
};

private IEnumerable<object[]> Data => _tests.Select(obj => new object[] { obj.Key, obj.Value });
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"type":"binary","data":"dGVzdA=="}
{"type":"binary","data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":null,"content_type":null,"charset":"utf-8"}}
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"data":"dGVzdA==","type":"binary"}
{"data":{"binary":"aGVsbG8=","type":"binary","size":0,"name":"dotnet-e5e-binary-response.blob","content_type":"application/octet-stream","charset":"utf-8"},"type":"binary"}
11 changes: 11 additions & 0 deletions src/Anexia.E5E.Tests/TestData/TestData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.IO;

namespace Anexia.E5E.Tests.TestData;

internal static class TestData
{
private static string ReadTestDataFile(string path) => File.ReadAllText(Path.Combine(Directory.GetCurrentDirectory(), "TestData", path));

internal static string BinaryRequestWithMultipleFiles => ReadTestDataFile("binary_request_with_multiple_files.json");
internal static string BinaryRequestWithUnknownContentType => ReadTestDataFile("binary_request_unknown_content_type.json");
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"context": {
"type": "integration-test",
"async": true,
"date": "2024-01-01T00:00:00Z"
},
"event": {
"type": "binary",
"data": {
"binary": "SGVsbG8gd29ybGQh",
"type": "binary",
"name": "my-file-1.name",
"size": 12,
"content_type": "application/my-content-type-1",
"charset": "utf-8"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"context": {
"type": "integration-test",
"async": true,
"date": "2024-01-01T00:00:00Z"
},
"event": {
"type": "binary",
"data": [
{
"binary": "SGVsbG8gd29ybGQh",
"type": "binary",
"name": "my-file-1.name",
"size": 12,
"content_type": "application/my-content-type-1",
"charset": "utf-8"
},
{
"binary": "SGVsbG8gd29ybGQh",
"type": "binary",
"name": "my-file-2.name",
"size": 12,
"content_type": "application/my-content-type-2",
"charset": "utf-8"
}
]
}
}
5 changes: 4 additions & 1 deletion src/Anexia.E5E.Tests/TestHelpers/TestRequestBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text.Json;

Expand All @@ -17,7 +18,9 @@ public TestRequestBuilder WithData<T>(T data)
_requestType = data switch
{
string => E5ERequestDataType.Text,
IEnumerable<byte> => E5ERequestDataType.Binary,
IEnumerable<byte> => throw new InvalidOperationException(
$"E5E does not compose binary requests just from the bytes. Please convert this call to use {nameof(E5EFileData)} instead."),
E5EFileData => E5ERequestDataType.Binary,
_ => E5ERequestDataType.StructuredObject,
};
_data = JsonSerializer.SerializeToElement(data);
Expand Down
15 changes: 15 additions & 0 deletions src/Anexia.E5E/Exceptions/E5EMultipleFilesInFormDataException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
using Anexia.E5E.Functions;

namespace Anexia.E5E.Exceptions;

/// <summary>
/// Exception that is thrown if the <see cref="E5EEvent.AsBytes"/> method is called, but there is more than one
/// file attached to the request.
/// </summary>
public sealed class E5EMultipleFilesInFormDataException : E5EException
{
internal E5EMultipleFilesInFormDataException() : base(
$"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.")
{
}
}
41 changes: 36 additions & 5 deletions src/Anexia.E5E/Functions/E5EEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ namespace Anexia.E5E.Functions;
/// <param name="Data">The data, not processed.</param>
/// <param name="RequestHeaders">The request headers, if any.</param>
/// <param name="Params">The request parameters, if any.</param>
public record E5EEvent(E5ERequestDataType Type,
public record E5EEvent(
E5ERequestDataType Type,
JsonElement? Data = null,
E5EHttpHeaders? RequestHeaders = null,
E5ERequestParameters? Params = null)
Expand Down Expand Up @@ -63,15 +64,45 @@ public record E5EEvent(E5ERequestDataType Type,
}

/// <summary>
/// Returns the value as byte enumerable.
/// Returns the bytes of the attached file.
/// </summary>
/// <exception cref="E5EInvalidConversionException">
/// Thrown if <see cref="Type" /> is not
/// <see cref="E5ERequestDataType.Binary" />.
/// Thrown if <see cref="Type" /> is not <see cref="E5ERequestDataType.Binary" />.
/// </exception>
/// <exception cref="E5EMultipleFilesInFormDataException">Thrown if there are multiple files attached to this request.</exception>
public byte[]? AsBytes()
{
E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type);
return As(E5ESerializationContext.Default.ByteArray);
if (Data.GetValueOrDefault().ValueKind == JsonValueKind.Array)
throw new E5EMultipleFilesInFormDataException();

#if NET8_0_OR_GREATER
var fileData = As(E5ESerializationContext.Default.E5EFileData);
#else
var fileData = As<E5EFileData>();
#endif

return fileData?.Bytes;
}

/// <summary>
/// If this request is a multipart/form-data request, all files attached to this request are deserialized.
/// </summary>
/// <returns></returns>
/// <exception cref="NotImplementedException"></exception>
public IEnumerable<E5EFileData> AsFiles()
{
E5EInvalidConversionException.ThrowIfNotMatch(E5ERequestDataType.Binary, Type);
return Data.GetValueOrDefault().ValueKind switch
{
#if NET8_0_OR_GREATER
JsonValueKind.Object => new[] { As(E5ESerializationContext.Default.E5EFileData)! },
JsonValueKind.Array => As(E5ESerializationContext.Default.IEnumerableE5EFileData),
#else
JsonValueKind.Object => new[] { As<E5EFileData>()! },
JsonValueKind.Array => As<IEnumerable<E5EFileData>>(),
#endif
_ => null,
} ?? Enumerable.Empty<E5EFileData>();
}
}
Loading

0 comments on commit 1ff4c15

Please sign in to comment.