using System.Net; using System.Text; using Yh.Platform.Abstractions; using Yh.Platform.OpenApi; using Xunit; namespace Yh.Platform.OpenApi.Tests; /// 伪造 HttpMessageHandler:捕获最近一次请求并回放预设响应。 public sealed class FakeHandler : HttpMessageHandler { private readonly Func _responder; public FakeHandler(Func responder) { _responder = responder; } public HttpRequestMessage? LastRequest { get; private set; } public string? LastRequestBody { get; private set; } protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { LastRequest = request; LastRequestBody = request.Content == null ? null : await request.Content.ReadAsStringAsync(cancellationToken); return _responder(request, LastRequestBody); } public static FakeHandler Json(HttpStatusCode statusCode, string json) => new((_, _) => new HttpResponseMessage(statusCode) { Content = new StringContent(json, Encoding.UTF8, "application/json"), }); } /// /// 信封感知解析对拍(对齐 contract/fixtures/envelope-*.json 约定): /// 成功为裸数据;业务失败为 HTTP 200 + {code≠0,msg,data},客户端必须抛 DException。 /// public class EnvelopeParsingTests { private static OpenApiOptions Options() => new() { BaseHost = "http://base.bestsch.com", BasePath = "/userbase" }; private static OpenApiUserClient Client(FakeHandler handler) => new(new HttpClient(handler), Options()); [Fact] public async Task 裸数据成功响应_解析为BoaUser() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"id":1001,"account":"zhangsan","name":"张三","currentRole":{"roleId":3,"roleName":"教师","schId":2}}"""); var user = await Client(handler).GetUserByIdAsync("1001"); Assert.NotNull(user); Assert.Equal(1001, user!.Id); Assert.Equal("zhangsan", user.Account); Assert.Equal("张三", user.Name); Assert.NotNull(user.CurrentRole); Assert.Equal(3, user.CurrentRole!.RoleId); Assert.Equal("教师", user.CurrentRole.RoleName); } [Fact] public async Task 包裹信封code0_提取data节点() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":0,"msg":"","data":{"id":1001,"account":"zhangsan","name":"张三"}}"""); var user = await Client(handler).GetUserByIdAsync("1001"); Assert.NotNull(user); Assert.Equal("zhangsan", user!.Account); } [Fact] public async Task 业务失败信封_抛DException并携带code与data() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":5,"msg":"组下存在子组或用户,禁止删除","data":{"groupId":9,"users":35}}"""); var ex = await Assert.ThrowsAsync(() => new OpenApiGroupClient(new HttpClient(handler), Options()).GetGroupByIdAsync(9)); var ec = Assert.IsType(ex.Code); Assert.Equal(5, ec.Code); Assert.Equal("组下存在子组或用户,禁止删除", ec.Msg); Assert.Contains("禁止删除", ex.Message); Assert.NotNull(ex.Data); } [Fact] public async Task 业务失败信封data为null_EnvelopeData为null() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":4,"msg":"登录已经过期,请重新登录","data":null}"""); var ex = await Assert.ThrowsAsync(() => Client(handler).GetUserByIdAsync("1")); Assert.Equal(4, Assert.IsType(ex.Code).Code); Assert.Null(ex.EnvelopeData); } [Fact] public async Task 非成功HTTP状态_抛OpenApiException() { var handler = FakeHandler.Json(HttpStatusCode.InternalServerError, "boom"); var ex = await Assert.ThrowsAsync(() => Client(handler).GetUserByIdAsync("1")); Assert.Contains("500", ex.Message); } [Fact] public async Task 列表解析_裸数组与单对象两种形态() { var arrayHandler = FakeHandler.Json(HttpStatusCode.OK, """[{"roleId":3,"roleName":"教师","schId":2},{"roleId":5,"roleName":"班主任","schId":2}]"""); var roles = await Client(arrayHandler).GetUserRoleListAsync(1001); Assert.Equal(2, roles.Count); Assert.Equal("教师", roles[0].RoleName); Assert.Equal(5, roles[1].RoleId); var singleHandler = FakeHandler.Json(HttpStatusCode.OK, """{"roleId":3,"roleName":"教师","schId":2}"""); var single = await Client(singleHandler).GetUserRoleListAsync(1001); Assert.Single(single); } [Fact] public async Task GET请求_URL与query构造正确() { var handler = FakeHandler.Json(HttpStatusCode.OK, "null"); await Client(handler).GetUserModuleAsync(1001, 2); var url = handler.LastRequest!.RequestUri!; Assert.Equal("http://base.bestsch.com/userbase/oapi/user/modules", $"{url.Scheme}://{url.Host}{url.AbsolutePath}"); Assert.Equal("?userId=1001&schId=2", url.Query); Assert.Equal(HttpMethod.Get, handler.LastRequest.Method); } [Fact] public async Task query中的null值参数被丢弃() { var handler = FakeHandler.Json(HttpStatusCode.OK, "[]"); await Client(handler).GetUserModuleAsync(1001, null); Assert.DoesNotContain("schId", handler.LastRequest!.RequestUri!.Query); Assert.Equal("?userId=1001", handler.LastRequest.RequestUri.Query); } [Fact] public async Task POST请求_编辑用户时路径id回写请求体() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"id":7,"account":"lisi","name":"李四"}"""); var request = new SysUserRequest { Account = "lisi", RealName = "李四" }; await Client(handler).EditUserAsync(7, request); Assert.Equal(HttpMethod.Post, handler.LastRequest!.Method); Assert.EndsWith("/userbase/oapi/user/edit/7", handler.LastRequest.RequestUri!.AbsolutePath); Assert.NotNull(handler.LastRequestBody); Assert.Contains("\"id\":7", handler.LastRequestBody); } [Fact] public async Task void解析_业务失败仍抛异常_成功不抛() { var ok = FakeHandler.Json(HttpStatusCode.OK, """{"code":0,"msg":"","data":null}"""); await Client(ok).DeleteUserAsync(1, 2, 3); var fail = FakeHandler.Json(HttpStatusCode.OK, """{"code":5,"msg":"无权限","data":null}"""); await Assert.ThrowsAsync(() => Client(fail).DeleteUserAsync(1, 2, 3)); } [Fact] public async Task 空响应体_返回default不抛() { var handler = new FakeHandler((_, _) => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("", Encoding.UTF8, "application/json"), }); var user = await Client(handler).GetUserByIdAsync("1"); Assert.Null(user); } }