Files
hrynco-notification-service/HrynCo.NotificationService.Services.Tests/Push/FcmPushSenderTests.cs
T
agrynco 1631786ce2 feat(push): add service-owned firebase channels and delivery
Add push contracts, provider delivery and channel diagnostics. Link email channels to services and document migration and validation.

Ref: IT-1074
2026-10-24 12:31:35 +03:00

79 lines
4.3 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
{
[Fact]
public async Task ResolvesCurrentServiceNameToActiveChannelAndUsesOnlyServerCredentials()
{
var services = Substitute.For<INotificationServiceRepository>();
var channels = Substitute.For<IPushChannelRepository>();
var client = Substitute.For<IFirebasePushClient>();
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<SendPushMessageData>(), Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>())
.Returns(new PushDeliveryOutcome(null));
var result = await new FcmPushSender(services, channels, client).SendAsync(Guid.NewGuid(), Data(service.Name), CancellationToken.None);
Assert.Null(result.Error);
await client.Received(1).SendAsync(Arg.Any<Guid>(), Arg.Any<SendPushMessageData>(),
Arg.Is<FirebaseServiceAccountCredentials>(c => c.ProjectId == "test-project" && c.ClientEmail == "test@example.invalid"), 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 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).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, Title = "Test", Body = "Test", Token = "test-device-token",
TokenHash = new string('a', 64), ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(10)
};
}