bag
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Yh.Platform.Abstractions;
|
||||
using Xunit;
|
||||
|
||||
namespace Yh.Platform.OpenApi.Tests;
|
||||
|
||||
/// <summary>
|
||||
/// 契约快照对拍(双端同步机制的核心防线):
|
||||
/// 基线 = UserBase springdoc 导出的权威快照 contract/openapi-oapi.exported.json
|
||||
/// (GET /userbase/v3/api-docs/oapi,UserBase 契约变更后重新导出更新该文件即可激活本测试发现漂移)。
|
||||
/// <list type="bullet">
|
||||
/// <item>端点对拍:SDK 契约路由表 ⊆ 快照(method+path 逐项),多出的快照端点以未覆盖清单输出;</item>
|
||||
/// <item>模型对拍:SDK POCO 属性集(camelCase)⇔ 快照 schema properties 键集,双向差额为零。</item>
|
||||
/// </list>
|
||||
/// 更新基线流程:UserBase 起服务 → GET /v3/api-docs/oapi 覆盖保存 exported.json → 跑本测试,红了就是契约漂移。
|
||||
/// </summary>
|
||||
public class ContractSnapshotTests
|
||||
{
|
||||
private static readonly string SnapshotPath =
|
||||
Path.Combine(AppContext.BaseDirectory, "contract", "openapi-oapi.exported.json");
|
||||
|
||||
private static JsonDocument LoadSnapshot()
|
||||
{
|
||||
Assert.True(File.Exists(SnapshotPath), $"权威契约快照缺失:{SnapshotPath}(从 UserBase GET /v3/api-docs/oapi 导出)");
|
||||
return JsonDocument.Parse(File.ReadAllText(SnapshotPath));
|
||||
}
|
||||
|
||||
/// <summary>PascalCase → camelCase(首字母小写,覆盖 Id→id、Idcard→idcard 等既有键形)。</summary>
|
||||
private static string ToCamel(string name)
|
||||
=> string.IsNullOrEmpty(name) ? name : char.ToLowerInvariant(name[0]) + name.Substring(1);
|
||||
|
||||
private static HashSet<string> PocoPropertyNames(Type type)
|
||||
=> type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Select(p => ToCamel(p.Name))
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
private static HashSet<string> SchemaPropertyNames(JsonElement schemas, string schemaName)
|
||||
{
|
||||
Assert.True(schemas.TryGetProperty(schemaName, out var schema),
|
||||
$"快照缺少 schema:{schemaName}(UserBase 侧可能已改名/删除)");
|
||||
return schema.GetProperty("properties").EnumerateObject()
|
||||
.Select(p => p.Name)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
// ───────────────────────── 端点对拍 ─────────────────────────
|
||||
|
||||
/// <summary>SDK 覆盖的契约路由表(method + path;来源:三个 Client 实现,加端点必须同步登记于此)。</summary>
|
||||
public static readonly (string Method, string Path)[] SdkRoutes =
|
||||
{
|
||||
// user 域
|
||||
("GET", "/oapi/user/byId"),
|
||||
("GET", "/oapi/user/byLoginName"),
|
||||
("GET", "/oapi/user/byPhone"),
|
||||
("GET", "/oapi/user/modules"),
|
||||
("GET", "/oapi/user/roles"),
|
||||
("GET", "/oapi/user/groupState"),
|
||||
("POST", "/oapi/user/add"),
|
||||
("POST", "/oapi/user/edit/{id}"),
|
||||
("POST", "/oapi/user/delete/{id}/{groupId}/{schoolId}"),
|
||||
("POST", "/oapi/user/deleteUsers"),
|
||||
("POST", "/oapi/user/batchAdd"),
|
||||
("POST", "/oapi/user/moveToGroup"),
|
||||
("POST", "/oapi/user/password"),
|
||||
("POST", "/oapi/user/groupState"),
|
||||
("POST", "/oapi/user/transferToSchool"),
|
||||
("POST", "/oapi/user/changeStatus"),
|
||||
("POST", "/oapi/user/batchChangeStatus"),
|
||||
// identity 域
|
||||
("GET", "/oapi/identity/current"),
|
||||
("GET", "/oapi/identity/available"),
|
||||
("POST", "/oapi/identity/switch"),
|
||||
// group 域
|
||||
("GET", "/oapi/group/tree"),
|
||||
("GET", "/oapi/group/byId"),
|
||||
("GET", "/oapi/group/list"),
|
||||
("POST", "/oapi/group/add"),
|
||||
("POST", "/oapi/group/edit/{id}"),
|
||||
("POST", "/oapi/group/delete/{id}/{schoolId}"),
|
||||
("POST", "/oapi/group/batchAdd"),
|
||||
("POST", "/oapi/group/move/{schoolId}"),
|
||||
// app 域
|
||||
("GET", "/oapi/app/{appId}"),
|
||||
("GET", "/oapi/app/list"),
|
||||
};
|
||||
|
||||
private static HashSet<(string, string)> SnapshotRoutes(JsonDocument snapshot)
|
||||
{
|
||||
var routes = new HashSet<(string, string)>();
|
||||
foreach (var path in snapshot.RootElement.GetProperty("paths").EnumerateObject())
|
||||
{
|
||||
foreach (var op in path.Value.EnumerateObject())
|
||||
{
|
||||
if (op.Name is "get" or "post" or "put" or "delete" or "patch")
|
||||
{
|
||||
routes.Add((op.Name.ToUpperInvariant(), path.Name));
|
||||
}
|
||||
}
|
||||
}
|
||||
return routes;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SDK端点必须在快照中存在()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var snapshotRoutes = SnapshotRoutes(snapshot);
|
||||
|
||||
var missing = SdkRoutes
|
||||
.Where(r => !snapshotRoutes.Contains((r.Method, r.Path)))
|
||||
.Select(r => $"{r.Method} {r.Path}")
|
||||
.ToList();
|
||||
Assert.True(missing.Count == 0,
|
||||
$"以下 SDK 端点在权威快照中不存在(UserBase 侧已删除或改路径,须同步修正 SDK):\n{string.Join('\n', missing)}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 快照端点若SDK未覆盖须显式登记()
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var snapshotRoutes = SnapshotRoutes(snapshot);
|
||||
var sdk = SdkRoutes.ToHashSet();
|
||||
|
||||
var uncovered = snapshotRoutes
|
||||
.Where(r => !sdk.Contains(r))
|
||||
.Select(r => $"{r.Item1} {r.Item2}")
|
||||
.OrderBy(x => x)
|
||||
.ToList();
|
||||
|
||||
// 不直接失败(UserBase 新增端点不应阻塞 SDK CI),但必须显式白名单化——未登记的未覆盖端点视为漂移信号
|
||||
var acknowledged = new HashSet<string>(StringComparer.Ordinal)
|
||||
{
|
||||
// 当前无未覆盖端点;UserBase 新增 /oapi 端点后,此清单须显式登记并排期跟进,防止无声失联
|
||||
};
|
||||
|
||||
var unacknowledged = uncovered.Where(u => !acknowledged.Contains(u)).ToList();
|
||||
Assert.True(unacknowledged.Count == 0,
|
||||
$"快照存在 SDK 未覆盖且未登记白名单的端点(评估跟进或显式放弃):\n{string.Join('\n', unacknowledged)}");
|
||||
}
|
||||
|
||||
// ───────────────────────── 模型对拍 ─────────────────────────
|
||||
|
||||
/// <summary>SDK 模型 ↔ 快照 schema 映射表(新模型对齐必须登记于此)。</summary>
|
||||
public static readonly (Type Type, string Schema)[] ModelMappings =
|
||||
{
|
||||
(typeof(BoaUser), "BoaUser"),
|
||||
(typeof(BoaRoleInfo), "BoaRoleInfo"),
|
||||
(typeof(BoaGroup), "BoaGroup"),
|
||||
(typeof(BoaModule), "BoaModule"),
|
||||
(typeof(BoaApp), "BoaApp"),
|
||||
(typeof(BoaAppType), "BoaAppType"),
|
||||
(typeof(SysUserRequest), "SysUserRequest"),
|
||||
(typeof(SysUserGroupDeleteItem), "SysUserGroupDeleteItem"),
|
||||
(typeof(SysUserMoveRequest), "SysUserMoveRequest"),
|
||||
(typeof(SysUserGroupStateRequest), "SysUserGroupStateRequest"),
|
||||
(typeof(SysUserTransferSchoolRequest), "SysUserTransferSchoolRequest"),
|
||||
(typeof(SysGroupRequest), "SysGroupRequest"),
|
||||
(typeof(SysGroupMoveRequest), "SysGroupMoveRequest"),
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(ModelCases))]
|
||||
public void 模型字段集与快照双向一致(Type type, string schemaName)
|
||||
{
|
||||
var snapshot = LoadSnapshot();
|
||||
var schemas = snapshot.RootElement.GetProperty("components").GetProperty("schemas");
|
||||
|
||||
var poco = PocoPropertyNames(type);
|
||||
var server = SchemaPropertyNames(schemas, schemaName);
|
||||
|
||||
var sdkOnly = poco.Except(server).ToList();
|
||||
var serverOnly = server.Except(poco).ToList();
|
||||
|
||||
Assert.True(sdkOnly.Count == 0,
|
||||
$"{type.Name} 存在快照没有的属性(服务端收不到,序列化键漂移):[{string.Join(", ", sdkOnly)}]");
|
||||
Assert.True(serverOnly.Count == 0,
|
||||
$"{type.Name} 缺少快照属性(服务端字段被丢弃):[{string.Join(", ", serverOnly)}]");
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> ModelCases()
|
||||
=> ModelMappings.Select(m => new object[] { m.Type, m.Schema });
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
global using System;
|
||||
global using System.Collections.Generic;
|
||||
global using System.Net;
|
||||
global using System.Net.Http;
|
||||
global using System.Text;
|
||||
global using System.Threading;
|
||||
global using System.Threading.Tasks;
|
||||
global using Yh.Platform.Abstractions;
|
||||
global using Yh.Platform.OpenApi;
|
||||
@@ -0,0 +1,25 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0</TargetFrameworks>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Newtonsoft.Json" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Yh.Platform.OpenApi\Yh.Platform.OpenApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\contract\fixtures\**\*.*" CopyToOutputDirectory="PreserveNewest" LinkBase="fixtures" />
|
||||
<!-- UserBase springdoc /v3/api-docs/oapi 导出的权威契约快照(双端同步对拍基线) -->
|
||||
<Content Include="..\..\contract\openapi-oapi.exported.json" CopyToOutputDirectory="PreserveNewest" Link="contract\openapi-oapi.exported.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,117 @@
|
||||
using Yh.Platform.Abstractions;
|
||||
using Yh.Platform.OpenApi;
|
||||
using Xunit;
|
||||
|
||||
namespace Yh.Platform.OpenApi.Tests;
|
||||
|
||||
public class OpenApiOptionsTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(null, null)]
|
||||
[InlineData("", null)]
|
||||
[InlineData(" ", null)]
|
||||
[InlineData("/userbase", "/userbase")]
|
||||
[InlineData("/userbase/", "/userbase")]
|
||||
[InlineData("userbase", "/userbase")]
|
||||
public void NormalizeBasePath_归一化规则(string? input, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, OpenApiOptions.NormalizeBasePath(input));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveBaseUrl_拼接host与path并去尾斜杠()
|
||||
{
|
||||
var options = new OpenApiOptions { BaseHost = "http://base.bestsch.com/", BasePath = "/userbase/" };
|
||||
Assert.Equal("http://base.bestsch.com/userbase", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveBaseUrl_path为空时挂根路径()
|
||||
{
|
||||
var options = new OpenApiOptions { BaseHost = "http://base.bestsch.com", BasePath = "" };
|
||||
Assert.Equal("http://base.bestsch.com", options.ResolveBaseUrl());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void 默认值_对齐Java客户端()
|
||||
{
|
||||
var options = new OpenApiOptions();
|
||||
Assert.True(options.Enabled);
|
||||
Assert.Equal("/userbase", options.BasePath);
|
||||
Assert.Equal(1500, options.ConnectTimeoutMs);
|
||||
Assert.Equal(5000, options.ReadTimeoutMs);
|
||||
Assert.Equal(5000, options.WriteTimeoutMs);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>静态门面 <see cref="YschBaseOpenApi"/> 的初始化准入与委托行为。</summary>
|
||||
public class YschBaseOpenApiTests : IDisposable
|
||||
{
|
||||
private sealed class StubUserClient : IOpenApiUserClient
|
||||
{
|
||||
public int Calls;
|
||||
public Task<BoaUser?> GetUserInfoAsync(int userId, CancellationToken ct = default)
|
||||
{
|
||||
Calls++;
|
||||
return Task.FromResult<BoaUser?>(new BoaUser(userId, "stub", "桩用户"));
|
||||
}
|
||||
|
||||
public Task<BoaUser?> GetUserByIdAsync(string userId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> GetUserByLoginNameAsync(string loginName, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> GetUserByPhoneAsync(string phone, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> GetUserInfoAsync(string loginName, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<List<BoaModule>> GetUserModuleAsync(int userId, int? schoolId = null, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> AddUserAsync(SysUserRequest request, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> EditUserAsync(int id, SysUserRequest request, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task DeleteUserAsync(int id, int groupId, int schoolId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task DeleteUsersAsync(IReadOnlyList<SysUserGroupDeleteItem> items, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<List<BoaUser>> BatchAddUsersAsync(IReadOnlyList<SysUserRequest> requests, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> MoveUserToGroupAsync(SysUserMoveRequest request, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<List<BoaRoleInfo>> GetUserRoleListAsync(int userId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaRoleInfo?> GetCurrentIdentityAsync(int userId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<List<BoaRoleInfo>> GetAvailableIdentitiesAsync(int userId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaRoleInfo?> SwitchIdentityAsync(int userId, int roleId, int schoolId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task UpdateUserPasswordAsync(int userId, string oldPassword, string newPassword, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task SetUserGroupStateAsync(SysUserGroupStateRequest request, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<int?> GetUserGroupStateAsync(int userId, int groupId, int schoolId, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task<BoaUser?> TransferUserToSchoolAsync(SysUserTransferSchoolRequest request, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task ChangeUserStatusAsync(int userId, bool disabled, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
public Task BatchChangeUserStatusAsync(IReadOnlyList<int> userIds, bool disabled, CancellationToken ct = default) => throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public YschBaseOpenApiTests()
|
||||
{
|
||||
YschBaseOpenApi.SetUserClient(null);
|
||||
YschBaseOpenApi.SetGroupClient(null);
|
||||
YschBaseOpenApi.SetAppClient(null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
YschBaseOpenApi.SetUserClient(null);
|
||||
YschBaseOpenApi.SetGroupClient(null);
|
||||
YschBaseOpenApi.SetAppClient(null);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 未初始化时抛InvalidOperationException并给出引导信息()
|
||||
{
|
||||
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => YschBaseOpenApi.GetUserInfoAsync(1));
|
||||
Assert.Contains("AddYhOpenApi", ex.Message);
|
||||
Assert.Contains("SetUserClient", ex.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task 初始化后门面委托到注入的client()
|
||||
{
|
||||
var stub = new StubUserClient();
|
||||
YschBaseOpenApi.SetUserClient(stub);
|
||||
|
||||
var user = await YschBaseOpenApi.GetUserInfoAsync(1001);
|
||||
|
||||
Assert.Equal(1, stub.Calls);
|
||||
Assert.NotNull(user);
|
||||
Assert.Equal(1001, user!.Id);
|
||||
Assert.Equal("stub", user.Account);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user