From 0805414a425d3b6e9bec455dadd733def0acdd66 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 4 Jun 2021 21:44:42 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=8F=90=E5=8F=96=E5=85=AC=E5=85=B1?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=B5=81=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKIT.FlurlHttpClient.Wechat.sln | 24 +- ...lurlHttpClient.Wechat.Api.UnitTests.csproj | 1 + .../WechatApiDeclarationTests.cs | 66 ++++ .../WechatApiDefinitionTests.cs | 347 ------------------ ...ttpClient.Wechat.TenpayV3.UnitTests.csproj | 1 + ...verterTests.cs => WechatConverterTests.cs} | 2 +- .../WechatTenpayDeclarationTests.cs | 66 ++++ .../WechatTenpayDefinitionTests.cs | 303 --------------- ...IT.FlurlHttpClient.Wechat.TestTools.csproj | 12 + .../TestAssertUtil.cs | 316 ++++++++++++++++ .../TestIOUtil.cs | 27 ++ .../TestReflectionUtil.cs | 158 ++++++++ ...urlHttpClient.Wechat.Work.UnitTests.csproj | 1 + .../WechatWorkDeclarationTests.cs | 52 +++ .../WechatWorkDefinitionTests.cs | 302 --------------- 15 files changed, 718 insertions(+), 960 deletions(-) create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDeclarationTests.cs delete mode 100644 test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDefinitionTests.cs rename test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/{WechatTenpayConverterTests.cs => WechatConverterTests.cs} (99%) create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDeclarationTests.cs delete mode 100644 test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDefinitionTests.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDeclarationTests.cs delete mode 100644 test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDefinitionTests.cs diff --git a/SKIT.FlurlHttpClient.Wechat.sln b/SKIT.FlurlHttpClient.Wechat.sln index c3f4ef22..9cca39f1 100644 --- a/SKIT.FlurlHttpClient.Wechat.sln +++ b/SKIT.FlurlHttpClient.Wechat.sln @@ -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} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj index d07beb62..d1e3e351 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/SKIT.FlurlHttpClient.Wechat.Api.UnitTests.csproj @@ -33,6 +33,7 @@ + diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDeclarationTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDeclarationTests.cs new file mode 100644 index 00000000..84d11603 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDeclarationTests.cs @@ -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); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDefinitionTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDefinitionTests.cs deleted file mode 100644 index a993a8db..00000000 --- a/test/SKIT.FlurlHttpClient.Wechat.Api.UnitTests/WechatApiDefinitionTests.cs +++ /dev/null @@ -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(); - var systemTextJsonAttribute = tProperty.GetCustomAttribute(); - 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(); - - 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(); - - 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[] GetAllFiles(string path) - { - var results = new List(); - - 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); - } - } -} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj index 00a21f4a..bc02e2fd 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests.csproj @@ -30,6 +30,7 @@ + diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayConverterTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatConverterTests.cs similarity index 99% rename from test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayConverterTests.cs rename to test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatConverterTests.cs index ded8f656..4d85b78d 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayConverterTests.cs +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatConverterTests.cs @@ -7,7 +7,7 @@ using Xunit; namespace SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests { - public class WechatTenpayConverterTests + public class WechatConverterTests { class JsonDateTimeOffsetTestEntity { diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDeclarationTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDeclarationTests.cs new file mode 100644 index 00000000..484f1401 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDeclarationTests.cs @@ -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); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDefinitionTests.cs b/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDefinitionTests.cs deleted file mode 100644 index b5dac33d..00000000 --- a/test/SKIT.FlurlHttpClient.Wechat.TenpayV3.UnitTests/WechatTenpayDefinitionTests.cs +++ /dev/null @@ -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(); - var systemTextJsonAttribute = tProperty.GetCustomAttribute(); - 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(); - - 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(); - - 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(); - - void VerifyJsonSamples(string subdir, string subns) - { - string[] GetFiles(string path) - { - var results = new List(); - - 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); - } - } -} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj b/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj new file mode 100644 index 00000000..ffa32df8 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/SKIT.FlurlHttpClient.Wechat.TestTools.csproj @@ -0,0 +1,12 @@ + + + + netcoreapp3.1 + 8.0 + + + + + + + diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs new file mode 100644 index 00000000..a0b3e3b7 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestAssertUtil.cs @@ -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(); + var systemTextJsonAttribute = propInfo.GetCustomAttribute(); + 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(); + + 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(); + + 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(); + + 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(); + + 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; + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs new file mode 100644 index 00000000..2c786d8d --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestIOUtil.cs @@ -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 results = new List(); + 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(); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs new file mode 100644 index 00000000..bea76f6e --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.TestTools/TestReflectionUtil.cs @@ -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 func = null; + func = new Func((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); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj index cd2cb35f..535408fb 100644 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/SKIT.FlurlHttpClient.Wechat.Work.UnitTests.csproj @@ -27,6 +27,7 @@ + diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDeclarationTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDeclarationTests.cs new file mode 100644 index 00000000..e4ef7e03 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDeclarationTests.cs @@ -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); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDefinitionTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDefinitionTests.cs deleted file mode 100644 index aefc582e..00000000 --- a/test/SKIT.FlurlHttpClient.Wechat.Work.UnitTests/WechatWorkDefinitionTests.cs +++ /dev/null @@ -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(); - var systemTextJsonAttribute = tProperty.GetCustomAttribute(); - 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(); - - 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(); - - 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[] GetAllFiles(string path) - { - var results = new List(); - - 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); - } - } -}