Files
2026-09-02 09:38:45 +08:00

186 lines
7.1 KiB
C#

using System.Net;
using System.Text;
using Yh.Platform.Abstractions;
using Yh.Platform.OpenApi;
using Xunit;
namespace Yh.Platform.OpenApi.Tests;
/// <summary>伪造 HttpMessageHandler:捕获最近一次请求并回放预设响应。</summary>
public sealed class FakeHandler : HttpMessageHandler
{
private readonly Func<HttpRequestMessage, string?, HttpResponseMessage> _responder;
public FakeHandler(Func<HttpRequestMessage, string?, HttpResponseMessage> responder)
{
_responder = responder;
}
public HttpRequestMessage? LastRequest { get; private set; }
public string? LastRequestBody { get; private set; }
protected override async Task<HttpResponseMessage> 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"),
});
}
/// <summary>
/// 信封感知解析对拍(对齐 contract/fixtures/envelope-*.json 约定):
/// 成功为裸数据;业务失败为 HTTP 200 + {code≠0,msg,data},客户端必须抛 DException。
/// </summary>
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<DException>(() => new OpenApiGroupClient(new HttpClient(handler), Options()).GetGroupByIdAsync(9));
var ec = Assert.IsType<OpenApiErrorCode>(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<DException>(() => Client(handler).GetUserByIdAsync("1"));
Assert.Equal(4, Assert.IsType<OpenApiErrorCode>(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<OpenApiException>(() => 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<DException>(() => 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);
}
}