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); } } }