bag
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
// 测试工程的运行调试入口(非测试用例)。
|
||||
//
|
||||
// 用法:
|
||||
// dotnet run --project tests/Yh.SchClass.OpenApi.Tests # 一键跑通全部 20 个端点
|
||||
// dotnet run --project tests/Yh.SchClass.OpenApi.Tests -- --only=OAStu
|
||||
// dotnet run --project tests/Yh.SchClass.DebugTool -- --list # 端点名清单见 DebugTool
|
||||
//
|
||||
// 地址优先级同 SchClassLiveDebug:环境变量 YH_SCHCLASS_DEBUG_* → appsettings.json → 内置默认。
|
||||
// 在 IDE 里直接 F5 调试本项目即可进入这里(在 Main 内打断点)。
|
||||
//
|
||||
// 注意:csproj 已设 GenerateProgramFile=false(关掉 Test SDK 自动 Main),dotnet test 不受影响。
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
public static class DebugMain
|
||||
{
|
||||
public static async Task<int> Main(string[] args)
|
||||
{
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
|
||||
string? only = null;
|
||||
foreach (var arg in args)
|
||||
{
|
||||
if (arg.StartsWith("--only=", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
only = arg[7..].Trim();
|
||||
}
|
||||
else if (arg.Equals("--off", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
Console.WriteLine("跳过联调:--off(同 YH_SCHCLASS_DEBUG_OFF=1)");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.GetEnvironmentVariable(SchClassLiveDebug.OffVar) is "1" or "true")
|
||||
{
|
||||
Console.WriteLine("跳过联调:已设置 " + SchClassLiveDebug.OffVar + "=1(清除该变量即可恢复联调)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.WriteLine("== SchClass OpenAPI 测试工程调试入口 ==");
|
||||
var failures = await SchClassLiveDebug.RunAllAsync(Console.WriteLine, only);
|
||||
if (failures.Count == 0)
|
||||
{
|
||||
Console.WriteLine("全部通过 ✅");
|
||||
return 0;
|
||||
}
|
||||
Console.WriteLine($"失败 {failures.Count} 个:{string.Join(", ", failures)}");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 伪造 <see cref="HttpMessageHandler"/>:捕获最近一次请求并回放预设响应。
|
||||
/// 与 <c>Yh.Platform.OpenApi.Tests</c> 的 FakeHandler 同构,便于两个 SDK 的测试互相参照。
|
||||
/// </summary>
|
||||
public sealed class FakeHandler : HttpMessageHandler
|
||||
{
|
||||
private readonly Func<HttpRequestMessage, string?, HttpResponseMessage> _responder;
|
||||
|
||||
public FakeHandler(Func<HttpRequestMessage, string?, HttpResponseMessage> responder)
|
||||
{
|
||||
_responder = responder;
|
||||
}
|
||||
|
||||
/// <summary>最近一次捕获到的请求。</summary>
|
||||
public HttpRequestMessage? LastRequest { get; private set; }
|
||||
|
||||
/// <summary>最近一次请求体(无请求体时为 null)。</summary>
|
||||
public string? LastRequestBody { get; private set; }
|
||||
|
||||
/// <summary>最近一次请求体的 Content-Type(无请求体时为 null)。</summary>
|
||||
public string? LastContentType { get; private set; }
|
||||
|
||||
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||
{
|
||||
LastRequest = request;
|
||||
if (request.Content == null)
|
||||
{
|
||||
LastRequestBody = null;
|
||||
LastContentType = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
LastRequestBody = await request.Content.ReadAsStringAsync(cancellationToken);
|
||||
LastContentType = request.Content.Headers.ContentType?.MediaType;
|
||||
}
|
||||
return _responder(request, LastRequestBody);
|
||||
}
|
||||
|
||||
/// <summary>回放一段固定 JSON(application/json)。</summary>
|
||||
public static FakeHandler Json(HttpStatusCode statusCode, string json)
|
||||
=> new((_, _) => new HttpResponseMessage(statusCode)
|
||||
{
|
||||
Content = new StringContent(json, Encoding.UTF8, "application/json"),
|
||||
});
|
||||
|
||||
/// <summary>回放指定状态码且无响应体(模拟 204 / 404 空响应)。</summary>
|
||||
public static FakeHandler Empty(HttpStatusCode statusCode)
|
||||
=> new((_, _) => new HttpResponseMessage(statusCode));
|
||||
|
||||
/// <summary>回放纯文本响应(模拟非 JSON 的错误页)。</summary>
|
||||
public static FakeHandler Text(HttpStatusCode statusCode, string text)
|
||||
=> new((_, _) => new HttpResponseMessage(statusCode)
|
||||
{
|
||||
Content = new StringContent(text, Encoding.UTF8, "text/plain"),
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.IO;
|
||||
global using System.Linq;
|
||||
global using System.Net;
|
||||
global using System.Net.Http;
|
||||
global using System.Text;
|
||||
global using System.Text.Json;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using Yh.SchClass.OpenApi;
|
||||
global using Yh.SchClass.OpenApi.Model;
|
||||
global using Xunit;
|
||||
|
||||
// Java 侧 Map<String,Object> 兼容行的 .NET 承载类型(与 src 侧同名别名保持一致)
|
||||
global using SchClassRow = System.Collections.Generic.Dictionary<string, System.Text.Json.JsonElement>;
|
||||
@@ -0,0 +1,187 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 联调调试用的环境配置。地址来源优先级:
|
||||
/// <para>
|
||||
/// 1. 环境变量(<c>YH_SCHCLASS_DEBUG_HOST</c> / <c>YH_SCHCLASS_DEBUG_PATH</c>);
|
||||
/// 2. <c>appsettings.json</c>:先独立节 <c>yh:schclass:open</c>,再共享节 <c>yh:open</c> 的
|
||||
/// <c>BaseHost</c> + <c>SchClassPath</c>;
|
||||
/// 3. 内置默认 <see cref="DefaultHost"/> + <see cref="DefaultPath"/>。
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// 关闭联调:置 <c>YH_SCHCLASS_DEBUG_OFF=1</c>,用例全部空跑通过(CI 用)。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class SchClassLiveDebug
|
||||
{
|
||||
public const string HostVar = "YH_SCHCLASS_DEBUG_HOST";
|
||||
public const string PathVar = "YH_SCHCLASS_DEBUG_PATH";
|
||||
public const string SchVar = "YH_SCHCLASS_DEBUG_SCH";
|
||||
public const string ClassVar = "YH_SCHCLASS_DEBUG_CLASS";
|
||||
public const string TeaVar = "YH_SCHCLASS_DEBUG_TEA";
|
||||
public const string GroupVar = "YH_SCHCLASS_DEBUG_GROUP";
|
||||
public const string OffVar = "YH_SCHCLASS_DEBUG_OFF";
|
||||
|
||||
/// <summary>配置与配置文件都未提供时的兜底联调主机。</summary>
|
||||
public const string DefaultHost = "http://base.yunhaixingtong.com";
|
||||
|
||||
/// <summary>配置与配置文件都未提供时的兜底应用路径。</summary>
|
||||
public const string DefaultPath = "/bschclass";
|
||||
|
||||
private static readonly IConfiguration? Config;
|
||||
|
||||
static SchClassLiveDebug()
|
||||
{
|
||||
var file = System.IO.Path.Combine(AppContext.BaseDirectory, "appsettings.json");
|
||||
if (File.Exists(file))
|
||||
{
|
||||
Config = new ConfigurationBuilder().AddJsonFile(file, optional: true).Build();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>是否开启联调(默认开启;置 <c>YH_SCHCLASS_DEBUG_OFF=1</c> 可关闭,CI 用它空跑)。</summary>
|
||||
public static bool Enabled
|
||||
{
|
||||
get
|
||||
{
|
||||
var off = Environment.GetEnvironmentVariable(OffVar);
|
||||
return !(off == "1" || "true".Equals(off, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
private static string? Env(string name)
|
||||
{
|
||||
var value = Environment.GetEnvironmentVariable(name);
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
private static string? Configured(string path)
|
||||
{
|
||||
var value = Config?[path];
|
||||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||
}
|
||||
|
||||
/// <summary>联调主机:环境变量 → yh:schclass:open:BaseHost → yh:open:BaseHost → 内置默认。</summary>
|
||||
public static string Host =>
|
||||
Env(HostVar)
|
||||
?? Configured("yh:schclass:open:BaseHost")
|
||||
?? Configured("yh:open:BaseHost")
|
||||
?? DefaultHost;
|
||||
|
||||
/// <summary>联调基础路径:环境变量 → yh:schclass:open:BasePath → yh:open:SchClassPath → 内置默认。</summary>
|
||||
public static string? Path =>
|
||||
Env(PathVar)
|
||||
?? Configured("yh:open:SchClassPath")
|
||||
?? DefaultPath;
|
||||
|
||||
/// <summary>按当前配置构建门面。</summary>
|
||||
public static SchClassOpenApi Create()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions { BaseHost = Host, BasePath = Path };
|
||||
return SchClassOpenApiFactory.Create(options);
|
||||
}
|
||||
|
||||
/// <summary>联调目标地址(用于日志输出)。</summary>
|
||||
public static string Target()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions { BaseHost = Host, BasePath = Path };
|
||||
return options.ResolveBaseUrl();
|
||||
}
|
||||
|
||||
public static string? Sch => Env(SchVar);
|
||||
public static string? ClassId => Env(ClassVar);
|
||||
public static int? TeaSerId => int.TryParse(Env(TeaVar), out var id) ? id : null;
|
||||
|
||||
/// <summary>DbClass 分组号,缺省 3(对联调环境真实有效的分组)。</summary>
|
||||
public static int? Group => int.TryParse(Env(GroupVar), out var g) ? g : null;
|
||||
|
||||
/// <summary>
|
||||
/// 一键跑通全部端点(测试用例与 <c>DebugMain.Main</c> 调试入口共用)。
|
||||
/// 缺省 ID 一律从真实数据推导:用捏造的 ID 会让端点恒成立 0 条,把真实问题伪装成"接口空"。
|
||||
/// 返回失败端点名列表;<paramref name="log"/> 输出进度。
|
||||
/// </summary>
|
||||
public static async Task<List<string>> RunAllAsync(Action<string> log, string? only = null)
|
||||
{
|
||||
var api = Create();
|
||||
var sch = Sch
|
||||
?? (await api.SchInfo.AllListAsync()).FirstOrDefault()?.SchSerId ?? "S001";
|
||||
var group = Group ?? 3;
|
||||
|
||||
var classId = ClassId;
|
||||
if (classId == null)
|
||||
{
|
||||
var cls = (await api.VClass.GetVClassListAsync(sch)).FirstOrDefault();
|
||||
classId = cls != null && cls.TryGetValue("ClassID", out var cid)
|
||||
? cid.ToString()
|
||||
: "1001";
|
||||
}
|
||||
|
||||
var teaId = TeaSerId;
|
||||
if (teaId == null)
|
||||
{
|
||||
var tea = (await api.DbClass.GetSchteaListAsync(group)).FirstOrDefault();
|
||||
teaId = tea != null
|
||||
&& tea.TryGetValue("UserTeaSerID", out var tid)
|
||||
&& int.TryParse(tid.ToString(), out var parsed)
|
||||
? parsed
|
||||
: 77;
|
||||
}
|
||||
|
||||
var cases = new List<(string Name, Func<Task<int>> Call)>
|
||||
{
|
||||
// Group=3 对应联调环境真实存在的分组;可用 YH_SCHCLASS_DEBUG_GROUP 覆盖
|
||||
("DbClass.GetSchteaList", async () => (await api.DbClass.GetSchteaListAsync(group)).Count),
|
||||
("VSchClass.GetVClassList", async () => (await api.VClass.GetVClassListAsync(sch)).Count),
|
||||
("VSchClass.GetSchArrVClassListAll", async () => (await api.VClass.GetSchArrVClassListAllAsync()).Count),
|
||||
("VSchClass.GetSchArrVClassListPost", async () => (await api.VClass.GetSchArrVClassListPostAsync(sch)).Count),
|
||||
("Schoolen.GetSchLenLevellist", async () => (await api.Schooling.GetSchLenLevelListAsync(sch)).Count),
|
||||
("Schoolen.GetSchLenlist", async () => (await api.Schooling.GetSchLenListAsync(sch)).Count),
|
||||
("UserInfo.GetUserList", async () => (await api.UserInfo.GetUserListAsync(sch)).Count),
|
||||
("SchInfo.AllList", async () => (await api.SchInfo.AllListAsync()).Count),
|
||||
("SchInfo.GetSchlist", async () => (await api.SchInfo.GetSchListAsync(null)).Count),
|
||||
("OATea.GetSchTeainfo", async () => (await api.TeaInfo.GetSchTeaInfoAsync(sch, null)).Count),
|
||||
("OATea.GetArrTeainfo", async () => (await api.TeaInfo.GetArrTeaInfoAsync(teaId.Value.ToString())).Count),
|
||||
("OAStu.GetAllStulist", async () => (await api.StuInfo.GetAllStuListAsync()).Count),
|
||||
("OAStu.GetSchStulist", async () => (await api.StuInfo.GetSchStuListAsync(sch, null)).Count),
|
||||
("OAStu.GetClaStulist", async () => (await api.StuInfo.GetClaStuListAsync(classId, null)).Count),
|
||||
("TeachInfo.GetSchTeachlist", async () => (await api.TeachInfo.GetSchTeachListAsync(sch, teaId)).Count),
|
||||
("TeachInfo.GetSchClaTeachlist", async () => (await api.TeachInfo.GetSchClaTeachListAsync(classId)).Count),
|
||||
("SubInfo.GetSchTeachSublist", async () => (await api.SubInfo.GetSchTeachSubListAsync(sch)).Count),
|
||||
("SubInfo.GetSchSublist", async () => (await api.SubInfo.GetSchSubListAsync(sch, null, null)).Count),
|
||||
("FamCom.GetStuUserFamIDList", async () => (await api.FamilyCompat.GetStuUserFamIdListAsync("1,2")).Count),
|
||||
("FamCom.GetAllStuFamMem", async () => (await api.FamilyCompat.GetAllStuFamMemAsync("1,2")).Count),
|
||||
};
|
||||
|
||||
if (only != null)
|
||||
{
|
||||
cases = cases.Where(c => c.Name.Contains(only, StringComparison.OrdinalIgnoreCase)).ToList();
|
||||
if (cases.Count == 0)
|
||||
{
|
||||
log($"--only={only} 未匹配任何端点");
|
||||
return new List<string> { "--only" };
|
||||
}
|
||||
}
|
||||
|
||||
var failures = new List<string>();
|
||||
log($"联调目标:{Target()} sch={sch} classId={classId} teaId={teaId} group={group}");
|
||||
|
||||
foreach (var (name, call) in cases)
|
||||
{
|
||||
try
|
||||
{
|
||||
var count = await call();
|
||||
log($" OK {name} -> {count} 条");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
failures.Add(name);
|
||||
log($" FAIL {name} -> {e.GetType().Name}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
log($"汇总:{cases.Count - failures.Count}/{cases.Count} 通过");
|
||||
return failures;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 联调调试用例:默认请求联调环境 <c>http://base.yunhaixingtong.com/bschclass</c>,
|
||||
/// 并把每个端点的返回条数/异常打印到测试输出,便于在 IDE 调试窗口里逐个端点排查。
|
||||
/// <para>
|
||||
/// 用 <c>YH_SCHCLASS_DEBUG_HOST</c> / <c>YH_SCHCLASS_DEBUG_PATH</c> 覆盖目标地址,
|
||||
/// 置 <c>YH_SCHCLASS_DEBUG_OFF=1</c> 可关闭联调(CI 用它空跑通过)。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class SchClassLiveDebugTests(ITestOutputHelper output)
|
||||
{
|
||||
private bool Live()
|
||||
{
|
||||
if (SchClassLiveDebug.Enabled)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
output.WriteLine("跳过联调:已设置 " + SchClassLiveDebug.OffVar + "=1(清除该变量即可恢复联调)");
|
||||
return false;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 联调_学校列表()
|
||||
{
|
||||
if (!Live()) return;
|
||||
|
||||
var api = SchClassLiveDebug.Create();
|
||||
var list = await api.SchInfo.AllListAsync();
|
||||
|
||||
foreach (var s in list)
|
||||
{
|
||||
output.WriteLine($"SchInfo: {s.SchSerId} / {s.UserSchSerId} / {s.SchName}");
|
||||
}
|
||||
output.WriteLine($"共 {list.Count} 所学校 —— {SchClassLiveDebug.Target()}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 联调_班级与学生()
|
||||
{
|
||||
if (!Live()) return;
|
||||
|
||||
var api = SchClassLiveDebug.Create();
|
||||
var sch = SchClassLiveDebug.Sch
|
||||
?? (await api.SchInfo.AllListAsync()).FirstOrDefault()?.SchSerId;
|
||||
if (string.IsNullOrEmpty(sch))
|
||||
{
|
||||
output.WriteLine("未取到学校序列号(设置 YH_SCHCLASS_DEBUG_SCH 或确认学校列表非空)");
|
||||
return;
|
||||
}
|
||||
|
||||
var classes = await api.VClass.GetVClassListAsync(sch);
|
||||
output.WriteLine($"VSchClass[{sch}] 共 {classes.Count} 个班级");
|
||||
|
||||
var students = await api.StuInfo.GetSchStuListAsync(sch, null);
|
||||
output.WriteLine($"OAStu[{sch}] 共 {students.Count} 名学生");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 联调_教师与任课()
|
||||
{
|
||||
if (!Live()) return;
|
||||
|
||||
var api = SchClassLiveDebug.Create();
|
||||
var sch = SchClassLiveDebug.Sch
|
||||
?? (await api.SchInfo.AllListAsync()).FirstOrDefault()?.SchSerId;
|
||||
|
||||
var teachers = await api.TeaInfo.GetSchTeaInfoAsync(sch, null);
|
||||
output.WriteLine($"OATea[{sch}] 共 {teachers.Count} 名教师");
|
||||
|
||||
if (SchClassLiveDebug.TeaSerId is { } teaId)
|
||||
{
|
||||
var teach = await api.TeachInfo.GetSchTeachListAsync(sch, teaId);
|
||||
output.WriteLine($"TeachInfo[{teaId}] 共 {teach.Count} 条任课");
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 联调_一键跑通全部端点()
|
||||
{
|
||||
if (!Live()) return;
|
||||
|
||||
var failures = await SchClassLiveDebug.RunAllAsync(output.WriteLine);
|
||||
Assert.Empty(failures);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 配置读取测试:对应 appsettings.json 的真实结构 ——
|
||||
/// <code>
|
||||
/// "yh": { "open": {
|
||||
/// "BaseHost": "http://base.yunhaixingtong.com",
|
||||
/// "BasePath": "/userbase", // UserBase 用,教务不用它
|
||||
/// "SchClassPath": "/bschclass" // 教务用它
|
||||
/// }}
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public class SchClassOpenApiConfigTests
|
||||
{
|
||||
private static IConfiguration BuildConfig(Dictionary<string, string?> kv)
|
||||
=> new ConfigurationBuilder().AddInMemoryCollection(kv).Build()!;
|
||||
|
||||
[Fact]
|
||||
public void 共享节_SchClassPath映射为教务BasePath()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
["yh:open:BasePath"] = "/userbase",
|
||||
["yh:open:SchClassPath"] = "/bschclass",
|
||||
});
|
||||
|
||||
var options = new SchClassOpenApiOptions();
|
||||
var shared = config.GetSection(SchClassOpenApiOptions.SharedSectionName);
|
||||
options.FillFromSharedSection(key => shared[key]);
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com", options.BaseHost);
|
||||
// 关键:教务用 SchClassPath,不是共享节里的 BasePath(/userbase)
|
||||
Assert.Equal("/bschclass", options.BasePath);
|
||||
Assert.Equal("http://base.yunhaixingtong.com/bschclass", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 共享节_路径带空格与斜杠可归一化()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
["yh:open:SchClassPath"] = " /bschclass/", // 模拟配置里常见的多余空格/尾斜杠
|
||||
});
|
||||
|
||||
var options = new SchClassOpenApiOptions();
|
||||
var shared = config.GetSection(SchClassOpenApiOptions.SharedSectionName);
|
||||
options.FillFromSharedSection(key => shared[key]);
|
||||
|
||||
Assert.Equal("/bschclass", options.BasePath);
|
||||
Assert.Equal("http://base.yunhaixingtong.com/bschclass", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 共享节_未配SchClassPath时BasePath保持空()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
["yh:open:BasePath"] = "/userbase", // 不允许被教务误用
|
||||
});
|
||||
|
||||
var options = new SchClassOpenApiOptions();
|
||||
var shared = config.GetSection(SchClassOpenApiOptions.SharedSectionName);
|
||||
options.FillFromSharedSection(key => shared[key]);
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com", options.BaseHost);
|
||||
Assert.Null(options.BasePath);
|
||||
Assert.Equal("http://base.yunhaixingtong.com", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 独立节优先_共享节仅回填缺失项()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:schclass:open:BasePath"] = "/sch-only", // 独立节只给了路径
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
["yh:open:SchClassPath"] = "/bschclass",
|
||||
["yh:open:AppKey"] = "100002",
|
||||
["yh:open:AppSecret"] = "secret",
|
||||
});
|
||||
|
||||
var options = new SchClassOpenApiOptions();
|
||||
config.GetSection(SchClassOpenApiOptions.SectionName).Bind(options);
|
||||
var shared = config.GetSection(SchClassOpenApiOptions.SharedSectionName);
|
||||
options.FillFromSharedSection(key => shared[key]);
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com", options.BaseHost); // 从共享节回填
|
||||
Assert.Equal("/sch-only", options.BasePath); // 独立节优先,不被覆盖
|
||||
Assert.Equal("100002", options.AppKey); // AppKey 回填
|
||||
Assert.Equal("secret", options.AppSecret);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DI_注册后可解析门面且地址来自配置()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
["yh:open:SchClassPath"] = "/bschclass",
|
||||
});
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSchClassOpenApi(config);
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var options = provider.GetRequiredService<IOptions<SchClassOpenApiOptions>>().Value;
|
||||
Assert.Equal("http://base.yunhaixingtong.com/bschclass", options.ResolveBaseUrl());
|
||||
|
||||
var facade = provider.GetRequiredService<SchClassOpenApi>();
|
||||
Assert.NotNull(facade.SchInfo);
|
||||
Assert.NotNull(facade.StuInfo);
|
||||
Assert.Equal(new Uri("http://base.yunhaixingtong.com/bschclass"), facade.HttpBaseAddress);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DI_独立节配置整节生效()
|
||||
{
|
||||
var config = BuildConfig(new Dictionary<string, string?>
|
||||
{
|
||||
["yh:schclass:open:BaseHost"] = "http://sch.example.com",
|
||||
["yh:schclass:open:BasePath"] = "/sch",
|
||||
["yh:schclass:open:ReadTimeoutMs"] = "3000",
|
||||
["yh:open:BaseHost"] = "http://base.yunhaixingtong.com",
|
||||
});
|
||||
|
||||
var services = new ServiceCollection();
|
||||
services.AddSchClassOpenApi(config);
|
||||
using var provider = services.BuildServiceProvider();
|
||||
|
||||
var options = provider.GetRequiredService<IOptions<SchClassOpenApiOptions>>().Value;
|
||||
Assert.Equal("http://sch.example.com", options.BaseHost);
|
||||
Assert.Equal("/sch", options.BasePath);
|
||||
Assert.Equal(3000, options.ReadTimeoutMs);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>聚合门面 <see cref="SchClassOpenApi"/> 与工厂 <see cref="SchClassOpenApiFactory"/> 的装配行为。</summary>
|
||||
public class SchClassOpenApiFacadeTests
|
||||
{
|
||||
[Fact]
|
||||
public void 门面装配全部10个子客户端()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions { BaseHost = "http://base.yunhaixingtong.com/bschclass/oapi/" };
|
||||
using var http = new HttpClient();
|
||||
|
||||
var api = new SchClassOpenApi(http, options);
|
||||
|
||||
Assert.NotNull(api.DbClass);
|
||||
Assert.NotNull(api.VClass);
|
||||
Assert.NotNull(api.Schooling);
|
||||
Assert.NotNull(api.UserInfo);
|
||||
Assert.NotNull(api.SchInfo);
|
||||
Assert.NotNull(api.TeaInfo);
|
||||
Assert.NotNull(api.StuInfo);
|
||||
Assert.NotNull(api.TeachInfo);
|
||||
Assert.NotNull(api.SubInfo);
|
||||
Assert.NotNull(api.FamilyCompat);
|
||||
Assert.Same(options, api.Options);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 门面构造_httpClient为空时抛()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(
|
||||
() => new SchClassOpenApi(null!, new SchClassOpenApiOptions()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 门面构造_options为空时抛()
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => new SchClassOpenApi(http, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 子客户端构造_参数为空时抛()
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
var options = new SchClassOpenApiOptions();
|
||||
|
||||
Assert.Throws<ArgumentNullException>(() => new SchClassVClassClient(null!, options));
|
||||
Assert.Throws<ArgumentNullException>(() => new SchClassVClassClient(http, null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 工厂_按配置创建HttpClient()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions
|
||||
{
|
||||
BaseHost = "http://base.yunhaixingtong.com/",
|
||||
BasePath = "/bschclass",
|
||||
ReadTimeoutMs = 3000,
|
||||
};
|
||||
|
||||
using var http = SchClassOpenApiFactory.CreateHttpClient(options);
|
||||
|
||||
Assert.Equal(new Uri("http://base.yunhaixingtong.com/bschclass"), http.BaseAddress);
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(3000), http.Timeout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 工厂_读超时非法时回落5秒()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions { BaseHost = "http://base.yunhaixingtong.com", ReadTimeoutMs = 0 };
|
||||
|
||||
using var http = SchClassOpenApiFactory.CreateHttpClient(options);
|
||||
|
||||
Assert.Equal(TimeSpan.FromMilliseconds(5000), http.Timeout);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 工厂_创建完整门面()
|
||||
{
|
||||
var facade = SchClassOpenApiFactory.Create(new SchClassOpenApiOptions { BaseHost = "http://base.yunhaixingtong.com" });
|
||||
|
||||
Assert.NotNull(facade);
|
||||
Assert.NotNull(facade.VClass);
|
||||
Assert.NotNull(facade.StuInfo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 工厂_配置为空时抛()
|
||||
{
|
||||
Assert.Throws<ArgumentNullException>(() => SchClassOpenApiFactory.CreateHttpClient(null!));
|
||||
Assert.Throws<ArgumentNullException>(() => SchClassOpenApiFactory.Create(null!));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
public class SchClassOpenApiOptionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", null)]
|
||||
[InlineData(" ", null)]
|
||||
[InlineData("/base.yunhaixingtong.com", "/base.yunhaixingtong.com")]
|
||||
[InlineData("/base.yunhaixingtong.com/", "/base.yunhaixingtong.com")]
|
||||
[InlineData("base.yunhaixingtong.com", "/base.yunhaixingtong.com")]
|
||||
[InlineData(" /base.yunhaixingtong.com/ ", "/base.yunhaixingtong.com")]
|
||||
public void NormalizeBasePath_归一化规则(string? input, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, SchClassOpenApiOptions.NormalizeBasePath(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveBaseUrl_拼接host与path并去尾斜杠()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions
|
||||
{
|
||||
BaseHost = "http://base.yunhaixingtong.com/",
|
||||
BasePath = "/base.yunhaixingtong.com/",
|
||||
};
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com/base.yunhaixingtong.com", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveBaseUrl_path为空时挂根路径()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions { BaseHost = "http://base.yunhaixingtong.com", BasePath = "" };
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveBaseUrl_host为空时返回空串()
|
||||
{
|
||||
Assert.Equal("", new SchClassOpenApiOptions().ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 默认值_对齐Java客户端()
|
||||
{
|
||||
var options = new SchClassOpenApiOptions();
|
||||
|
||||
Assert.True(options.Enabled);
|
||||
Assert.Null(options.BaseHost);
|
||||
Assert.Null(options.BasePath);
|
||||
Assert.Equal(1500, options.ConnectTimeoutMs);
|
||||
Assert.Equal(5000, options.ReadTimeoutMs);
|
||||
Assert.Equal(5000, options.WriteTimeoutMs);
|
||||
Assert.Null(options.AppKey);
|
||||
Assert.Null(options.AppSecret);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 配置节名_独立于用户中心()
|
||||
{
|
||||
Assert.Equal("yh:schclass:open", SchClassOpenApiOptions.SectionName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>其余客户端(学制 / 科目 / 任课 / 学校教师 / 家长兼容)的端点契约对拍。</summary>
|
||||
public class SchClassOtherClientsTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Schoolen_学制与年级_5个端点()
|
||||
{
|
||||
const string root = "/oapi/Schoolen/";
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.Schooling.GetSchLenLevelListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchLenLevellist", "?SchSerID=S1");
|
||||
|
||||
await api.Schooling.GetLevelIdListAsync(3);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetLevelIdlist", "?Schooling=3");
|
||||
|
||||
await api.Schooling.GetSchLenLevelFirstAsync("11");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchLenLevelFirst", "?GradeId=11");
|
||||
|
||||
await api.Schooling.GetSchLenListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchLenlist", "?SchSerID=S1");
|
||||
|
||||
await api.Schooling.GetAllClassSubjectAsync(2, "S1", "2026-09-01");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetAllClassSubject", "?schId=2&schserid=S1&startTime=2026-09-01");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SubInfo_科目_4个端点()
|
||||
{
|
||||
const string root = "/oapi/SubInfo/";
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.SubInfo.GetSchSubListAsync("S1", 3, 11);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchSublist", "?SchID=S1&schooling=3&gradeId=11");
|
||||
|
||||
await api.SubInfo.GetSchIdsSubListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchIdsSublist", "?SchIds=S1,S2");
|
||||
|
||||
await api.SubInfo.GetSchTeaSubListAsync("77");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchTeaSublist", "?UserTeaSerID=77");
|
||||
|
||||
await api.SubInfo.GetSchTeachSubListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchTeachSublist", "?SchID=S1");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TeachInfo_任课_8个端点()
|
||||
{
|
||||
const string root = "/oapi/TeachInfo/";
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.TeachInfo.GetSchTeachListAsync("S1", 77);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchTeachlist", "?SchID=S1&UserTeaSerID=77");
|
||||
|
||||
await api.TeachInfo.GetEleSchTeachListAsync("S1", 77);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetEleSchTeachlist", "?SchID=S1&UserTeaSerID=77");
|
||||
|
||||
await api.TeachInfo.GetSchArrTeachListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchArrTeachlist", "?SchIDArr=S1,S2");
|
||||
|
||||
await api.TeachInfo.GetEleSchArrTeachListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetEleSchArrTeachList", "?SchIDArr=S1,S2");
|
||||
|
||||
await api.TeachInfo.GetSchClaTeachListAsync("1001");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetSchClaTeachlist", "?ClassID=1001");
|
||||
|
||||
await api.TeachInfo.GetEleSchClaTeachListAsync("1001");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetEleSchClaTeachlist", "?ClassID=1001");
|
||||
|
||||
await api.TeachInfo.GetElectiveClassByGradeAsync(1, 2);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, root + "GetElectiveClassByGrade", "?schoolId=1&gradeId=2");
|
||||
|
||||
await api.TeachInfo.GetElectiveClassByTeachersAsync("77,78");
|
||||
SchClassTestHost.VerifyBody(handler, root + "GetElectiveClassByTeachers", "77,78");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DbClass_学校教师名单_1个端点()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
|
||||
await SchClassTestHost.Api(handler).DbClass.GetSchteaListAsync(30);
|
||||
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, "/oapi/DbClass/GetSchteaList", "?Group=30");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FamCom_家长兼容_2个端点均为POST文本体()
|
||||
{
|
||||
const string root = "/oapi/Common/FamCom/";
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.FamilyCompat.GetStuUserFamIdListAsync("1,2");
|
||||
SchClassTestHost.VerifyBody(handler, root + "GetStuUserFamIDList", "1,2");
|
||||
|
||||
await api.FamilyCompat.GetAllStuFamMemAsync("1,2");
|
||||
SchClassTestHost.VerifyBody(handler, root + "GetAllStuFamMem", "1,2");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 响应解析与状态码归一(教务 OA 兼容端点约定):
|
||||
/// 成功返回裸数据;404 / 204 表示"无数据"归一为 null / 空列表;400 及其它非 2xx 抛异常。
|
||||
/// </summary>
|
||||
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<SchClassOpenApiException>(() => 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<SchClassOpenApiException>(() => 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<SchClassOpenApiException>(() => 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, "<html>502 Bad Gateway</html>");
|
||||
|
||||
var ex = await Assert.ThrowsAsync<SchClassOpenApiException>(() => 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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>SchInfo(学校)端点契约与 <see cref="SchInfoVo"/> 反序列化。</summary>
|
||||
public class SchClassSchInfoClientTests
|
||||
{
|
||||
private const string Root = "/oapi/SchInfo/";
|
||||
|
||||
[Fact]
|
||||
public async Task SchInfo_全部7个端点_路径与参数名对齐()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.SchInfo.GetSchListAsync("1,2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchlist", "?SchIDArr=1,2");
|
||||
|
||||
await api.SchInfo.AllListAsync();
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "AllList");
|
||||
|
||||
await api.SchInfo.GetSchFirstAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchFirst", "?SchID=S1");
|
||||
|
||||
await api.SchInfo.GetUserSchFirstAsync(9);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetUserSchFirst", "?UserSchID=9");
|
||||
|
||||
await api.SchInfo.GetUserSchListAsync("9,10");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetUserSchlist", "?UserSchIds=9,10");
|
||||
|
||||
await api.SchInfo.GetSchoolByGroupIdAsync(3);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchoolByGroupId", "?groupId=3");
|
||||
|
||||
await api.SchInfo.GetSchoolBySchoolenIdAsync(4);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchoolBySchoolenId", "?groupId=4");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SchInfoVO_按JsonProperty锚定的键名反序列化()
|
||||
{
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, """
|
||||
[{"SchSerId":"S001","UserSchSerId":9,"SchName":"第一中学"},
|
||||
{"SchSerId":"S002","UserSchSerId":10,"SchName":"第二中学"}]
|
||||
""");
|
||||
|
||||
var list = await SchClassTestHost.Api(handler).SchInfo.AllListAsync();
|
||||
|
||||
Assert.Equal(2, list.Count);
|
||||
Assert.Equal("S001", list[0].SchSerId);
|
||||
Assert.Equal(9, list[0].UserSchSerId);
|
||||
Assert.Equal("第二中学", list[1].SchName);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSchFirst_未找到时返回null()
|
||||
{
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, "null");
|
||||
|
||||
Assert.Null(await SchClassTestHost.Api(handler).SchInfo.GetSchFirstAsync("nope"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// OAStu(学生)端点的请求契约对拍。注意本控制器参数名<b>几乎全小写</b>
|
||||
/// (<c>schserid</c> / <c>schgroupid</c> / <c>stuids</c> / <c>schserids</c>),
|
||||
/// 与 VSchClass 的 <c>SchSerID</c> 形成对照,不可混用。
|
||||
/// </summary>
|
||||
public class SchClassStuInfoClientTests
|
||||
{
|
||||
private const string Root = "/oapi/OAStu/";
|
||||
|
||||
[Fact]
|
||||
public async Task OAStu_全部19个端点_路径与参数名对齐()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.StuInfo.GetAllStuListAsync();
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetAllStulist");
|
||||
|
||||
await api.StuInfo.GetEleAllStuListAsync();
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleAllStulist");
|
||||
|
||||
await api.StuInfo.GetSchStuListAsync("S1", "10");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchStulist", "?schserid=S1&schgroupid=10");
|
||||
|
||||
await api.StuInfo.GetEleSchStuListAsync("S1", "10");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleSchStulist", "?schserid=S1&schgroupid=10");
|
||||
|
||||
await api.StuInfo.GetClaStuListAsync("1001", "20");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetClaStulist", "?ClassID=1001&clagroupid=20");
|
||||
|
||||
await api.StuInfo.GetEleClaStuListAsync("1001", "20");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleClaStulist", "?ClassID=1001&clagroupid=20");
|
||||
|
||||
await api.StuInfo.GetClaArrStuListAsync("1001,1002", "20,21");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetClaArrStulist", "?ClassIDs=1001,1002&clagroupids=20,21");
|
||||
|
||||
await api.StuInfo.GetEleClaArrStuListAsync("1001,1002", "20,21");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleClaArrStulist", "?ClassIDs=1001,1002&clagroupids=20,21");
|
||||
|
||||
await api.StuInfo.GetStuIdsListAsync("1,2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetStuIdslist", "?stuids=1,2");
|
||||
|
||||
await api.StuInfo.GetEleStuIdsListAsync("1,2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleStuIdslist", "?stuids=1,2");
|
||||
|
||||
await api.StuInfo.GetStuUserFamIdListAsync("1,2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetStuUserFamIDList", "?stuids=1,2");
|
||||
|
||||
await api.StuInfo.GetFamStuListAsync("IC1,IC2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetFamStulist", "?invitecodes=IC1,IC2");
|
||||
|
||||
await api.StuInfo.GetStuFamMemAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetStuFamMem", "?SchSerID=S1");
|
||||
|
||||
await api.StuInfo.GetSchBySchIdsAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchBySchIds", "?schserids=S1,S2");
|
||||
|
||||
await api.StuInfo.GetEleSchIdsStuListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleSchIdsStulist", "?schserids=S1,S2");
|
||||
|
||||
await api.StuInfo.GetEleSchBySchIdsAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleSchBySchIds", "?schserids=S1,S2");
|
||||
|
||||
await api.StuInfo.GetStudentsByElectiveIdAsync(5);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetStudentsByElectiveId", "?electiveIds=5");
|
||||
|
||||
await api.StuInfo.GetHistoryStudentsBySchoolIdAsync(1, 2026);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "getHistoryStudentsBySchoolId", "?schoolId=1&year=2026");
|
||||
|
||||
await api.StuInfo.GetHistoryClassAsync(1, 2026);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "getHistoryClass", "?schoolId=1&year=2026");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 学生行字段_按键名原样读取()
|
||||
{
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, """
|
||||
[{"Account":"2026001","UserStuSerID":11,"StuName":"小明","StuIDCard":"110101","StuSex":"男",
|
||||
"StuStatus":1,"ClassID":1001,"ClassStuID":2001,"GroupID":30,"StuPhoto":null,"CardID":"C1",
|
||||
"StuCurrentTel":"138","invitecode":"IC1","SchSerID":"S1","Graduated":0,"isjiedu":0}]
|
||||
""");
|
||||
|
||||
var rows = await SchClassTestHost.Api(handler).StuInfo.GetAllStuListAsync();
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("小明", row["StuName"].GetString());
|
||||
Assert.Equal(1001, row["ClassID"].GetInt32());
|
||||
Assert.Equal("S1", row["SchSerID"].GetString());
|
||||
Assert.Equal(JsonValueKind.Null, row["StuPhoto"].ValueKind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 历史学生行_使用小写字段名()
|
||||
{
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, """
|
||||
[{"stuId":11,"stuName":"小明","classId":1001,"className":"高一(1)班",
|
||||
"grade":1,"name":"第一中学","school_system_id":3,"system_grade":10}]
|
||||
""");
|
||||
|
||||
var rows = await SchClassTestHost.Api(handler).StuInfo.GetHistoryStudentsBySchoolIdAsync(1, 2026);
|
||||
|
||||
var row = Assert.Single(rows);
|
||||
Assert.Equal("小明", row["stuName"].GetString());
|
||||
Assert.Equal("高一(1)班", row["className"].GetString());
|
||||
Assert.Equal(3, row["school_system_id"].GetInt32());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>OATea(教师)端点契约与 <see cref="TeaInfoVo"/> 反序列化。</summary>
|
||||
public class SchClassTeaInfoClientTests
|
||||
{
|
||||
private const string Root = "/oapi/OATea/";
|
||||
|
||||
[Fact]
|
||||
public async Task OATea_全部4个端点_路径与参数名对齐()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.TeaInfo.GetTeaInfoAsync(77);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetTeainfo", "?uid=77");
|
||||
|
||||
await api.TeaInfo.GetArrTeaInfoAsync("77,78");
|
||||
SchClassTestHost.VerifyBody(handler, Root + "GetArrTeainfo", "77,78");
|
||||
|
||||
await api.TeaInfo.GetSchTeaInfoAsync("S1", 2);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchTeainfo", "?SchSerID=S1&schId=2");
|
||||
|
||||
await api.TeaInfo.GetSchTeaListAsync("S1", 2);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchTeaList", "?SchSerID=S1&schId=2");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TeaInfoVO_字段按Java原名反序列化()
|
||||
{
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, """
|
||||
{"SchSerID":"S001","Teapolitical":"党员","TeaWorkStart":"2010-09-01","Account":"wang",
|
||||
"SchSerIDs":"S001","role":3,"JiaoYuJu":"市教育局","TeaSex":"男","IsLeadingMiddle":0,
|
||||
"TeaOriginPlace":"北京","Photo":null,"TeaIDCard":"110101","Name":"王老师","GroupID":30,
|
||||
"SchName":"第一中学","TeaTeachStart":"2010-09-01","TeaSerID":"77","UserID":1001,
|
||||
"TeaNation":"汉","Tel":"13800000000","TeaWorkType":"专任","TeaNo":7,"TeaBirth":"1985-01-01"}
|
||||
""");
|
||||
|
||||
var tea = await SchClassTestHost.Api(handler).TeaInfo.GetTeaInfoAsync(77);
|
||||
|
||||
Assert.NotNull(tea);
|
||||
Assert.Equal("王老师", tea!.Name);
|
||||
Assert.Equal("S001", tea.SchSerId);
|
||||
// 注意:查询参数 uid 传的是教师 teaId(77),而 VO.UserID 是用户中心 ID(1001),两者不是一回事
|
||||
Assert.Equal("77", tea.TeaSerId);
|
||||
Assert.Equal(1001, tea.UserId);
|
||||
Assert.Equal(3, tea.Role);
|
||||
Assert.Equal(30, tea.GroupId);
|
||||
Assert.Equal("汉", tea.TeaNation);
|
||||
Assert.Null(tea.Photo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 教师不存在_服务端404时返回null()
|
||||
{
|
||||
var handler = FakeHandler.Empty(HttpStatusCode.NotFound);
|
||||
|
||||
Assert.Null(await SchClassTestHost.Api(handler).TeaInfo.GetTeaInfoAsync(999999));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetSchTeainfo_缺参数时服务端400抛异常()
|
||||
{
|
||||
var handler = FakeHandler.Text(HttpStatusCode.BadRequest, "schId 与 SchSerID 不能同时为空");
|
||||
|
||||
var ex = await Assert.ThrowsAsync<SchClassOpenApiException>(
|
||||
() => SchClassTestHost.Api(handler).TeaInfo.GetSchTeaInfoAsync());
|
||||
|
||||
Assert.Equal(400, ex.StatusCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>测试宿主:提供共享配置、门面实例与端点契约断言辅助。</summary>
|
||||
internal static class SchClassTestHost
|
||||
{
|
||||
/// <summary>测试用配置(不设 BasePath,端点直接挂在根路径下)。</summary>
|
||||
public static SchClassOpenApiOptions Options()
|
||||
=> new() { BaseHost = "http://base.yunhaixingtong.com" };
|
||||
|
||||
/// <summary>用伪造 handler 装配门面。</summary>
|
||||
public static SchClassOpenApi Api(FakeHandler handler)
|
||||
=> new(new HttpClient(handler), Options());
|
||||
|
||||
/// <summary>回放一段无害响应(JSON 字面量 null),使任意端点都能安全走完解析流程。</summary>
|
||||
public static FakeHandler Ok() => FakeHandler.Json(HttpStatusCode.OK, "null");
|
||||
|
||||
/// <summary>断言最近一次请求的 method / 路径 / query。</summary>
|
||||
public static void Verify(FakeHandler handler, HttpMethod method, string path, string query = "")
|
||||
{
|
||||
var request = handler.LastRequest;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(method, request!.Method);
|
||||
Assert.Equal(path, request.RequestUri!.AbsolutePath);
|
||||
Assert.Equal(query, Uri.UnescapeDataString(request.RequestUri.Query));
|
||||
}
|
||||
|
||||
/// <summary>断言最近一次请求的方法、路径与请求体(用于 POST <c>@RequestBody String</c> 端点)。</summary>
|
||||
public static void VerifyBody(FakeHandler handler, string path, string? body, string contentType = "text/plain")
|
||||
{
|
||||
var request = handler.LastRequest;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request!.Method);
|
||||
Assert.Equal(path, request.RequestUri!.AbsolutePath);
|
||||
Assert.Equal(body, handler.LastRequestBody);
|
||||
Assert.Equal(contentType, handler.LastContentType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>UserInfo(师生统一用户)端点契约与 <see cref="UserInfoVo"/> 反序列化。</summary>
|
||||
public class SchClassUserInfoClientTests
|
||||
{
|
||||
private const string Root = "/oapi/UserInfo/";
|
||||
|
||||
[Fact]
|
||||
public async Task UserInfo_全部2个端点_路径与参数名对齐()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.UserInfo.GetUserListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetUserList", "?SchSerID=S1");
|
||||
|
||||
await api.UserInfo.GetUserIdListAsync(1001);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "getUserIdList", "?userid=1001");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UserInfoVO_大小写不敏感匹配Jackson推导键名()
|
||||
{
|
||||
// Jackson 从 getter 推导的键名大小写与 Java 字段不完全一致(如 schSerID / idcard),
|
||||
// Web 默认大小写不敏感,故两种形态都能命中。
|
||||
var handler = FakeHandler.Json(HttpStatusCode.OK, """
|
||||
[{"schSerID":"S001","no":1,"idcard":"110101","userID":1001,"sex":1,"usertype":1,
|
||||
"serID":null,"photo":null,"tel":"13800000000","uids":"wang","groupID":30,"name":"王老师"}]
|
||||
""");
|
||||
|
||||
var list = await SchClassTestHost.Api(handler).UserInfo.GetUserListAsync("S001");
|
||||
|
||||
var user = Assert.Single(list);
|
||||
Assert.Equal("S001", user.SchSerId);
|
||||
Assert.Equal("110101", user.IdCard);
|
||||
Assert.Equal(1001, user.UserId);
|
||||
Assert.Equal(30, user.GroupId);
|
||||
Assert.Equal("王老师", user.Name);
|
||||
Assert.Null(user.SerId);
|
||||
Assert.Null(user.Photo);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetUserList_服务端204无内容时返回空列表()
|
||||
{
|
||||
var handler = FakeHandler.Empty(HttpStatusCode.NoContent);
|
||||
|
||||
Assert.Empty(await SchClassTestHost.Api(handler).UserInfo.GetUserListAsync("S1"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task getUserIdList_未找到时服务端404返回空列表()
|
||||
{
|
||||
var handler = FakeHandler.Empty(HttpStatusCode.NotFound);
|
||||
|
||||
Assert.Empty(await SchClassTestHost.Api(handler).UserInfo.GetUserIdListAsync(-1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
namespace Yh.SchClass.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// VSchClass(班级)端点的请求契约对拍:路径、HTTP 方法、query 参数名必须与 Java
|
||||
/// <c>VClassController</c> 逐字一致(参数名大小写敏感)。
|
||||
/// </summary>
|
||||
public class SchClassVClassClientTests
|
||||
{
|
||||
private const string Root = "/oapi/VSchClass/";
|
||||
|
||||
[Fact]
|
||||
public async Task VSchClass_全部17个端点_路径与参数名对齐()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.VClass.GetVClassListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetVClassList", "?SchSerID=S1");
|
||||
|
||||
await api.VClass.GetEleVClassListAsync("S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleVClassList", "?SchSerID=S1");
|
||||
|
||||
await api.VClass.GetSchArrVClassListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchArrVClassList", "?SchIDArr=S1,S2");
|
||||
|
||||
await api.VClass.GetEleSchArrVClassListAsync("S1,S2");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleSchArrVClassList", "?SchIDArr=S1,S2");
|
||||
|
||||
await api.VClass.GetFunTimeSchClassAsync("2026-09-01", "S1");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetFunTimeSchClass", "?time=2026-09-01&SchSerID=S1");
|
||||
|
||||
await api.VClass.GetVClassFirstAsync("1001");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetVClassFirst", "?ClassID=1001");
|
||||
|
||||
await api.VClass.GetEleVClassFirstAsync("1001");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleVClassFirst", "?ClassID=1001");
|
||||
|
||||
await api.VClass.GetVClassIdsListAsync("1001,1002");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetVClassIdsList", "?ClassIds=1001,1002");
|
||||
|
||||
await api.VClass.GetEleVClassIdsListAsync("1001,1002");
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleVClassIdsList", "?ClassIds=1001,1002");
|
||||
|
||||
await api.VClass.GetGradeClassListAsync("S1", 3, 11);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetGradeClassList", "?SchSerID=S1&Schooling=3&GradeId=11");
|
||||
|
||||
await api.VClass.GetTeachClassArrAsync(77);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetTeachClassArr", "?UserTeaSerID=77");
|
||||
|
||||
await api.VClass.GetSchArrVClassListAllAsync();
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetSchArrVClassListAll");
|
||||
|
||||
await api.VClass.GetEleSchArrVClassListAllAsync();
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetEleSchArrVClassListAll");
|
||||
|
||||
// POST @RequestBody String:按 text/plain 原样提交,不加引号
|
||||
await api.VClass.GetSchArrVClassListPostAsync("S1,S2");
|
||||
SchClassTestHost.VerifyBody(handler, Root + "GetSchArrVClassListPost", "S1,S2");
|
||||
|
||||
await api.VClass.GetElectiveClassByIdAsync(5);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetElectiveClassById", "?electiveId=5");
|
||||
|
||||
await api.VClass.GetElectiveClassByGradeAsync(1, 2);
|
||||
SchClassTestHost.Verify(handler, HttpMethod.Get, Root + "GetElectiveClassByGrade", "?schoolId=1&gradeId=2");
|
||||
|
||||
await api.VClass.GetElectiveClassByTeachersAsync("7,8");
|
||||
SchClassTestHost.VerifyBody(handler, Root + "GetElectiveClassByTeachers", "7,8");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task null参数_从query中丢弃()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = SchClassTestHost.Api(handler);
|
||||
|
||||
await api.VClass.GetGradeClassListAsync("S1", null, null);
|
||||
|
||||
Assert.Equal("?SchSerID=S1", Uri.UnescapeDataString(handler.LastRequest!.RequestUri!.Query));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 完整URL_由baseHost与oapi前缀拼成()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
|
||||
await SchClassTestHost.Api(handler).VClass.GetVClassListAsync("S1");
|
||||
|
||||
Assert.Equal("http://base.yunhaixingtong.com/oapi/VSchClass/GetVClassList?SchSerID=S1",
|
||||
handler.LastRequest!.RequestUri!.ToString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 配置了BasePath时_路径前缀正确拼接()
|
||||
{
|
||||
var handler = SchClassTestHost.Ok();
|
||||
var api = new SchClassOpenApi(new HttpClient(handler),
|
||||
new SchClassOpenApiOptions { BaseHost = "http://gw", BasePath = "/base.yunhaixingtong.com" });
|
||||
|
||||
await api.VClass.GetSchArrVClassListAllAsync();
|
||||
|
||||
Assert.Equal("/base.yunhaixingtong.com/oapi/VSchClass/GetSchArrVClassListAll",
|
||||
handler.LastRequest!.RequestUri!.AbsolutePath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
<!-- 调试入口:让测试工程可直接 dotnet run / F5,进入 DebugMain.Main 联调;
|
||||
GenerateProgramFile=false 关闭 Test SDK 自动生成的空 Main,避免 CS0017。
|
||||
dotnet test / vstest 不受影响,照常发现全部用例。 -->
|
||||
<OutputType>Exe</OutputType>
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Yh.SchClass.OpenApi\Yh.SchClass.OpenApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"yh": {
|
||||
"open": {
|
||||
"Enabled": true,
|
||||
"BaseHost": "http://base.yunhaixingtong.com",
|
||||
"BasePath": "/userbase",
|
||||
"SchClassPath": "/bschclass",
|
||||
"AppKey": "100002",
|
||||
"AppSecret": "2314a3b158b24a838b2b9b69fd3db118"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user