diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs index 21786155bf..5be89375a0 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/IRegularTestController.cs @@ -1,6 +1,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using Volo.Abp.Content; namespace Volo.Abp.Http.DynamicProxying; @@ -8,6 +9,24 @@ public interface IRegularTestController { Task IncrementValueAsync(int value); + Task GetPlainStringAsync(); + + Task GetProducesJsonStringAsync(); + + Task GetProducesTextStringAsync(); + + Task GetNullStringAsync(); + + Task GetProducesJsonNullStringAsync(); + + Task GetEmptyStringAsync(); + + Task GetEscapedStringAsync(); + + Task DownloadIconAsync(); + + Task GetByteArrayAsync(); + Task GetException1Async(); Task GetException2Async(); diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PeopleAppServiceClientProxy_ReturnContentTypes_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PeopleAppServiceClientProxy_ReturnContentTypes_Tests.cs new file mode 100644 index 0000000000..b89692eb90 --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/PeopleAppServiceClientProxy_ReturnContentTypes_Tests.cs @@ -0,0 +1,84 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Http.Client; +using Volo.Abp.TestApp.Application; +using Xunit; + +namespace Volo.Abp.Http.DynamicProxying; + +public class PeopleAppServiceClientProxy_ReturnContentTypes_Tests : AbpHttpClientTestBase +{ + private readonly IPeopleAppService _peopleAppService; + + public PeopleAppServiceClientProxy_ReturnContentTypes_Tests() + { + _peopleAppService = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task EchoStatusAsync_Should_Return_Plain_String_Without_Quotes() + { + var status = await _peopleAppService.EchoStatusAsync(); + status.ShouldBe("Open"); + status.StartsWith("\"").ShouldBeFalse(); + status.EndsWith("\"").ShouldBeFalse(); + } + + [Fact] + public async Task EchoStatusWithProducesJsonAsync_Should_Return_Plain_String_Without_Quotes() + { + var status = await _peopleAppService.EchoStatusWithProducesJsonAsync(); + status.ShouldBe("Open"); + status.StartsWith("\"").ShouldBeFalse(); + status.EndsWith("\"").ShouldBeFalse(); + } + + [Fact] + public async Task GetBinaryImageAsync_Should_Return_Real_Binary_Not_Json_Metadata() + { + using var content = await _peopleAppService.GetBinaryImageAsync(); + using var ms = new MemoryStream(); + await content.GetStream().CopyToAsync(ms); + var bytes = ms.ToArray(); + + content.FileName.ShouldBe("tiny.png"); + content.ContentType.ShouldStartWith("image/png"); + bytes.Length.ShouldBeGreaterThan(8); + + bytes[0].ShouldBe((byte)0x89); + bytes[1].ShouldBe((byte)0x50); + bytes[2].ShouldBe((byte)0x4E); + bytes[3].ShouldBe((byte)0x47); + } + + [Fact] + public async Task ThrowFromStringAsync_Should_Surface_Server_Exception_To_Client() + { + await Should.ThrowAsync( + () => _peopleAppService.ThrowFromStringAsync() + ); + } + + [Fact] + public async Task DownloadAsync_Should_Still_Work() + { + using var content = await _peopleAppService.DownloadAsync(); + using var reader = new StreamReader(content.GetStream()); + var text = await reader.ReadToEndAsync(); + text.ShouldBe("DownloadAsync"); + content.FileName.ShouldBe("download.rtf"); + content.ContentType.ShouldStartWith("application/rtf"); + } + + [Fact] + public async Task UploadAsync_String_Return_Should_Stay_Unquoted() + { + using var ms = new MemoryStream(System.Text.Encoding.UTF8.GetBytes("hello")); + var result = await _peopleAppService.UploadAsync( + new Content.RemoteStreamContent(ms, "upload.txt", "text/plain")); + result.ShouldBe("hello:text/plain:upload.txt"); + result.StartsWith("\"").ShouldBeFalse(); + } +} diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs index 46b355090b..437d047b71 100644 --- a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestController.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Content; namespace Volo.Abp.Http.DynamicProxying; @@ -20,6 +23,75 @@ public class RegularTestController : AbpController, IRegularTestController return Task.FromResult(value + 1); } + [HttpGet] + [Route("plain-string")] + public Task GetPlainStringAsync() + { + return Task.FromResult("Open"); + } + + [HttpGet] + [Route("produces-json-string")] + [Produces("application/json")] + public Task GetProducesJsonStringAsync() + { + return Task.FromResult("Open"); + } + + [HttpGet] + [Route("produces-text-string")] + [Produces("text/plain")] + public Task GetProducesTextStringAsync() + { + return Task.FromResult("Open"); + } + + [HttpGet] + [Route("null-string")] + public Task GetNullStringAsync() + { + return Task.FromResult(null!); + } + + [HttpGet] + [Route("produces-json-null-string")] + [Produces("application/json")] + public Task GetProducesJsonNullStringAsync() + { + return Task.FromResult(null!); + } + + [HttpGet] + [Route("empty-string")] + public Task GetEmptyStringAsync() + { + return Task.FromResult(string.Empty); + } + + [HttpGet] + [Route("escaped-string")] + [Produces("application/json")] + public Task GetEscapedStringAsync() + { + return Task.FromResult("a\"b\\c\nd"); + } + + [HttpGet] + [Route("download-icon")] + public Task DownloadIconAsync() + { + var bytes = Encoding.UTF8.GetBytes("ICON-BYTES"); + return Task.FromResult( + new RemoteStreamContent(new MemoryStream(bytes), "icon.bin", "application/octet-stream")); + } + + [HttpGet] + [Route("byte-array")] + public Task GetByteArrayAsync() + { + return Task.FromResult(new byte[] { 1, 2, 3, 4 }); + } + [HttpGet] [Route("get-exception1")] public Task GetException1Async() diff --git a/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_ReturnContentTypes_Tests.cs b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_ReturnContentTypes_Tests.cs new file mode 100644 index 0000000000..a6725d6e6c --- /dev/null +++ b/framework/test/Volo.Abp.Http.Client.Tests/Volo/Abp/Http/DynamicProxying/RegularTestControllerClientProxy_ReturnContentTypes_Tests.cs @@ -0,0 +1,102 @@ +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Xunit; + +namespace Volo.Abp.Http.DynamicProxying; + +public class RegularTestControllerClientProxy_ReturnContentTypes_Tests : AbpHttpClientTestBase +{ + private readonly IRegularTestController _controller; + + public RegularTestControllerClientProxy_ReturnContentTypes_Tests() + { + _controller = ServiceProvider.GetRequiredService(); + } + + [Fact] + public async Task GetPlainStringAsync_Should_Return_Unwrapped_String() + { + var result = await _controller.GetPlainStringAsync(); + result.ShouldBe("Open"); + result.StartsWith("\"").ShouldBeFalse(); + } + + [Fact] + public async Task GetProducesJsonStringAsync_Should_Strip_Json_Quotes() + { + var result = await _controller.GetProducesJsonStringAsync(); + result.ShouldBe("Open"); + result.StartsWith("\"").ShouldBeFalse(); + result.EndsWith("\"").ShouldBeFalse(); + } + + [Fact] + public async Task GetProducesTextStringAsync_Should_Return_Raw_Text_Body() + { + var result = await _controller.GetProducesTextStringAsync(); + result.ShouldBe("Open"); + result.StartsWith("\"").ShouldBeFalse(); + } + + [Fact] + public async Task GetNullStringAsync_Should_Return_Default_Or_Empty() + { + var result = await _controller.GetNullStringAsync(); + (result == null || result == string.Empty).ShouldBeTrue(); + } + + [Fact] + public async Task GetEmptyStringAsync_Should_Return_Empty() + { + var result = await _controller.GetEmptyStringAsync(); + (result == null || result == string.Empty).ShouldBeTrue(); + } + + [Fact] + public async Task GetProducesJsonNullStringAsync_Should_Not_Return_Literal_Null() + { + // Server returns JSON `null` body (4 chars). The unwrap MUST NOT pass through "null" literal — + // it should produce empty/null on the client side instead. + var result = await _controller.GetProducesJsonNullStringAsync(); + result.ShouldNotBe("null"); + (result == null || result == string.Empty).ShouldBeTrue(); + } + + [Fact] + public async Task GetEscapedStringAsync_Should_Decode_Escaped_Characters() + { + // Server JSON-encodes the string with escapes: "a\"b\\c\nd" + // Without unwrap fix client would receive the raw JSON string including escapes. + var result = await _controller.GetEscapedStringAsync(); + result.ShouldBe("a\"b\\c\nd"); + } + + [Fact] + public async Task DownloadIconAsync_Should_Return_Binary_Bytes() + { + using var content = await _controller.DownloadIconAsync(); + using var ms = new MemoryStream(); + await content.GetStream().CopyToAsync(ms); + ms.ToArray().ShouldBe(System.Text.Encoding.UTF8.GetBytes("ICON-BYTES")); + content.FileName.ShouldBe("icon.bin"); + } + + [Fact] + public async Task GetByteArrayAsync_Should_Round_Trip_Bytes() + { + // byte[] is not IRemoteStreamContent; goes through default JSON path + // (server JSON-encodes as base64). Ensures our Accept logic didn't break + // the existing non-stream binary case. + var bytes = await _controller.GetByteArrayAsync(); + bytes.ShouldBe(new byte[] { 1, 2, 3, 4 }); + } + + [Fact] + public async Task Existing_IncrementValueAsync_Regression_Should_Still_Work() + { + var result = await _controller.IncrementValueAsync(41); + result.ShouldBe(42); + } +} diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs index a39216add5..24ad49d7fa 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/IPeopleAppService.cs @@ -39,4 +39,12 @@ public interface IPeopleAppService : ICrudAppService Task GetParamsFromQueryAsync(GetParamsInput input); Task GetParamsFromFormAsync(GetParamsInput input); + + Task EchoStatusAsync(); + + Task EchoStatusWithProducesJsonAsync(); + + Task GetBinaryImageAsync(); + + Task ThrowFromStringAsync(); } diff --git a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs index a3920ec8ca..5eb9370754 100644 --- a/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs +++ b/framework/test/Volo.Abp.TestApp/Volo/Abp/TestApp/Application/PeopleAppService.cs @@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Shouldly; +using Volo.Abp; using Volo.Abp.Application.Dtos; using Volo.Abp.TestApp.Domain; using Volo.Abp.Domain.Repositories; @@ -94,6 +95,30 @@ public class PeopleAppService : CrudAppService, IPeople return new RemoteStreamContent(memoryStream, "download.rtf", "application/rtf"); } + public Task EchoStatusAsync() + { + return Task.FromResult("Open"); + } + + [Produces("application/json")] + public Task EchoStatusWithProducesJsonAsync() + { + return Task.FromResult("Open"); + } + + public Task GetBinaryImageAsync() + { + var bytes = Convert.FromBase64String( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII="); + return Task.FromResult( + new RemoteStreamContent(new MemoryStream(bytes), "tiny.png", "image/png")); + } + + public Task ThrowFromStringAsync() + { + throw new BusinessException("TestApp.StringEndpointBoom", "string endpoint failed"); + } + public async Task UploadAsync(IRemoteStreamContent streamContent) { using (var reader = new StreamReader(streamContent.GetStream()))