using System.Net; using System.Net.Http.Headers; using MiaoJiZhang.Api.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; namespace MiaoJiZhang.Api.Tests; public sealed class PushProviderTests { private static readonly PushEnvelope Message = new( "message-1", "测试标题", "测试正文", "system", "home", null, 3600); [Fact] public async Task Xiaomi_Http200BusinessFailure_IsNotAccepted() { var factory = new StubHttpClientFactory(_ => Json( """{"result":"error","code":70000003,"description":"invalid registration_id"}""")); var provider = Provider("xiaomi", factory, new Dictionary { ["Push:Providers:xiaomi:production:Enabled"] = "true", ["Push:Providers:xiaomi:production:AppSecret"] = "server-secret", }); var result = await provider.SendAsync( "production", "com.nx.miaoji", "invalid-token", Message, CancellationToken.None); Assert.False(result.Accepted); Assert.True(result.InvalidToken); Assert.Equal("provider_error", result.ErrorCode); } [Fact] public async Task Xiaomi_Http200Success_IsAccepted() { var factory = new StubHttpClientFactory(_ => Json( """{"result":"ok","code":0,"data":{"id":"xiaomi-message"}}""")); var provider = Provider("xiaomi", factory, new Dictionary { ["Push:Providers:xiaomi:production:Enabled"] = "true", ["Push:Providers:xiaomi:production:AppSecret"] = "server-secret", }); var result = await provider.SendAsync( "production", "com.nx.miaoji", "valid-token", Message, CancellationToken.None); Assert.True(result.Accepted); Assert.False(result.Retryable); Assert.False(result.InvalidToken); } [Fact] public async Task Huawei_AccessTokens_AreCachedPerFlavor() { var authCalls = new Dictionary(); var sendTokens = new Dictionary>(); var factory = new StubHttpClientFactory(request => { var path = request.RequestUri!.AbsolutePath; var flavor = path.Contains("production", StringComparison.Ordinal) ? "production" : "internal"; if (path.EndsWith("/auth", StringComparison.Ordinal)) { authCalls[flavor] = authCalls.GetValueOrDefault(flavor) + 1; return Json($$"""{"access_token":"{{flavor}}-token","expires_in":3600}"""); } sendTokens.TryAdd(flavor, []); sendTokens[flavor].Add(request.Headers.Authorization?.Parameter ?? ""); return Json("""{"code":"80000000","requestId":"huawei-message"}"""); }); var provider = Provider("huawei", factory, new Dictionary { ["Push:Providers:huawei:production:Enabled"] = "true", ["Push:Providers:huawei:production:AppId"] = "production-app", ["Push:Providers:huawei:production:AppSecret"] = "production-secret", ["Push:Providers:huawei:production:AuthUrl"] = "https://push.test/production/auth", ["Push:Providers:huawei:production:SendUrl"] = "https://push.test/production/send", ["Push:Providers:huawei:internal:Enabled"] = "true", ["Push:Providers:huawei:internal:AppId"] = "internal-app", ["Push:Providers:huawei:internal:AppSecret"] = "internal-secret", ["Push:Providers:huawei:internal:AuthUrl"] = "https://push.test/internal/auth", ["Push:Providers:huawei:internal:SendUrl"] = "https://push.test/internal/send", }); Assert.True((await provider.SendAsync( "production", "com.nx.miaoji", "token-1", Message, CancellationToken.None)).Accepted); Assert.True((await provider.SendAsync( "internal", "com.nx.miaoji.internal", "token-2", Message, CancellationToken.None)).Accepted); Assert.True((await provider.SendAsync( "production", "com.nx.miaoji", "token-3", Message, CancellationToken.None)).Accepted); Assert.Equal(1, authCalls["production"]); Assert.Equal(1, authCalls["internal"]); Assert.Equal(["production-token", "production-token"], sendTokens["production"]); Assert.Equal(["internal-token"], sendTokens["internal"]); } private static OfficialPushProvider Provider( string name, IHttpClientFactory factory, Dictionary values) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(values) .Build(); return new OfficialPushProvider( name, configuration, factory, NullLogger.Instance); } private static HttpResponseMessage Json(string body) => new(HttpStatusCode.OK) { Content = new StringContent(body), }; private sealed class StubHttpClientFactory : IHttpClientFactory { private readonly HttpClient client; public StubHttpClientFactory(Func response) { client = new HttpClient(new StubHandler(response)); } public HttpClient CreateClient(string name) => client; } private sealed class StubHandler(Func response) : HttpMessageHandler { protected override Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) => Task.FromResult(response(request)); } }