From c414de558b795f3e47890e670927e97bb0c04bd2 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 11 Jun 2021 18:31:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(wxads):=20=E5=AF=BC=E5=85=A5=E5=BE=AE?= =?UTF-8?q?=E4=BF=A1=E5=B9=BF=E5=91=8A=E5=B9=B3=E5=8F=B0=20API=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E9=A1=B9=E7=9B=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SKIT.FlurlHttpClient.Wechat.sln | 16 +- .../FlurlHttpRequestOptionsExtensions.cs | 26 ++++ .../Interceptors/WechatAdsAuthenticator.cs | 33 ++++ .../SKIT.FlurlHttpClient.Wechat.Ads.csproj | 37 +++++ .../WechatAdsClient.cs | 145 ++++++++++++++++++ .../WechatAdsClientOptions.cs | 37 +++++ .../WechatAdsEndpoints.cs | 15 ++ .../WechatAdsException.cs | 27 ++++ .../WechatAdsRequest.cs | 32 ++++ .../WechatAdsResponse.cs | 55 +++++++ .../.gitignore | 1 + ...lurlHttpClient.Wechat.Ads.UnitTests.csproj | 31 ++++ .../TestClients.cs | 19 +++ .../TestConfigs.cs | 35 +++++ .../WechatAdsSecurityTests.cs | 24 +++ .../appsettings.json | 10 ++ 16 files changed, 542 insertions(+), 1 deletion(-) create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/Extensions/Internal/FlurlHttpRequestOptionsExtensions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/Interceptors/WechatAdsAuthenticator.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/SKIT.FlurlHttpClient.Wechat.Ads.csproj create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClient.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClientOptions.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsEndpoints.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsException.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsRequest.cs create mode 100644 src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsResponse.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/.gitignore create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestClients.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestConfigs.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/WechatAdsSecurityTests.cs create mode 100644 test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/appsettings.json diff --git a/SKIT.FlurlHttpClient.Wechat.sln b/SKIT.FlurlHttpClient.Wechat.sln index 9cca39f1..dc818d0c 100644 --- a/SKIT.FlurlHttpClient.Wechat.sln +++ b/SKIT.FlurlHttpClient.Wechat.sln @@ -18,6 +18,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat EndProject 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("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Ads", "src\SKIT.FlurlHttpClient.Wechat.Ads\SKIT.FlurlHttpClient.Wechat.Ads.csproj", "{7F155EFB-152F-4798-9984-99102B21D2F8}" +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}" @@ -30,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SKIT.FlurlHttpClient.Wechat EndProject 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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SKIT.FlurlHttpClient.Wechat.Ads.UnitTests", "test\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests\SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj", "{561E0BFB-7817-41FE-BAF2-D78817679AC1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +72,14 @@ Global {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 {DBF84F66-1436-4599-93AB-7C16A3A2C3A4}.Release|Any CPU.Build.0 = Release|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F155EFB-152F-4798-9984-99102B21D2F8}.Release|Any CPU.Build.0 = Release|Any CPU + {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {561E0BFB-7817-41FE-BAF2-D78817679AC1}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,11 +89,13 @@ Global {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} + {4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} {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} {DBF84F66-1436-4599-93AB-7C16A3A2C3A4} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} - {4C48D9D5-1D7F-4616-A05D-256555C310FC} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} + {7F155EFB-152F-4798-9984-99102B21D2F8} = {3E34ADB9-1F52-4C96-9A42-DE782DE1AAA3} + {561E0BFB-7817-41FE-BAF2-D78817679AC1} = {C95AF531-CF44-44AA-AC90-F4DF9F941674} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {F08ED64E-2517-4B51-A4BE-D33D56CC7B39} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/Extensions/Internal/FlurlHttpRequestOptionsExtensions.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/Extensions/Internal/FlurlHttpRequestOptionsExtensions.cs new file mode 100644 index 00000000..458220d1 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/Extensions/Internal/FlurlHttpRequestOptionsExtensions.cs @@ -0,0 +1,26 @@ +using System; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + internal static class FlurlHttpRequestOptionsExtensions + { + public static IFlurlRequest SetOptions(this IFlurlRequest request, WechatAdsRequest options) + { + if (request == null) throw new ArgumentNullException(nameof(request)); + if (options == null) throw new ArgumentNullException(nameof(options)); + + if (options.Timeout.HasValue) + { + request.WithTimeout(TimeSpan.FromMilliseconds(options.Timeout.Value)); + } + + if (!string.IsNullOrEmpty(options.Version)) + { + request.SetQueryParam("version", options.Version); + } + + return request; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/Interceptors/WechatAdsAuthenticator.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/Interceptors/WechatAdsAuthenticator.cs new file mode 100644 index 00000000..75d277e0 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/Interceptors/WechatAdsAuthenticator.cs @@ -0,0 +1,33 @@ +using System; +using System.Linq; +using System.Text; +using Flurl; +using Flurl.Http; + +namespace SKIT.FlurlHttpClient.Wechat.Ads.Interceptors +{ + internal class WechatAdsAuthenticator + { + private readonly string _agencyId; + private readonly string _agencyApiKey; + + public WechatAdsAuthenticator(string agencyId, string agencyApiKey) + { + _agencyId = agencyId; + _agencyApiKey = agencyApiKey; + } + + public void Authenticate(FlurlCall flurlCall) + { + if (flurlCall == null) throw new ArgumentNullException(nameof(flurlCall)); + + string timestamp = DateTimeOffset.Now.ToLocalTime().ToUnixTimeSeconds().ToString(); + string nonce = Guid.NewGuid().ToString("N"); + string sign = Security.MD5Utility.Hash($"{_agencyId}{timestamp}{nonce}{_agencyApiKey}").ToLower(); + string token = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{_agencyId},{timestamp},{nonce},{sign}")); + + flurlCall.Request.RemoveQueryParam("agency_token"); + flurlCall.Request.SetQueryParam("agency_token", token); + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/SKIT.FlurlHttpClient.Wechat.Ads.csproj b/src/SKIT.FlurlHttpClient.Wechat.Ads/SKIT.FlurlHttpClient.Wechat.Ads.csproj new file mode 100644 index 00000000..ddbebeea --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/SKIT.FlurlHttpClient.Wechat.Ads.csproj @@ -0,0 +1,37 @@ + + + + net461; netstandard2.0 + 8.0 + enable + true + + + + SKIT.FlurlHttpClient.Wechat.Ads + MIT + https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat + Flurl.Http Wechat Weixin MicroMessage WechatAds WechatAdvertisting 微信 广告平台 微信广告平台 腾讯广告平台 广点通 微信广点通 腾讯广点通 + 0.1.0-alpha + Flurl.Http client for Wechat Ads Open API. 基于 Flurl.Http 的微信广告平台(广点通)API 客户端。 + Fu Diwei + git + https://github.com/fudiwei/DotNetCore.SKIT.FlurlHttpClient.Wechat.git + true + true + snupkg + + + + + + + + + + + + + + + diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClient.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClient.cs new file mode 100644 index 00000000..33fc09a8 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClient.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Flurl.Http; +using Flurl.Http.Configuration; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 一个微信广告平台 API HTTP 客户端。 + /// + public class WechatAdsClient : WechatClientBase + { + /// + /// 获取当前客户端使用的微信广告平台服务商 ID。 + /// + public string AgencyId { get; } + + /// + /// 获取当前客户端使用的微信广告平台服务商 AppId。 + /// + public string AgencyAppId { get; } + + /// + /// 获取当前客户端使用的微信广告平台服务商 ApiKey。 + /// + internal string AgencyApiKey { get; } + + /// + /// 获取当前客户端使用的 JSON 序列化器。 + /// + internal ISerializer JsonSerializer + { + get { return ProxyFlurlClient.Settings?.JsonSerializer ?? new FlurlNewtonsoftJsonSerializer(); } + } + + /// + /// 用指定的配置项初始化 类的新实例。 + /// + /// 配置项。 + public WechatAdsClient(WechatAdsClientOptions options) + : base() + { + if (options == null) throw new ArgumentNullException(nameof(options)); + + AgencyId = options.AgencyId; + AgencyAppId = options.AgencyAppId; + AgencyApiKey = options.AgencyApiKey; + + ProxyFlurlClient.BaseUrl = options.Endpoints ?? WechatAdsEndpoints.DEFAULT; + ProxyFlurlClient.WithTimeout(TimeSpan.FromMilliseconds(options.Timeout)); + + var interceptorAuthenticator = new Interceptors.WechatAdsAuthenticator( + agencyId: options.AgencyId, + agencyApiKey: options.AgencyApiKey + ); + ProxyFlurlClient.BeforeCall((call) => interceptorAuthenticator.Authenticate(call)); + } + + /// + /// 用指定的微信广告平台服务商 ID、微信广告平台服务商 AppId、微信广告平台服务商 ApiKey 初始化 类的新实例。 + /// + /// 微信广告平台服务商 ID。 + /// 微信广告平台服务商 AppId。 + /// 微信广告平台服务商 ApiKey。 + public WechatAdsClient(string agencyId, string agencyAppId, string agencyApiKey) + : this(new WechatAdsClientOptions() { AgencyId = agencyId, AgencyAppId = agencyAppId, AgencyApiKey = agencyApiKey }) + { + } + + /// + /// 异步发起请求。 + /// + /// + /// + /// + /// + /// + public async Task SendRequestAsync(IFlurlRequest request, HttpContent? content = null, CancellationToken cancellationToken = default) + where T : WechatAdsResponse, new() + { + try + { + using IFlurlResponse response = await base.SendRequestAsync(request, content, cancellationToken).ConfigureAwait(false); + return await GetResposneAsync(response).ConfigureAwait(false); + } + catch (FlurlHttpException ex) + { + throw new WechatAdsException(ex.Message, ex); + } + } + + /// + /// 异步发起请求。 + /// + /// + /// + /// + /// + /// + public async Task SendRequestWithJsonAsync(IFlurlRequest request, object? data = null, CancellationToken cancellationToken = default) + where T : WechatAdsResponse, new() + { + try + { + using IFlurlResponse response = await base.SendRequestWithJsonAsync(request, data, cancellationToken).ConfigureAwait(false); + return await GetResposneAsync(response).ConfigureAwait(false); + } + catch (FlurlHttpException ex) + { + throw new WechatAdsException(ex.Message, ex); + } + } + + private async Task GetResposneAsync(IFlurlResponse response) + where T : WechatAdsResponse, new() + { + string contentType = response.Headers.GetAll("Content-Type").FirstOrDefault() ?? string.Empty; + string contentDisposition = response.Headers.GetAll("Content-Disposition").FirstOrDefault() ?? string.Empty; + bool contentTypeIsNotJson = + (response.StatusCode != (int)HttpStatusCode.OK) || + (!contentType.StartsWith("application/json") && !contentType.StartsWith("text/json")) || + (contentDisposition.StartsWith("attachment")); + + T result = contentTypeIsNotJson ? new T() : await response.GetJsonAsync().ConfigureAwait(false); + result.RawStatus = response.StatusCode; + result.RawHeaders = new ReadOnlyDictionary( + response.Headers + .GroupBy(e => e.Name) + .ToDictionary( + k => k.Key, + v => string.Join(", ", v.Select(e => e.Value)) + ) + ); + result.RawBytes = await response.ResponseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false); + return result; + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClientOptions.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClientOptions.cs new file mode 100644 index 00000000..763f53fd --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsClientOptions.cs @@ -0,0 +1,37 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 一个用于构造 时使用的配置项。 + /// + public class WechatAdsClientOptions + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。 + /// 默认值:30000 + /// + public int Timeout { get; set; } = 30 * 1000; + + /// + /// 获取或设置微信广告平台 API 域名。 + /// 默认值: + /// + public string? Endpoints { get; set; } = WechatAdsEndpoints.DEFAULT; + + /// + /// 获取或设置微信广告平台服务商 ID。 + /// + public string AgencyId { get; set; } = default!; + + /// + /// 获取或设置微信广告平台服务商 AppId。 + /// + public string AgencyAppId { get; set; } = default!; + + /// + /// 获取或设置微信广告平台服务商 ApiKey。 + /// + public string AgencyApiKey { get; set; } = default!; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsEndpoints.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsEndpoints.cs new file mode 100644 index 00000000..380a8b99 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsEndpoints.cs @@ -0,0 +1,15 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 微信广告平台 API 接口域名。 + /// + public static class WechatAdsEndpoints + { + /// + /// 主域名(默认)。 + /// + public const string DEFAULT = "https://api.weixin.qq.com"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsException.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsException.cs new file mode 100644 index 00000000..393020f0 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsException.cs @@ -0,0 +1,27 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 微信广告平台 API 出错时引发的异常。 + /// + public class WechatAdsException : WechatExceptionBase + { + /// + public WechatAdsException() + { + } + + /// + public WechatAdsException(string message) + : base(message) + { + } + + /// + public WechatAdsException(string message, Exception innerException) + : base(message, innerException) + { + } + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsRequest.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsRequest.cs new file mode 100644 index 00000000..82ad498e --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsRequest.cs @@ -0,0 +1,32 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 微信广告平台 API 请求的基类。 + /// + public abstract class WechatAdsRequest : IWechatRequest + { + /// + /// 获取或设置请求超时时间(单位:毫秒)。如果不指定将使用构造 时的 参数,这在需要指定特定耗时请求(比如上传或下载文件)的超时时间时很有用。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual int? Timeout { get; set; } + + /// + /// 获取或设置微信广告平台的 AccessToken。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string? AccessToken { get; set; } + + /// + /// 获取或设置微信广告平台的版本号。 + /// 默认值:v1.0 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public virtual string? Version { get; set; } = "v1.0"; + } +} diff --git a/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsResponse.cs b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsResponse.cs new file mode 100644 index 00000000..26a8e002 --- /dev/null +++ b/src/SKIT.FlurlHttpClient.Wechat.Ads/WechatAdsResponse.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace SKIT.FlurlHttpClient.Wechat.Ads +{ + /// + /// 微信广告平台 API 响应的基类。 + /// + public abstract class WechatAdsResponse : IWechatResponse + { + /// + /// 获取原始的 HTTP 响应状态码。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public int RawStatus { get; internal set; } + + /// + /// 获取原始的 HTTP 响应表头集合。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public IDictionary RawHeaders { get; internal set; } = default!; + + /// + /// 获取原始的 HTTP 响应正文。 + /// + [Newtonsoft.Json.JsonIgnore] + [System.Text.Json.Serialization.JsonIgnore] + public byte[] RawBytes { get; internal set; } = default!; + + /// + /// 获取微信广告平台 API 返回的错误码。 + /// + [Newtonsoft.Json.JsonProperty("errcode")] + [System.Text.Json.Serialization.JsonPropertyName("errcode")] + public virtual int ErrorCode { get; set; } + + /// + /// 获取微信广告平台 API 返回的错误描述。 + /// + [Newtonsoft.Json.JsonProperty("errmsg")] + [System.Text.Json.Serialization.JsonPropertyName("errmsg")] + public virtual string? ErrorMessage { get; set; } + + /// + /// 获取一个值,该值指示调用微信广告平台 API 是否成功(即 HTTP 状态码为 200、且 errcode 值为 0)。 + /// + /// + public virtual bool IsSuccessful() + { + return RawStatus == 200 && ErrorCode == 0; + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/.gitignore b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/.gitignore new file mode 100644 index 00000000..63666fbf --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/.gitignore @@ -0,0 +1 @@ +appsettings.local.json \ No newline at end of file diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj new file mode 100644 index 00000000..6c93fb21 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests.csproj @@ -0,0 +1,31 @@ + + + + netcoreapp3.1 + 8.0 + + + + + + + PreserveNewest + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestClients.cs b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestClients.cs new file mode 100644 index 00000000..ac2e757f --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestClients.cs @@ -0,0 +1,19 @@ +using System; + +namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests +{ + class TestClients + { + static TestClients() + { + Instance = new WechatAdsClient(new WechatAdsClientOptions() + { + AgencyId = TestConfigs.WechatAgencyId, + AgencyAppId = TestConfigs.WechatAgencyAppId, + AgencyApiKey = TestConfigs.WechatAgencyApiKey + }); + } + + public static readonly WechatAdsClient Instance; + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestConfigs.cs b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestConfigs.cs new file mode 100644 index 00000000..18b69603 --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/TestConfigs.cs @@ -0,0 +1,35 @@ +using System; +using System.IO; +using System.Text.Json; + +namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests +{ + class TestConfigs + { + static TestConfigs() + { + // NOTICE: 请在项目根目录下建立 appsettings.local.json,按照 appsettings.json 的格式填入测试参数。 + // WARN: 敏感信息请不要提交到 git! + + using var stream = File.OpenRead("appsettings.local.json"); + using var json = JsonDocument.Parse(stream); + + var config = json.RootElement.GetProperty("WechatConfig"); + WechatAgencyId = config.GetProperty("AgencyId").GetString(); + WechatAgencyAppId = config.GetProperty("AgencyAppId").GetString(); + WechatAgencyApiKey = config.GetProperty("AgencyApiKey").GetString(); + WechatAccessToken = config.GetProperty("AccessToken").GetString(); + + ProjectSourceDirectory = json.RootElement.GetProperty("ProjectSourceDirectory").GetString(); + ProjectTestDirectory = json.RootElement.GetProperty("ProjectTestDirectory").GetString(); + } + + public static readonly string WechatAgencyId; + public static readonly string WechatAgencyAppId; + public static readonly string WechatAgencyApiKey; + public static readonly string WechatAccessToken; + + public static readonly string ProjectSourceDirectory; + public static readonly string ProjectTestDirectory; + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/WechatAdsSecurityTests.cs b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/WechatAdsSecurityTests.cs new file mode 100644 index 00000000..4f5b8e9d --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/WechatAdsSecurityTests.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace SKIT.FlurlHttpClient.Wechat.Ads.UnitTests +{ + public class WechatAdsSecurityTests + { + [Fact(DisplayName = "信息摘要(MD5)")] + public void Md5HashTest() + { + string rawData = "spidbff89d5138160943040012345678901234567890uFolxxiZbrZ/PRbyen5uK5D1kgIB2yHyDsfDGxxgeG"; + + string actualHash = Security.MD5Utility.Hash(rawData); + string expectdHash = "32c03e8fcdb08e653e42805e302f70ed"; + + Assert.Equal(expectdHash, actualHash, ignoreCase: true); + } + } +} diff --git a/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/appsettings.json b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/appsettings.json new file mode 100644 index 00000000..082663ed --- /dev/null +++ b/test/SKIT.FlurlHttpClient.Wechat.Ads.UnitTests/appsettings.json @@ -0,0 +1,10 @@ +{ + "WechatConfig": { + "AgencyId": "请在此填写用于测试的微信广告 AgencyId", + "AgencyAppId": "请在此填写用于测试的微信广告 AgencyAppId", + "AgencyApiKey": "请在此填写用于测试的微信广告 AgencyApiKey", + "AccessToken": "请在此填写用于测试的微信广告 AccessToken" + }, + "ProjectSourceDirectory": "请输入当前 SDK 项目所在的目录完整路径,如 C:\\Project\\src\\SKIT.FlurlHttpClient.Wechat.Api\\", + "ProjectTestDirectory": "请输入当前测试项目所在的目录完整路径,如 C:\\Project\\test\\SKIT.FlurlHttpClient.Wechat.Api.UnitTests\\" +} \ No newline at end of file