mirror of
https://gitee.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git
synced 2025-04-05 08:37:22 +08:00
test: 提取公共测试流程
This commit is contained in:
parent
3e491dc2f5
commit
0805414a42
@ -16,15 +16,19 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3", "src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj", "{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work", "src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj", "{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{C95AF531-CF44-44AA-AC90-F4DF9F941674}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{4C48D9D5-1D7F-4616-A05D-256555C310FC}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TestTools", "test\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj", "{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Api.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj", "{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests\SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj", "{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat.Work.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Work.UnitTests\SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj", "{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@ -44,6 +48,14 @@ Global
|
||||
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -52,10 +64,6 @@ Global
|
||||
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
@ -68,10 +76,12 @@ Global
|
||||
{63F7116F-320A-4CD8-9B84-2C675412F70F} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
|
||||
{082C1F69-7932-473F-A700-49584371BE8C} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
|
||||
{6FE502D4-C43D-49C9-9E57-D1EE566FD1C3} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
|
||||
{A8453835-4EE8-4FD4-9766-9C0DCB54CDB3} = {4C48D9D5-1D7F-4616-A05D-256555C310FC}
|
||||
{0C87A7D9-26EA-4821-AF3F-6D28B3006B24} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
|
||||
{5ECE2E7A-9AE8-49BF-902D-41A7756C3E78} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
|
||||
{CDD123E6-2622-4368-BAEE-8B95F05F1AB2} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3}
|
||||
{DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
|
||||
{4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39}
|
||||
|
@ -33,6 +33,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Api\SKIT.FlurlHttpClient.Wechat.Api.csproj" />
|
||||
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
|
||||
{
|
||||
public class WechatApiDeclarationTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Api");
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型命名")]
|
||||
public void ApiModelsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型定义")]
|
||||
public void ApiModelsDefinitionTest()
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 事件定义")]
|
||||
public void ApiEventsDefinitionTest()
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, "EventSamples");
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
TestAssertUtil.VerifyApiEventsDefinition(_assembly, workdir, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 接口命名")]
|
||||
public void ApiExtensionsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,347 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Api.UnitTests
|
||||
{
|
||||
public class WechatApiDefinitionTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Api");
|
||||
|
||||
[Fact(DisplayName = "验证模型定义")]
|
||||
public void ModelDefinitionsTest()
|
||||
{
|
||||
static void TrySetPropertiesValueRecursively(object obj)
|
||||
{
|
||||
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var tProperty in lstProperty)
|
||||
{
|
||||
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
|
||||
continue;
|
||||
|
||||
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
|
||||
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
||||
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
|
||||
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
|
||||
|
||||
if (tProperty.PropertyType.IsPrimitive)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.IsArray)
|
||||
{
|
||||
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
|
||||
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
|
||||
propEl = Convert.ChangeType(propEl, tEl);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Array prop = Array.CreateInstance(tEl, 1);
|
||||
prop.SetValue(propEl, 0);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else if (tProperty.PropertyType == typeof(string))
|
||||
{
|
||||
tProperty.SetValue(obj, string.Empty);
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System" &&
|
||||
tProperty.PropertyType.Name.StartsWith("Nullable"))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
|
||||
{
|
||||
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
|
||||
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
|
||||
propEl = Convert.ChangeType(propEl, tGeneric);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
|
||||
object prop = Activator.CreateInstance(tList);
|
||||
|
||||
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else
|
||||
{
|
||||
object prop = Activator.CreateInstance(tProperty.PropertyType);
|
||||
TrySetPropertiesValueRecursively(prop);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var lstModel = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<Type>();
|
||||
|
||||
foreach (Type tModel in lstModel)
|
||||
{
|
||||
// 模型命名结尾必为 Request 或 Response
|
||||
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatApiRequest、且有同名 Response
|
||||
if (tModel.Name.EndsWith("Request"))
|
||||
{
|
||||
if (!typeof(WechatApiRequest).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatApiResponse、且有同名 Request
|
||||
if (tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
if (!typeof(WechatApiResponse).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
|
||||
try
|
||||
{
|
||||
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
|
||||
TrySetPropertiesValueRecursively(instance);
|
||||
|
||||
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
|
||||
new FlurlSystemTextJsonSerializer().Serialize(instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证接口定义")]
|
||||
public void InterfaceDefinitionsTest()
|
||||
{
|
||||
var lstInterface = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name) &&
|
||||
e.Name.StartsWith("WechatApiClientExecute") &&
|
||||
e.Name.EndsWith("Extensions")
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<MethodInfo>();
|
||||
|
||||
foreach (Type tInterface in lstInterface)
|
||||
{
|
||||
var lstMethod = tInterface.GetMethods()
|
||||
.Where(e =>
|
||||
e.IsPublic &&
|
||||
e.IsStatic &&
|
||||
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatApiClient)
|
||||
)
|
||||
.ToList();
|
||||
foreach (MethodInfo tMethod in lstMethod)
|
||||
{
|
||||
var lstParam = tMethod.GetParameters();
|
||||
|
||||
// 参数签名必为 this client + request + cancelToken
|
||||
if (lstParam.Length != 3)
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二个参数必为 WechatApiRequest 子类
|
||||
if (!typeof(WechatApiRequest).IsAssignableFrom(lstParam[1].ParameterType))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 方法名与第二个参数、返回值均有相同命名
|
||||
string func = tMethod.Name;
|
||||
string para = lstParam[1].ParameterType.Name;
|
||||
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
|
||||
if (para == null || !para.EndsWith("Request"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (retv == null || !retv.EndsWith("Response"))
|
||||
{
|
||||
if (!tMethod.ReturnType.GenericTypeArguments.First().IsGenericType)
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证字段定义")]
|
||||
public void FieldDefinitionsTest()
|
||||
{
|
||||
var exceptions = new List<string>();
|
||||
|
||||
string[] GetAllFiles(string path)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
string[] dirs = Directory.GetDirectories(path);
|
||||
string[] files = Directory.GetFiles(path);
|
||||
|
||||
results.AddRange(files);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
results.AddRange(GetAllFiles(dir));
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
void VerifyJsonSamples(string subdir, string subns)
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
var lstFile = GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
Assert.NotEmpty(lstFile);
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
|
||||
settings.CheckAdditionalContent = true;
|
||||
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
|
||||
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
|
||||
|
||||
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
|
||||
else
|
||||
throw new Exception($"Deserialize `{name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VerifyXmlSamples(string subdir, string subns)
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
var lstFile = GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".xml", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
Assert.NotEmpty(lstFile);
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string xml = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using StringReader reader = new StringReader(xml);
|
||||
System.Xml.Serialization.XmlSerializer xmlSerializer = new System.Xml.Serialization.XmlSerializer(type, new System.Xml.Serialization.XmlRootAttribute("xml"));
|
||||
xmlSerializer.Deserialize(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
|
||||
if (ex is System.Xml.XmlException)
|
||||
throw new Exception($"Deserialize `{name}` by System.Xml failed.", ex);
|
||||
else
|
||||
throw new Exception($"Deserialize `{name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerifyJsonSamples("ModelSamples", "Models");
|
||||
VerifyJsonSamples("EventSamples", "Events");
|
||||
VerifyXmlSamples("EventSamples", "Events");
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
@ -30,6 +30,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.TenpayV3\SKIT.FlurlHttpClient.Wechat.TenpayV3.csproj" />
|
||||
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -7,7 +7,7 @@ using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
|
||||
{
|
||||
public class WechatTenpayConverterTests
|
||||
public class WechatConverterTests
|
||||
{
|
||||
class JsonDateTimeOffsetTestEntity
|
||||
{
|
@ -0,0 +1,66 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
|
||||
{
|
||||
public class WechatTenpayDeclarationTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV3");
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型命名")]
|
||||
public void ApiModelsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型定义")]
|
||||
public void ApiModelsDefinitionTest()
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 事件定义")]
|
||||
public void ApiEventsDefinitionTest()
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, "EventSamples");
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
TestAssertUtil.VerifyApiEventsDefinition(_assembly, workdir, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 接口命名")]
|
||||
public void ApiExtensionsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,303 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests
|
||||
{
|
||||
public class WechatTenpayDefinitionTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.TenpayV3");
|
||||
|
||||
[Fact(DisplayName = "验证模型定义")]
|
||||
public void ModelDefinitionsTest()
|
||||
{
|
||||
static void TrySetPropertiesValueRecursively(object obj)
|
||||
{
|
||||
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var tProperty in lstProperty)
|
||||
{
|
||||
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
|
||||
continue;
|
||||
|
||||
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
|
||||
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
||||
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
|
||||
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
|
||||
|
||||
if (tProperty.PropertyType.IsPrimitive)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.IsArray)
|
||||
{
|
||||
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
|
||||
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
|
||||
propEl = Convert.ChangeType(propEl, tEl);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Array prop = Array.CreateInstance(tEl, 1);
|
||||
prop.SetValue(propEl, 0);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else if (tProperty.PropertyType == typeof(string))
|
||||
{
|
||||
tProperty.SetValue(obj, string.Empty);
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System" &&
|
||||
tProperty.PropertyType.Name.StartsWith("Nullable"))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
|
||||
{
|
||||
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
|
||||
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
|
||||
propEl = Convert.ChangeType(propEl, tGeneric);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
|
||||
object prop = Activator.CreateInstance(tList);
|
||||
|
||||
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else
|
||||
{
|
||||
object prop = Activator.CreateInstance(tProperty.PropertyType);
|
||||
TrySetPropertiesValueRecursively(prop);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var lstModel = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<Type>();
|
||||
|
||||
foreach (Type tModel in lstModel)
|
||||
{
|
||||
// 模型命名结尾必为 Request 或 Response
|
||||
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatTenpayRequest、且有同名 Response
|
||||
if (tModel.Name.EndsWith("Request"))
|
||||
{
|
||||
if (!typeof(WechatTenpayRequest).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatTenpayResponse、且有同名 Request
|
||||
if (tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
if (!typeof(WechatTenpayResponse).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
|
||||
try
|
||||
{
|
||||
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
|
||||
TrySetPropertiesValueRecursively(instance);
|
||||
|
||||
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
|
||||
new FlurlSystemTextJsonSerializer().Serialize(instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证接口定义")]
|
||||
public void InterfaceDefinitionsTest()
|
||||
{
|
||||
var lstInterface = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name) &&
|
||||
e.Name.StartsWith("WechatTenpayClientExecute") &&
|
||||
e.Name.EndsWith("Extensions")
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<MethodInfo>();
|
||||
|
||||
foreach (Type tInterface in lstInterface)
|
||||
{
|
||||
var lstMethod = tInterface.GetMethods()
|
||||
.Where(e =>
|
||||
e.IsPublic &&
|
||||
e.IsStatic &&
|
||||
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatTenpayClient)
|
||||
)
|
||||
.ToList();
|
||||
foreach (MethodInfo tMethod in lstMethod)
|
||||
{
|
||||
var lstParam = tMethod.GetParameters();
|
||||
|
||||
// 参数签名必为 this client + request + cancelToken
|
||||
if (lstParam.Length != 3)
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二个参数必为 WechatTenpayRequest 子类
|
||||
if (!typeof(WechatTenpayRequest).IsAssignableFrom(lstParam[1].ParameterType))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 方法名与第二个参数、返回值均有相同命名
|
||||
string func = tMethod.Name;
|
||||
string para = lstParam[1].ParameterType.Name;
|
||||
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
|
||||
if (para == null || !para.EndsWith("Request"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (retv == null || !retv.EndsWith("Response"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证字段定义")]
|
||||
public void FieldDefinitionsTest()
|
||||
{
|
||||
var exceptions = new List<string>();
|
||||
|
||||
void VerifyJsonSamples(string subdir, string subns)
|
||||
{
|
||||
string[] GetFiles(string path)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
string[] dirs = Directory.GetDirectories(path);
|
||||
string[] files = Directory.GetFiles(path);
|
||||
|
||||
results.AddRange(files);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
results.AddRange(GetFiles(dir));
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
var lstFile = GetFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
Assert.NotEmpty(lstFile);
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
|
||||
settings.CheckAdditionalContent = true;
|
||||
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
|
||||
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
|
||||
|
||||
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
|
||||
else
|
||||
throw new Exception($"Deserialize `{name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerifyJsonSamples("ModelSamples", "Models");
|
||||
VerifyJsonSamples("EventSamples", "Events");
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp3.1</TargetFramework>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat\SKIT.FlurlHttpClient.Wechat.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
316
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs
Normal file
316
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs
Normal file
@ -0,0 +1,316 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Xml.Serialization;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestAssertUtil
|
||||
{
|
||||
private static bool TryJsonize(string json, Type type, out Exception exception)
|
||||
{
|
||||
exception = null;
|
||||
|
||||
var newtonsoftJsonSettings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
|
||||
newtonsoftJsonSettings.CheckAdditionalContent = true;
|
||||
newtonsoftJsonSettings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
|
||||
var newtonsoftJsonSerializer = new FlurlNewtonsoftJsonSerializer(newtonsoftJsonSettings);
|
||||
var systemTextJsonSerializer = new FlurlSystemTextJsonSerializer();
|
||||
|
||||
try
|
||||
{
|
||||
newtonsoftJsonSerializer.Deserialize(json, type);
|
||||
systemTextJsonSerializer.Deserialize(json, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
exception = new Exception($"通过 Newtonsoft.Json 反序列化 `{type.Name}` 失败。", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
exception = new Exception($"通过 System.Text.Json 反序列化 `{type.Name}` 失败。", ex);
|
||||
else
|
||||
exception = new Exception($"JSON 反序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object instance = Activator.CreateInstance(type);
|
||||
TestReflectionUtil.InitializeProperties(instance);
|
||||
|
||||
newtonsoftJsonSerializer.Serialize(instance, type);
|
||||
systemTextJsonSerializer.Serialize(instance, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
exception = new Exception($"通过 Newtonsoft.Json 序列化 `{type.Name}` 失败。", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
exception = new Exception($"通过 System.Text.Json 序列化 `{type.Name}` 失败。", ex);
|
||||
else
|
||||
exception = new Exception($"JSON 序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
|
||||
PropertyInfo[] lstPropInfo = TestReflectionUtil.GetAllProperties(type);
|
||||
foreach (PropertyInfo propInfo in lstPropInfo)
|
||||
{
|
||||
var newtonsoftJsonAttribute = propInfo.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
|
||||
var systemTextJsonAttribute = propInfo.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
||||
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
|
||||
exception = new Exception($"类型 `{type.Name}` 的可 JSON 序列化字段声明不一致:`{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`。");
|
||||
}
|
||||
|
||||
return exception == null;
|
||||
}
|
||||
|
||||
public static bool VerifyApiModelsNaming(Assembly assembly, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
foreach (Type modelType in lstModelType)
|
||||
{
|
||||
string name = modelType.Name.Split('`')[0];
|
||||
|
||||
if (!name.EndsWith("Request") && !name.EndsWith("Response"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类名结尾应为 \"Request\" 或 \"eponse\"。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (name.EndsWith("Request"))
|
||||
{
|
||||
if (!typeof(IWechatRequest).IsAssignableFrom(modelType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类需实现自 `IWechatRequest`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Request".Length)}Response"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 是请求模型,但不存在对应的响应模型。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (name.EndsWith("Response"))
|
||||
{
|
||||
if (!typeof(IWechatResponse).IsAssignableFrom(modelType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 类需实现自 `IWechatResponse`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModelType.Any(e => e.Name == $"{name.Substring(0, name.Length - "Response".Length)}Request"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{name}` 是响应模型,但不存在对应的请求模型。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiModelsDefinition(Assembly assembly, string workdir, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
var lstFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
if (!lstFile.Any())
|
||||
{
|
||||
lstError.Add(new Exception($"路径 \"{workdir}\" 下不存在 JSON 格式的模型示例文件,请检查路径是否正确。"));
|
||||
}
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Models.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryJsonize(json, type, out Exception ex))
|
||||
{
|
||||
lstError.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiEventsDefinition(Assembly assembly, string workdir, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
if (workdir == null) throw new ArgumentNullException(nameof(workdir));
|
||||
|
||||
var lstModelType = TestReflectionUtil.GetAllApiModelsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
var lstJsonFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
var lstXmlFile = TestIOUtil.GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".xml", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToArray();
|
||||
if (!lstJsonFile.Any() && !lstXmlFile.Any())
|
||||
{
|
||||
lstError.Add(new Exception($"路径 \"{workdir}\" 下不存在 JSON 或 XML 格式的事件示例文件,请检查路径是否正确。"));
|
||||
}
|
||||
|
||||
foreach (string file in lstJsonFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Events.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryJsonize(json, type, out Exception ex))
|
||||
{
|
||||
lstError.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string file in lstXmlFile)
|
||||
{
|
||||
string xml = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = assembly.GetType($"{assembly.GetName().Name}.Events.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
lstError.Add(new Exception($"类型 `{name}`不存在。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using StringReader reader = new StringReader(xml);
|
||||
XmlSerializer xmlSerializer = new XmlSerializer(type, new XmlRootAttribute("xml"));
|
||||
xmlSerializer.Deserialize(reader);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exception = new Exception($"XML 反序列化 `{type.Name}` 遇到问题。", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool VerifyApiExtensionsNaming(Assembly assembly, out Exception exception)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
var lstExtType = TestReflectionUtil.GetAllApiExtensionsTypes(assembly);
|
||||
var lstError = new List<Exception>();
|
||||
|
||||
foreach (Type extType in lstExtType)
|
||||
{
|
||||
MethodInfo[] lstMethod = extType.GetMethods()
|
||||
.Where(e =>
|
||||
e.IsPublic &&
|
||||
e.IsStatic &&
|
||||
typeof(IWechatClient).IsAssignableFrom(e.GetParameters().FirstOrDefault().ParameterType)
|
||||
)
|
||||
.ToArray();
|
||||
|
||||
foreach (MethodInfo methodInfo in lstMethod)
|
||||
{
|
||||
ParameterInfo[] lstParamInfo = methodInfo.GetParameters();
|
||||
|
||||
// 参数签名必为 this client + request + cancelToken
|
||||
if (lstParamInfo.Length != 3)
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法需有且仅有 3 个入参。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二个参数必为 IWechatRequest 子类
|
||||
if (!typeof(IWechatRequest).IsAssignableFrom(lstParamInfo[1].ParameterType))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法第 1 个入参需实现自 `IWechatRequest`。"));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 方法名与第二个参数、返回值均有相同命名
|
||||
string func = methodInfo.Name;
|
||||
string para = lstParamInfo[1].ParameterType.Name;
|
||||
string retv = methodInfo.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
|
||||
if (para == null || !para.EndsWith("Request"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法第 1 个入参类名应以 `Request` 结尾。"));
|
||||
continue;
|
||||
}
|
||||
else if (retv == null || !retv.EndsWith("Response"))
|
||||
{
|
||||
if (!methodInfo.ReturnType.GenericTypeArguments.First().IsGenericType)
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法返回值类名应以 `Response` 结尾。"));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法与请求模型应同名。"));
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
|
||||
{
|
||||
lstError.Add(new Exception($"`{extType.Name}.{methodInfo.Name}` 方法与响应模型应同名。"));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lstError.Any())
|
||||
{
|
||||
exception = new AggregateException(lstError);
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
27
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs
Normal file
27
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs
Normal file
@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestIOUtil
|
||||
{
|
||||
public static string[] GetAllFiles(string path)
|
||||
{
|
||||
if (path == null) throw new ArgumentNullException(nameof(path));
|
||||
|
||||
List<string> results = new List<string>();
|
||||
string[] dirs = Directory.GetDirectories(path);
|
||||
string[] files = Directory.GetFiles(path);
|
||||
|
||||
results.AddRange(files);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
results.AddRange(GetAllFiles(dir));
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
158
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs
Normal file
158
test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs
Normal file
@ -0,0 +1,158 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat
|
||||
{
|
||||
public static class TestReflectionUtil
|
||||
{
|
||||
public static Type[] GetAllApiModelsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name + ".Models") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetAllApiExtensionsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name) &&
|
||||
e.Name.StartsWith("Wechat") &&
|
||||
e.Name.Contains("ClientExecute") &&
|
||||
e.Name.EndsWith("Extensions")
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static Type[] GetAllApiEventsTypes(Assembly assembly)
|
||||
{
|
||||
if (assembly == null) throw new ArgumentNullException(nameof(assembly));
|
||||
|
||||
return assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(assembly.GetName().Name + ".Events") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static PropertyInfo[] GetAllProperties(Type type)
|
||||
{
|
||||
if (type == null) throw new ArgumentNullException(nameof(type));
|
||||
|
||||
var lstProperty = type.GetProperties(BindingFlags.Public | BindingFlags.Instance).ToList();
|
||||
|
||||
type.GetNestedTypes()
|
||||
.Where(e =>
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface
|
||||
)
|
||||
.ToList()
|
||||
.ForEach(e =>
|
||||
{
|
||||
lstProperty.AddRange(GetAllProperties(e));
|
||||
});
|
||||
|
||||
return lstProperty.Distinct().ToArray();
|
||||
}
|
||||
|
||||
public static object InitializeProperties(object obj)
|
||||
{
|
||||
const int MAX_DEPTH = 10; // 防止无限递归
|
||||
int CUR_DEPTH = 0;
|
||||
|
||||
Func<object, object> func = null;
|
||||
func = new Func<object, object>((obj) =>
|
||||
{
|
||||
CUR_DEPTH++;
|
||||
|
||||
if (CUR_DEPTH >= MAX_DEPTH)
|
||||
return obj;
|
||||
|
||||
PropertyInfo[] lstPropInfo = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (PropertyInfo propInfo in lstPropInfo)
|
||||
{
|
||||
if (propInfo.SetMethod == null || !propInfo.SetMethod.IsPublic)
|
||||
continue;
|
||||
|
||||
if (propInfo.PropertyType.IsPrimitive)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.IsArray)
|
||||
{
|
||||
Type elType = propInfo.PropertyType.Assembly.GetType(propInfo.PropertyType.FullName.Replace("[]", string.Empty));
|
||||
object elObj = (elType == typeof(string)) ? string.Empty : Activator.CreateInstance(elType);
|
||||
elObj = Convert.ChangeType(elObj, elType);
|
||||
func(elObj);
|
||||
|
||||
Array prop = Array.CreateInstance(elType, 1);
|
||||
prop.SetValue(elObj, 0);
|
||||
|
||||
propInfo.SetValue(obj, prop);
|
||||
}
|
||||
else if (propInfo.PropertyType == typeof(string))
|
||||
{
|
||||
propInfo.SetValue(obj, string.Empty);
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System" &&
|
||||
propInfo.PropertyType.Name.StartsWith("Nullable"))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(propInfo.PropertyType.Name.StartsWith("IDictionary") || propInfo.PropertyType.Name.StartsWith("Dictionary")))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (propInfo.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(propInfo.PropertyType.Name.StartsWith("IList") || propInfo.PropertyType.Name.StartsWith("List")))
|
||||
{
|
||||
Type elElementType = propInfo.PropertyType.GetGenericArguments().Single();
|
||||
object elElementObj = (elElementType == typeof(string)) ? string.Empty : Activator.CreateInstance(elElementType);
|
||||
elElementObj = Convert.ChangeType(elElementObj, elElementType);
|
||||
func(elElementObj);
|
||||
|
||||
Type elListType = typeof(List<>).MakeGenericType(new Type[] { elElementType });
|
||||
object elListObj = Activator.CreateInstance(elListType);
|
||||
elListType.GetMethod("Add").Invoke(elListObj, new[] { elElementObj });
|
||||
|
||||
propInfo.SetValue(obj, elListObj);
|
||||
}
|
||||
else
|
||||
{
|
||||
object elObj = Activator.CreateInstance(propInfo.PropertyType);
|
||||
func(elObj);
|
||||
|
||||
propInfo.SetValue(obj, elObj);
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
});
|
||||
|
||||
return func(obj);
|
||||
}
|
||||
}
|
||||
}
|
@ -27,6 +27,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SKIT.FlurlHttpClient.Wechat.Work\SKIT.FlurlHttpClient.Wechat.Work.csproj" />
|
||||
<ProjectReference Include="..\SKIT.FlurlHttpClient.Wechat.TestTools\SKIT.FlurlHttpClient.Wechat.TestTools.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
|
||||
{
|
||||
public class WechatWorkDeclarationTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Work");
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型命名")]
|
||||
public void ApiModelsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiModelsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 模型定义")]
|
||||
public void ApiModelsDefinitionTest()
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, "ModelSamples");
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
TestAssertUtil.VerifyApiModelsDefinition(_assembly, workdir, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证 API 接口命名")]
|
||||
public void ApiExtensionsNamingTest()
|
||||
{
|
||||
TestAssertUtil.VerifyApiExtensionsNaming(_assembly, out var ex);
|
||||
|
||||
if (ex != null)
|
||||
throw ex;
|
||||
|
||||
Assert.Null(ex);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,302 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace SKIT.FlurlHttpClient.Wechat.Work.UnitTests
|
||||
{
|
||||
public class WechatWorkDefinitionTests
|
||||
{
|
||||
private static readonly Assembly _assembly = Assembly.Load("SKIT.FlurlHttpClient.Wechat.Work");
|
||||
|
||||
[Fact(DisplayName = "验证模型定义")]
|
||||
public void ModelDefinitionsTest()
|
||||
{
|
||||
static void TrySetPropertiesValueRecursively(object obj)
|
||||
{
|
||||
var lstProperty = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
||||
foreach (var tProperty in lstProperty)
|
||||
{
|
||||
if (tProperty.SetMethod == null || !tProperty.SetMethod.IsPublic)
|
||||
continue;
|
||||
|
||||
var newtonsoftJsonAttribute = tProperty.GetCustomAttribute<Newtonsoft.Json.JsonPropertyAttribute>();
|
||||
var systemTextJsonAttribute = tProperty.GetCustomAttribute<System.Text.Json.Serialization.JsonPropertyNameAttribute>();
|
||||
if (newtonsoftJsonAttribute?.PropertyName != systemTextJsonAttribute?.Name)
|
||||
throw new Exception($"`{obj.GetType().Name}` fields mismatching: `{newtonsoftJsonAttribute.PropertyName}` & `{systemTextJsonAttribute.Name}`");
|
||||
|
||||
if (tProperty.PropertyType.IsPrimitive)
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.IsArray)
|
||||
{
|
||||
Type tEl = tProperty.PropertyType.Assembly.GetType(tProperty.PropertyType.FullName.Replace("[]", string.Empty));
|
||||
object propEl = (tEl == typeof(string)) ? string.Empty : Activator.CreateInstance(tEl);
|
||||
propEl = Convert.ChangeType(propEl, tEl);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Array prop = Array.CreateInstance(tEl, 1);
|
||||
prop.SetValue(propEl, 0);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else if (tProperty.PropertyType == typeof(string))
|
||||
{
|
||||
tProperty.SetValue(obj, string.Empty);
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System" &&
|
||||
tProperty.PropertyType.Name.StartsWith("Nullable"))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IDictionary") || tProperty.PropertyType.Name.StartsWith("Dictionary")))
|
||||
{
|
||||
// noop
|
||||
}
|
||||
else if (tProperty.PropertyType.Namespace == "System.Collections.Generic" &&
|
||||
(tProperty.PropertyType.Name.StartsWith("IList") || tProperty.PropertyType.Name.StartsWith("List")))
|
||||
{
|
||||
Type tGeneric = tProperty.PropertyType.GetGenericArguments().Single();
|
||||
object propEl = (tGeneric == typeof(string)) ? string.Empty : Activator.CreateInstance(tGeneric);
|
||||
propEl = Convert.ChangeType(propEl, tGeneric);
|
||||
TrySetPropertiesValueRecursively(propEl);
|
||||
|
||||
Type tList = typeof(List<>).MakeGenericType(new Type[] { tGeneric });
|
||||
object prop = Activator.CreateInstance(tList);
|
||||
|
||||
tList.GetMethod("Add").Invoke(prop, new[] { propEl });
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
else
|
||||
{
|
||||
object prop = Activator.CreateInstance(tProperty.PropertyType);
|
||||
TrySetPropertiesValueRecursively(prop);
|
||||
|
||||
tProperty.SetValue(obj, prop);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var lstModel = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name + ".Models") &&
|
||||
e.IsClass &&
|
||||
!e.IsAbstract &&
|
||||
!e.IsInterface &&
|
||||
!e.IsNested
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<Type>();
|
||||
|
||||
foreach (Type tModel in lstModel)
|
||||
{
|
||||
// 模型命名结尾必为 Request 或 Response
|
||||
if (!tModel.Name.EndsWith("Request") && !tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatTenpayRequest、且有同名 Response
|
||||
if (tModel.Name.EndsWith("Request"))
|
||||
{
|
||||
if (!typeof(WechatWorkRequest).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Request".Length)}Response"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Request 必继承自 WechatTenpayResponse、且有同名 Request
|
||||
if (tModel.Name.EndsWith("Response"))
|
||||
{
|
||||
if (!typeof(WechatWorkResponse).IsAssignableFrom(tModel))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lstModel.Any(e => e.Name == $"{tModel.Name.Substring(0, tModel.Name.Length - "Response".Length)}Request"))
|
||||
{
|
||||
exceptions.Add(tModel);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归构造模型,并尝试 JSON 序列化以检测是否有序列化问题
|
||||
try
|
||||
{
|
||||
object instance = _assembly.CreateInstance(tModel.Namespace + "." + tModel.Name);
|
||||
TrySetPropertiesValueRecursively(instance);
|
||||
|
||||
new FlurlNewtonsoftJsonSerializer().Serialize(instance);
|
||||
new FlurlSystemTextJsonSerializer().Serialize(instance);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Serialize `{tModel.Name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证接口定义")]
|
||||
public void InterfaceDefinitionsTest()
|
||||
{
|
||||
var lstInterface = _assembly.GetTypes()
|
||||
.Where(e =>
|
||||
e.Namespace != null &&
|
||||
e.Namespace.Equals(_assembly.GetName().Name) &&
|
||||
e.Name.StartsWith("WechatTenpayClientExecute") &&
|
||||
e.Name.EndsWith("Extensions")
|
||||
)
|
||||
.ToList();
|
||||
|
||||
var exceptions = new List<MethodInfo>();
|
||||
|
||||
foreach (Type tInterface in lstInterface)
|
||||
{
|
||||
var lstMethod = tInterface.GetMethods()
|
||||
.Where(e =>
|
||||
e.IsPublic &&
|
||||
e.IsStatic &&
|
||||
e.GetParameters().FirstOrDefault().ParameterType == typeof(WechatWorkClient)
|
||||
)
|
||||
.ToList();
|
||||
foreach (MethodInfo tMethod in lstMethod)
|
||||
{
|
||||
var lstParam = tMethod.GetParameters();
|
||||
|
||||
// 参数签名必为 this client + request + cancelToken
|
||||
if (lstParam.Length != 3)
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 第二个参数必为 WechatTenpayRequest 子类
|
||||
if (!typeof(WechatWorkRequest).IsAssignableFrom(lstParam[1].ParameterType))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 方法名与第二个参数、返回值均有相同命名
|
||||
string func = tMethod.Name;
|
||||
string para = lstParam[1].ParameterType.Name;
|
||||
string retv = tMethod.ReturnType.GenericTypeArguments.FirstOrDefault()?.Name;
|
||||
if (para == null || !para.EndsWith("Request"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (retv == null || !retv.EndsWith("Response"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{para.Substring(0, para.Length - "Request".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
else if (!string.Equals(func, $"Execute{retv.Substring(0, retv.Length - "Response".Length)}Async"))
|
||||
{
|
||||
exceptions.Add(tMethod);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "验证字段定义")]
|
||||
public void FieldDefinitionsTest()
|
||||
{
|
||||
var exceptions = new List<string>();
|
||||
|
||||
string[] GetAllFiles(string path)
|
||||
{
|
||||
var results = new List<string>();
|
||||
|
||||
string[] dirs = Directory.GetDirectories(path);
|
||||
string[] files = Directory.GetFiles(path);
|
||||
|
||||
results.AddRange(files);
|
||||
|
||||
foreach (string dir in dirs)
|
||||
{
|
||||
results.AddRange(GetAllFiles(dir));
|
||||
}
|
||||
|
||||
return results.ToArray();
|
||||
}
|
||||
|
||||
void VerifyJsonSamples(string subdir, string subns)
|
||||
{
|
||||
string workdir = Path.Combine(Environment.CurrentDirectory, subdir);
|
||||
Assert.True(Directory.Exists(workdir));
|
||||
|
||||
var lstFile = GetAllFiles(workdir)
|
||||
.Where(e => string.Equals(Path.GetExtension(e), ".json", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ToList();
|
||||
Assert.NotEmpty(lstFile);
|
||||
|
||||
foreach (string file in lstFile)
|
||||
{
|
||||
string json = File.ReadAllText(file);
|
||||
string name = Path.GetFileNameWithoutExtension(file);
|
||||
|
||||
Type type = _assembly.GetType($"{_assembly.GetName().Name}.{subns}.{name}");
|
||||
if (type == null)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var settings = FlurlNewtonsoftJsonSerializer.GetDefaultSerializerSettings();
|
||||
settings.CheckAdditionalContent = true;
|
||||
settings.MissingMemberHandling = Newtonsoft.Json.MissingMemberHandling.Error;
|
||||
new FlurlNewtonsoftJsonSerializer(settings).Deserialize(json, type);
|
||||
|
||||
new FlurlSystemTextJsonSerializer().Deserialize(json, type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
exceptions.Add(name);
|
||||
|
||||
if (ex is Newtonsoft.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by Newtonsoft.Json failed.", ex);
|
||||
else if (ex is System.Text.Json.JsonException)
|
||||
throw new Exception($"Deserialize `{name}` by System.Text.Json failed.", ex);
|
||||
else
|
||||
throw new Exception($"Deserialize `{name}` failed.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VerifyJsonSamples("ModelSamples", "Models");
|
||||
|
||||
Assert.Empty(exceptions);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user