diff --git a/Directory.Packages.props b/Directory.Packages.props index 1397c7567..6e0da532b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -35,6 +35,7 @@ + diff --git a/src/MagicOnion.Server.JsonTranscoding/MagicOnionJsonTranscodingGrpcMethodBinder.cs b/src/MagicOnion.Server.JsonTranscoding/MagicOnionJsonTranscodingGrpcMethodBinder.cs index 7f54a5df2..910942702 100644 --- a/src/MagicOnion.Server.JsonTranscoding/MagicOnionJsonTranscodingGrpcMethodBinder.cs +++ b/src/MagicOnion.Server.JsonTranscoding/MagicOnionJsonTranscodingGrpcMethodBinder.cs @@ -44,7 +44,11 @@ public void BindUnary(IMagicOnio var memStream = new MemoryStream(); await context.Request.BodyReader.CopyToAsync(memStream); - var request = grpcMethod.RequestMarshaller.ContextualDeserializer(new DeserializationContextImpl(memStream.ToArray())); + // If the request type is `Nil` (parameter-less method), we always ignore the request body. + TRawRequest request = (typeof(TRequest) == typeof(Nil)) + ? (TRawRequest)(object)Box.Create(Nil.Default) + : grpcMethod.RequestMarshaller.ContextualDeserializer(new DeserializationContextImpl(memStream.ToArray())); + var response = await unaryMethodHandler(handle.Instance, request, serverCallContext); context.Response.ContentType = "application/json"; diff --git a/tests/MagicOnion.Server.InternalTesting/MagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.InternalTesting/MagicOnionApplicationFactory.cs index 22c7ebddb..14f6600c7 100644 --- a/tests/MagicOnion.Server.InternalTesting/MagicOnionApplicationFactory.cs +++ b/tests/MagicOnion.Server.InternalTesting/MagicOnionApplicationFactory.cs @@ -35,7 +35,7 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) builder.ConfigureServices(services => { services.AddKeyedSingleton>(ItemsKey); - services.AddMagicOnion(); + OnConfigureMagicOnionBuilder(services.AddMagicOnion()); }); builder.Configure(app => { @@ -47,6 +47,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder) }); } + protected virtual void OnConfigureMagicOnionBuilder(IMagicOnionServerBuilder builder){} + protected abstract IEnumerable GetServiceImplementationTypes(); public WebApplicationFactory WithMagicOnionOptions(Action configure) diff --git a/tests/MagicOnion.Server.JsonTranscoding.Tests/JsonTranscodingEnabledMagicOnionApplicationFactory.cs b/tests/MagicOnion.Server.JsonTranscoding.Tests/JsonTranscodingEnabledMagicOnionApplicationFactory.cs new file mode 100644 index 000000000..a742d6f67 --- /dev/null +++ b/tests/MagicOnion.Server.JsonTranscoding.Tests/JsonTranscodingEnabledMagicOnionApplicationFactory.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace MagicOnion.Server.JsonTranscoding.Tests; + +public class JsonTranscodingEnabledMagicOnionApplicationFactory : MagicOnionApplicationFactory +{ + protected override void OnConfigureMagicOnionBuilder(IMagicOnionServerBuilder builder) + { + builder.AddJsonTranscoding(); + } +} diff --git a/tests/MagicOnion.Server.JsonTranscoding.Tests/MagicOnion.Server.JsonTranscoding.Tests.csproj b/tests/MagicOnion.Server.JsonTranscoding.Tests/MagicOnion.Server.JsonTranscoding.Tests.csproj index a2b37b466..d0d2b65e9 100644 --- a/tests/MagicOnion.Server.JsonTranscoding.Tests/MagicOnion.Server.JsonTranscoding.Tests.csproj +++ b/tests/MagicOnion.Server.JsonTranscoding.Tests/MagicOnion.Server.JsonTranscoding.Tests.csproj @@ -12,10 +12,16 @@ + + + + + + diff --git a/tests/MagicOnion.Server.JsonTranscoding.Tests/UnaryFunctionalTests.cs b/tests/MagicOnion.Server.JsonTranscoding.Tests/UnaryFunctionalTests.cs new file mode 100644 index 000000000..c60214a96 --- /dev/null +++ b/tests/MagicOnion.Server.JsonTranscoding.Tests/UnaryFunctionalTests.cs @@ -0,0 +1,130 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Net.Http.Headers; +using System.Text.Json; +using MessagePack; +using Microsoft.Extensions.DependencyInjection; + +namespace MagicOnion.Server.JsonTranscoding.Tests; + +public class UnaryFunctionalTests(JsonTranscodingEnabledMagicOnionApplicationFactory factory) : IClassFixture> +{ + [Fact] + public async Task NotImplemented() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + + // Act + var response = await httpClient.PostAsync($"http://localhost/_/ITestService/NotImplemented", new StringContent(string.Empty, new MediaTypeHeaderValue("application/json"))); + + // Assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + [Fact] + public async Task Method_NoParameter_NoResult() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + + // Act + var response = await httpClient.PostAsync($"http://localhost/_/ITestService/Method_NoParameter_NoResult", new StringContent(string.Empty, new MediaTypeHeaderValue("application/json"))); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + var result = default(object); // null + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(result, JsonSerializer.Deserialize(content)); + Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString()); + Assert.True((bool)factory.Items.GetValueOrDefault($"{nameof(Method_NoParameter_NoResult)}.Called", false)); + } + + [Fact] + public async Task Method_NoParameter_ResultRefType() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + + // Act + var response = await httpClient.PostAsync($"http://localhost/_/ITestService/Method_NoParameter_ResultRefType", new StringContent(string.Empty, new MediaTypeHeaderValue("application/json"))); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + var result = nameof(Method_NoParameter_ResultRefType); // string + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(result, JsonSerializer.Deserialize(content)); + Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString()); + } + + [Fact] + public async Task Method_NoParameter_ResultComplexType() + { + // Arrange + var httpClient = factory.CreateDefaultClient(); + + // Act + var response = await httpClient.PostAsync($"http://localhost/_/ITestService/Method_NoParameter_ResultComplexType", new StringContent(string.Empty, new MediaTypeHeaderValue("application/json"))); + var content = await response.Content.ReadAsStringAsync(); + + // Assert + object[] result = [1234, "Alice", true, new object[] { 98765432100, "Hello!" }]; + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(JsonSerializer.Serialize(result), JsonSerializer.Serialize(JsonSerializer.Deserialize(content))); + Assert.Equal("application/json", response.Content.Headers.ContentType?.ToString()); + } +} + +public interface ITestService : IService +{ + UnaryResult Method_NoParameter_NoResult(); + UnaryResult Method_NoParameter_ResultRefType(); + UnaryResult Method_NoParameter_ResultComplexType(); +} + +[MessagePackObject] +public class TestResponse +{ + [Key(0)] + public int A { get; set; } + [Key(1)] + public required string B { get; init; } + [Key(2)] + public bool C { get; set; } + [Key(3)] + public required InnerResponse Inner { get; init; } + + [MessagePackObject] + public class InnerResponse + { + [Key(0)] + public long D { get; set; } + [Key(1)] + public required string E { get; init; } + } +} + +public class TestService([FromKeyedServices(MagicOnionApplicationFactory.ItemsKey)] ConcurrentDictionary items) : ServiceBase, ITestService +{ + public UnaryResult Method_NoParameter_NoResult() + { + items[$"{nameof(Method_NoParameter_NoResult)}.Called"] = true; + return default; + } + +public UnaryResult Method_NoParameter_ResultRefType() + => UnaryResult.FromResult(nameof(Method_NoParameter_ResultRefType)); + + public UnaryResult Method_NoParameter_ResultComplexType() + => UnaryResult.FromResult(new TestResponse() + { + A = 1234, + B = "Alice", + C = true, + Inner = new TestResponse.InnerResponse() + { + D = 98765432100, + E = "Hello!", + }, + }); +} diff --git a/tests/MagicOnion.Server.JsonTranscoding.Tests/Usings.cs b/tests/MagicOnion.Server.JsonTranscoding.Tests/Usings.cs new file mode 100644 index 000000000..e6bc4f2db --- /dev/null +++ b/tests/MagicOnion.Server.JsonTranscoding.Tests/Usings.cs @@ -0,0 +1,2 @@ +global using Xunit; +global using MagicOnion.Server.InternalTesting;