Files

155 lines
5.8 KiB
C#

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<string, string?>
{
["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<string, string?>
{
["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<string, int>();
var sendTokens = new Dictionary<string, List<string>>();
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<string, string?>
{
["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<string, string?> values)
{
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(values)
.Build();
return new OfficialPushProvider(
name,
configuration,
factory,
NullLogger<OfficialPushProvider>.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<HttpRequestMessage, HttpResponseMessage> response)
{
client = new HttpClient(new StubHandler(response));
}
public HttpClient CreateClient(string name) => client;
}
private sealed class StubHandler(Func<HttpRequestMessage, HttpResponseMessage> response)
: HttpMessageHandler
{
protected override Task<HttpResponseMessage> SendAsync(
HttpRequestMessage request,
CancellationToken cancellationToken) =>
Task.FromResult(response(request));
}
}