namespace Yh.SchClass.OpenApi.Tests; /// /// 响应解析与状态码归一(教务 OA 兼容端点约定): /// 成功返回裸数据;404 / 204 表示"无数据"归一为 null / 空列表;400 及其它非 2xx 抛异常。 /// public class SchClassResponseParsingTests { private static readonly SchClassOpenApiOptions Options = new() { BaseHost = "http://base.yunhaixingtong.com" }; private static SchClassVClassClient VClass(FakeHandler handler) => new(new HttpClient(handler), Options); private static SchClassTeaInfoClient Tea(FakeHandler handler) => new(new HttpClient(handler), Options); private static SchClassSchoolingClient Schooling(FakeHandler handler) => new(new HttpClient(handler), Options); [Fact] public async Task 裸数组_解析为兼容行列表() { var handler = FakeHandler.Json(HttpStatusCode.OK, """ [{"Id":1,"Name":"高一(1)班","ClassID":1001,"SchSerID":"S001"}, {"Id":2,"Name":"高一(2)班","ClassID":1002,"SchSerID":"S001"}] """); var rows = await VClass(handler).GetVClassListAsync("S001"); Assert.Equal(2, rows.Count); Assert.Equal("高一(1)班", rows[0]["Name"].GetString()); Assert.Equal(1001, rows[0]["ClassID"].GetInt32()); Assert.Equal("S001", rows[1]["SchSerID"].GetString()); } [Fact] public async Task 单对象_归一为单项列表() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"Id":9,"Name":"高三(7)班"}"""); var rows = await VClass(handler).GetVClassListAsync("S001"); Assert.Single(rows); Assert.Equal(9, rows[0]["Id"].GetInt32()); } [Fact] public async Task 裸字符串数组_解析为List_string() { var handler = FakeHandler.Json(HttpStatusCode.OK, """["语文","数学"]"""); var subs = await new SchClassSubInfoClient(new HttpClient(handler), Options) .GetSchTeachSubListAsync("S001"); Assert.Equal(new[] { "语文", "数学" }, subs); } [Fact] public async Task HTTP404_列表归一为空列表() { var rows = await VClass(FakeHandler.Empty(HttpStatusCode.NotFound)).GetVClassListAsync("S001"); Assert.Empty(rows); } [Fact] public async Task HTTP404_单对象归一为null() { var row = await VClass(FakeHandler.Empty(HttpStatusCode.NotFound)).GetVClassFirstAsync("1001"); Assert.Null(row); } [Fact] public async Task HTTP204_归一为空列表() { var handler = FakeHandler.Json(HttpStatusCode.NoContent, ""); var list = await Tea(handler).GetArrTeaInfoAsync("1,2,3"); Assert.Empty(list); } [Fact] public async Task HTTP400_抛异常并携带状态码与响应体() { var handler = FakeHandler.Text(HttpStatusCode.BadRequest, "Required String parameter 'userIds' is not present"); var ex = await Assert.ThrowsAsync(() => Tea(handler).GetArrTeaInfoAsync(null!)); Assert.Equal(400, ex.StatusCode); Assert.Contains("400", ex.Message); Assert.Contains("userIds", ex.ResponseBody!); } [Fact] public async Task HTTP500_抛异常() { var handler = FakeHandler.Text(HttpStatusCode.InternalServerError, "boom"); var ex = await Assert.ThrowsAsync(() => VClass(handler).GetVClassListAsync("S001")); Assert.Equal(500, ex.StatusCode); Assert.Contains("500", ex.Message); } [Fact] public async Task 空响应体_返回空列表不抛() { var handler = new FakeHandler((_, _) => new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("", Encoding.UTF8, "application/json"), }); Assert.Empty(await VClass(handler).GetVClassListAsync("S001")); Assert.Null(await VClass(handler).GetVClassFirstAsync("1")); } [Fact] public async Task JSON字面量null_返回空列表() { var handler = FakeHandler.Json(HttpStatusCode.OK, "null"); Assert.Empty(await VClass(handler).GetVClassListAsync("S001")); } [Fact] public async Task 信封code0_提取data节点() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":0,"msg":"","data":[{"Id":1,"Name":"高一(1)班"}]}"""); var rows = await VClass(handler).GetVClassListAsync("S001"); Assert.Single(rows); Assert.Equal("高一(1)班", rows[0]["Name"].GetString()); } [Fact] public async Task 信封code0且data为null_归一为空() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":0,"msg":"ok","data":null}"""); Assert.Empty(await VClass(handler).GetVClassListAsync("S001")); } [Fact] public async Task 信封业务失败_抛异常并携带msg() { var handler = FakeHandler.Json(HttpStatusCode.OK, """{"code":3,"msg":"学校不存在","data":null}"""); var ex = await Assert.ThrowsAsync(() => VClass(handler).GetVClassListAsync("S001")); Assert.Equal("学校不存在", ex.Message); Assert.Equal(0, ex.StatusCode); Assert.Contains("学校不存在", ex.ResponseBody!); } [Fact] public async Task 业务行含code字段_不被误判为信封() { // 学制等级行自身带 code=gradeId 字段,只有 code 而没有 data/msg,必须原样返回 var handler = FakeHandler.Json(HttpStatusCode.OK, """[{"Id":1,"Name":"一年级","OwnerId":2,"Levell":1,"code":11,"sx":1}]"""); var rows = await Schooling(handler).GetSchLenLevelListAsync("S001"); Assert.Single(rows); Assert.Equal(11, rows[0]["code"].GetInt32()); Assert.Equal("一年级", rows[0]["Name"].GetString()); } [Fact] public async Task 非法JSON_抛解析异常() { var handler = FakeHandler.Json(HttpStatusCode.OK, "502 Bad Gateway"); var ex = await Assert.ThrowsAsync(() => VClass(handler).GetVClassListAsync("S001")); Assert.Contains("解析失败", ex.Message); } [Fact] public async Task 兼容行键名_大小写与Java侧put顺序一致() { var handler = FakeHandler.Json(HttpStatusCode.OK, """[{"SchSerID":"S001","TeaTel":"13800000000","TeaName":"王老师","UserTeaSerID":77}]"""); var rows = await new SchClassDbClassClient(new HttpClient(handler), Options).GetSchteaListAsync(1); Assert.Equal(new[] { "SchSerID", "TeaTel", "TeaName", "UserTeaSerID" }, rows[0].Keys); Assert.Equal("王老师", rows[0]["TeaName"].GetString()); } }