aba4173827
Share template keys across email and push with independent editors and revisions. Render localized actions and preserve version-one delivery. Ref: IT-1627
89 lines
5.7 KiB
C#
89 lines
5.7 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using HrynCo.NotificationService.Contracts.Messages;
|
|
using HrynCo.NotificationService.DAL.Abstract.Push;
|
|
using HrynCo.NotificationService.DAL.Abstract.PushChannels;
|
|
using HrynCo.NotificationService.DAL.Abstract.Repositories;
|
|
using HrynCo.NotificationService.DAL.Abstract.Templates;
|
|
using HrynCo.NotificationService.Worker.Services.Push;
|
|
using NSubstitute;
|
|
|
|
namespace HrynCo.NotificationService.Services.Tests.Push;
|
|
|
|
public sealed class FcmPushSenderTests
|
|
{
|
|
[Theory]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public async Task ResolvesCurrentServiceNameToActiveChannelAndUsesOnlyServerCredentials(int version)
|
|
{
|
|
var services = Substitute.For<INotificationServiceRepository>();
|
|
var channels = Substitute.For<IPushChannelRepository>();
|
|
var client = Substitute.For<IFirebasePushClient>();
|
|
var templates = Substitute.For<IPushTemplateDefinitionRepository>();
|
|
templates.GetAsync(Arg.Any<Guid>(), "Test", Arg.Any<CancellationToken>()).Returns(new PushTemplateDefinition { Key = "Test", Languages = [new PushTemplateTranslation { LanguageCode = "en", Title = "Test", Body = "Test" }] });
|
|
var service = new NotificationServiceInfo { Id = Guid.NewGuid(), Name = "Invemory-Renamed" };
|
|
var channel = new PushChannel { Id = Guid.NewGuid(), ServiceId = service.Id, FirebaseProjectId = "test-project", IsActive = true };
|
|
services.GetByNameAsync(service.Name, Arg.Any<CancellationToken>()).Returns(service);
|
|
channels.GetActiveForServiceAsync(service.Id, Arg.Any<CancellationToken>()).Returns(channel);
|
|
using var rsa = RSA.Create(2048);
|
|
var json = JsonSerializer.Serialize(new
|
|
{
|
|
type = "service_account", project_id = "test-project", client_email = "test@example.invalid",
|
|
private_key = rsa.ExportPkcs8PrivateKeyPem(), token_uri = "https://untrusted.invalid/token"
|
|
});
|
|
channels.GetCredentialsAsync(channel.Id, Arg.Any<CancellationToken>()).Returns(json);
|
|
client.SendAsync(Arg.Any<Guid>(), Arg.Any<RenderedPushMessageData>(), Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>())
|
|
.Returns(new PushDeliveryOutcome(null));
|
|
var data = version == 1 ? Data(service.Name) with { Version = 1, Title = "Legacy title", Body = "Legacy body", TemplateKey = null, TemplateData = null } : Data(service.Name);
|
|
var result = await new FcmPushSender(services, channels, client, templates).SendAsync(Guid.NewGuid(), data, CancellationToken.None);
|
|
Assert.Null(result.Error);
|
|
await client.Received(1).SendAsync(Arg.Any<Guid>(), Arg.Any<RenderedPushMessageData>(),
|
|
Arg.Is<FirebaseServiceAccountCredentials>(c => c.ProjectId == "test-project" && c.ClientEmail == "test@example.invalid"), Arg.Any<CancellationToken>());
|
|
await templates.Received(version == 1 ? 0 : 1).GetAsync(Arg.Any<Guid>(), Arg.Any<string>(), Arg.Any<CancellationToken>());
|
|
if (version == 1)
|
|
await client.Received().SendAsync(Arg.Any<Guid>(), Arg.Is<RenderedPushMessageData>(m => m.Title == "Legacy title" && m.Body == "Legacy body" && m.Buttons.Count == 0), Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(false, false)]
|
|
[InlineData(true, false)]
|
|
[InlineData(true, true)]
|
|
public async Task MissingServiceInactiveChannelOrInvalidCredentialsDoNotContactFirebase(bool exists, bool active)
|
|
{
|
|
var services = Substitute.For<INotificationServiceRepository>();
|
|
var channels = Substitute.For<IPushChannelRepository>();
|
|
var client = Substitute.For<IFirebasePushClient>();
|
|
var templates = Substitute.For<IPushTemplateDefinitionRepository>();
|
|
templates.GetAsync(Arg.Any<Guid>(), "Test", Arg.Any<CancellationToken>()).Returns(new PushTemplateDefinition { Key = "Test", Languages = [new PushTemplateTranslation { LanguageCode = "en", Title = "Test", Body = "Test" }] });
|
|
var service = new NotificationServiceInfo { Id = Guid.NewGuid(), Name = "Invemory" };
|
|
if (exists) services.GetByNameAsync(service.Name, Arg.Any<CancellationToken>()).Returns(service);
|
|
if (active)
|
|
{
|
|
var channel = new PushChannel { Id = Guid.NewGuid(), ServiceId = service.Id, FirebaseProjectId = "test-project", IsActive = true };
|
|
channels.GetActiveForServiceAsync(service.Id, Arg.Any<CancellationToken>()).Returns(channel);
|
|
channels.GetCredentialsAsync(channel.Id, Arg.Any<CancellationToken>()).Returns("secret-must-not-leak");
|
|
}
|
|
var result = await new FcmPushSender(services, channels, client, templates).SendAsync(Guid.NewGuid(), Data(service.Name), CancellationToken.None);
|
|
Assert.NotNull(result.Error);
|
|
Assert.DoesNotContain("secret-must-not-leak", result.Error);
|
|
await client.DidNotReceiveWithAnyArgs().SendAsync(default, default!, default!, default);
|
|
}
|
|
|
|
[Fact]
|
|
public void PublicPayloadHasNoFirebaseAuthorizationFields()
|
|
{
|
|
var json = JsonSerializer.Serialize(Data("Invemory"));
|
|
Assert.DoesNotContain("Fcm", json);
|
|
Assert.DoesNotContain("Credentials", json);
|
|
Assert.DoesNotContain("AccessToken", json);
|
|
}
|
|
|
|
private static SendPushMessageData Data(string serviceName) => new()
|
|
{
|
|
RegistrationId = Guid.NewGuid(), BindingId = Guid.NewGuid(), UserId = Guid.NewGuid(),
|
|
ServiceName = serviceName, Version = 2, TemplateKey = "Test", LanguageCode = "en", TemplateData = new PushTemplateData { Bindings = new Dictionary<string, string>(), Actions = new Dictionary<string, PushActionMetadata>() }, Token = "test-device-token",
|
|
TokenHash = new string('a', 64), ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(10)
|
|
};
|
|
}
|