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
This commit is contained in:
@@ -13,10 +13,16 @@ public sealed class DevelopmentDataSeederTests
|
||||
private readonly IEmailTemplateRepository _templates = Substitute.For<IEmailTemplateRepository>();
|
||||
private readonly INotificationServiceRepository _services = Substitute.For<INotificationServiceRepository>();
|
||||
|
||||
public DevelopmentDataSeederTests()
|
||||
{
|
||||
_services.SaveAsync(Arg.Any<NotificationServiceInfo>(), true, Arg.Any<CancellationToken>())
|
||||
.Returns(call => { call.Arg<NotificationServiceInfo>().Id = Guid.NewGuid(); return Task.CompletedTask; });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SeedAsync_WhenConfigurationIsMissing_CreatesMailpitChannelAndTestTemplate()
|
||||
{
|
||||
_channels.GetByServiceAsync("TestService", Arg.Any<CancellationToken>())
|
||||
_channels.GetByServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Array.Empty<EmailChannel>());
|
||||
_templates.GetAsync("TestService", "TestEmail", "en", Arg.Any<CancellationToken>())
|
||||
.Returns((EmailTemplate?)null);
|
||||
@@ -39,10 +45,10 @@ public sealed class DevelopmentDataSeederTests
|
||||
[Fact]
|
||||
public async Task SeedAsync_WhenConfigurationExists_DoesNotDuplicateOrOverwriteIt()
|
||||
{
|
||||
_channels.GetByServiceAsync("TestService", Arg.Any<CancellationToken>())
|
||||
_channels.GetByServiceAsync(Arg.Any<Guid>(), Arg.Any<CancellationToken>())
|
||||
.Returns([new EmailChannel
|
||||
{
|
||||
ServiceName = "TestService",
|
||||
ServiceId = Guid.NewGuid(),
|
||||
EmailChannelType = EmailChannelType.Smtp,
|
||||
Settings = new SmtpChannelSettings()
|
||||
}]);
|
||||
@@ -75,7 +81,7 @@ public sealed class DevelopmentDataSeederTests
|
||||
|
||||
private static bool IsMailpitChannel(EmailChannel channel)
|
||||
{
|
||||
return channel.ServiceName == "TestService" &&
|
||||
return channel.ServiceId != Guid.Empty &&
|
||||
channel.IsActive &&
|
||||
channel.Settings is SmtpChannelSettings smtp &&
|
||||
smtp.Host == "mailpit" &&
|
||||
|
||||
+19
-4
@@ -12,7 +12,7 @@ public sealed class SendEmailDeliveryValidatorTests
|
||||
string? error = SendEmailDeliveryValidator.GetValidationError(
|
||||
CreateMessage(),
|
||||
CreateContext(),
|
||||
SendEmailConsumer.SupportedMessageType);
|
||||
SupportedMessageType.SendEmailV1);
|
||||
|
||||
Assert.Null(error);
|
||||
}
|
||||
@@ -25,7 +25,7 @@ public sealed class SendEmailDeliveryValidatorTests
|
||||
string? error = SendEmailDeliveryValidator.GetValidationError(
|
||||
CreateMessage(),
|
||||
context,
|
||||
SendEmailConsumer.SupportedMessageType);
|
||||
SupportedMessageType.SendEmailV1);
|
||||
|
||||
Assert.Equal("AMQP MessageId is required.", error);
|
||||
}
|
||||
@@ -41,18 +41,33 @@ public sealed class SendEmailDeliveryValidatorTests
|
||||
string? error = SendEmailDeliveryValidator.GetValidationError(
|
||||
CreateMessage(),
|
||||
context,
|
||||
SendEmailConsumer.SupportedMessageType);
|
||||
SupportedMessageType.SendEmailV1);
|
||||
|
||||
Assert.Equal("Unsupported AMQP message type 'Notification.SendEmail.v2'.", error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("Notification.SendPush.v1")]
|
||||
[InlineData("notification.sendemail.v1")]
|
||||
[InlineData("SendEmailV1")]
|
||||
[InlineData("0")]
|
||||
[InlineData("")]
|
||||
[InlineData(null)]
|
||||
public void OtherOrMissingWireType_IsRejected(string? wireType)
|
||||
{
|
||||
string? error = SendEmailDeliveryValidator.GetValidationError(CreateMessage(),
|
||||
CreateContext() with { MessageType = wireType }, SupportedMessageType.SendEmailV1);
|
||||
|
||||
Assert.Equal($"Unsupported AMQP message type '{wireType ?? "<missing>"}'.", error);
|
||||
}
|
||||
|
||||
private static RabbitMqMessageContext CreateContext() => new()
|
||||
{
|
||||
QueueName = SendEmailConsumer.IncomingQueue,
|
||||
Exchange = string.Empty,
|
||||
RoutingKey = SendEmailConsumer.IncomingQueue,
|
||||
MessageId = Guid.NewGuid().ToString("D"),
|
||||
MessageType = SendEmailConsumer.SupportedMessageType,
|
||||
MessageType = "Notification.SendEmail.v1",
|
||||
CorrelationId = "correlation-id",
|
||||
ContentType = "application/json"
|
||||
};
|
||||
|
||||
@@ -23,12 +23,12 @@ public sealed class SendEmailServiceTests
|
||||
Password = "not-a-real-secret",
|
||||
UseSsl = true,
|
||||
FromEmail = "notifications@example.com",
|
||||
FromName = "StoreMate"
|
||||
FromName = "Invemory"
|
||||
};
|
||||
var channel = new EmailChannel
|
||||
{
|
||||
Id = channelId,
|
||||
ServiceName = "StoreMate-Prod",
|
||||
ServiceId = Guid.NewGuid(),
|
||||
Priority = 1,
|
||||
EmailChannelType = EmailChannelType.Smtp,
|
||||
Settings = settings,
|
||||
@@ -36,7 +36,7 @@ public sealed class SendEmailServiceTests
|
||||
};
|
||||
var template = new EmailTemplate
|
||||
{
|
||||
ServiceName = "StoreMate-Prod",
|
||||
ServiceName = "Invemory-Prod",
|
||||
Key = "EmailVerification",
|
||||
LanguageCode = "uk",
|
||||
Subject = "Verify {{AppName}}",
|
||||
@@ -46,17 +46,21 @@ public sealed class SendEmailServiceTests
|
||||
};
|
||||
|
||||
IEmailChannelRepository channels = Substitute.For<IEmailChannelRepository>();
|
||||
channels.GetByServiceAsync("StoreMate-Prod", Arg.Any<CancellationToken>())
|
||||
channels.GetByServiceAsync(channel.ServiceId, Arg.Any<CancellationToken>())
|
||||
.Returns([channel]);
|
||||
var services = Substitute.For<INotificationServiceRepository>();
|
||||
services.GetByNameAsync("Invemory-Prod", Arg.Any<CancellationToken>())
|
||||
.Returns(new NotificationServiceInfo { Id = channel.ServiceId, Name = "Invemory-Prod" });
|
||||
IEmailChannelUsageRepository usage = Substitute.For<IEmailChannelUsageRepository>();
|
||||
IEmailTemplateService templates = Substitute.For<IEmailTemplateService>();
|
||||
templates.GetAsync("StoreMate-Prod", "EmailVerification", "uk", Arg.Any<CancellationToken>())
|
||||
templates.GetAsync("Invemory-Prod", "EmailVerification", "uk", Arg.Any<CancellationToken>())
|
||||
.Returns(template);
|
||||
var renderer = new EmailTemplateRenderingService();
|
||||
var smtp = new RecordingSmtpEmailSender();
|
||||
INotificationResultPublisher resultPublisher = Substitute.For<INotificationResultPublisher>();
|
||||
var service = new SendEmailService(
|
||||
channels,
|
||||
services,
|
||||
usage,
|
||||
templates,
|
||||
renderer,
|
||||
@@ -71,12 +75,12 @@ public sealed class SendEmailServiceTests
|
||||
},
|
||||
Data = new SendEmailMessageData
|
||||
{
|
||||
ServiceName = "StoreMate-Prod",
|
||||
ServiceName = "Invemory-Prod",
|
||||
TemplateKey = "EmailVerification",
|
||||
RecipientEmail = "owner@example.com",
|
||||
RecipientName = "Owner",
|
||||
LanguageCode = "uk",
|
||||
Variables = new Dictionary<string, string> { ["AppName"] = "StoreMate" }
|
||||
Variables = new Dictionary<string, string> { ["AppName"] = "Invemory" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -84,7 +88,7 @@ public sealed class SendEmailServiceTests
|
||||
|
||||
Assert.Equal(1, smtp.SendCount);
|
||||
Assert.Same(settings, smtp.Settings);
|
||||
Assert.Equal("Verify StoreMate", smtp.Email?.Subject);
|
||||
Assert.Equal("Verify Invemory", smtp.Email?.Subject);
|
||||
Assert.Equal("owner@example.com", smtp.RecipientEmail);
|
||||
Assert.Equal("Owner", smtp.RecipientName);
|
||||
await usage.Received(1).IncrementUsageAsync(
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
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)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Push;
|
||||
using HrynCo.NotificationService.DAL.Abstract.PushChannels;
|
||||
using HrynCo.NotificationService.Worker.Services.Push;
|
||||
using NSubstitute;
|
||||
|
||||
namespace HrynCo.NotificationService.Services.Tests.Push;
|
||||
|
||||
public sealed class PushChannelTestServiceTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(false)]
|
||||
[InlineData(true)]
|
||||
public async Task UsesSavedCredentialsWithoutSavingTokenAndSeparatesAuthorizationFromSend(bool send)
|
||||
{
|
||||
var channels = Substitute.For<IPushChannelRepository>();
|
||||
var firebase = Client();
|
||||
var id = Guid.NewGuid();
|
||||
channels.GetByIdAsync(id, Arg.Any<CancellationToken>()).Returns(new PushChannel
|
||||
{ Id = id, ServiceId = Guid.NewGuid(), FirebaseProjectId = "test-project", IsActive = false });
|
||||
channels.GetCredentialsAsync(id, Arg.Any<CancellationToken>()).Returns(Credentials());
|
||||
var outcome = await new PushChannelTestService(channels, firebase).TestAsync(new PushChannelTestSettings
|
||||
{ ChannelId = id, FirebaseProjectId = "test-project", RecipientToken = send ? "test-token" : null }, send, default);
|
||||
Assert.Null(outcome.Error);
|
||||
await firebase.Received(send ? 1 : 0).SendTestAsync("test-token", Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>());
|
||||
await firebase.Received(send ? 0 : 1).CheckAuthorizationAsync(Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>());
|
||||
await channels.DidNotReceiveWithAnyArgs().SaveAsync(default!, default!, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task UnsavedCredentialsOverrideSavedConfiguration()
|
||||
{
|
||||
var channels = Substitute.For<IPushChannelRepository>();
|
||||
var firebase = Client();
|
||||
var outcome = await new PushChannelTestService(channels, firebase).TestAsync(new PushChannelTestSettings
|
||||
{ ChannelId = Guid.NewGuid(), FirebaseProjectId = "test-project", CredentialsJson = Credentials() }, false, default);
|
||||
Assert.Null(outcome.Error);
|
||||
await channels.DidNotReceiveWithAnyArgs().GetCredentialsAsync(default, default);
|
||||
await firebase.Received(1).CheckAuthorizationAsync(Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("changed-project")]
|
||||
[InlineData("invalid-json")]
|
||||
[InlineData("missing-token")]
|
||||
[InlineData("unknown-channel")]
|
||||
public async Task InvalidInputNeverContactsFirebase(string scenario)
|
||||
{
|
||||
var channels = Substitute.For<IPushChannelRepository>();
|
||||
var firebase = Client();
|
||||
var id = Guid.NewGuid();
|
||||
if (scenario != "unknown-channel") channels.GetByIdAsync(id, Arg.Any<CancellationToken>()).Returns(new PushChannel
|
||||
{ Id = id, ServiceId = Guid.NewGuid(), FirebaseProjectId = "test-project" });
|
||||
channels.GetCredentialsAsync(id, Arg.Any<CancellationToken>()).Returns(Credentials());
|
||||
var settings = new PushChannelTestSettings
|
||||
{
|
||||
ChannelId = id, FirebaseProjectId = scenario == "changed-project" ? "other-project" : "test-project",
|
||||
CredentialsJson = scenario == "invalid-json" ? "private-secret-do-not-echo" : null
|
||||
};
|
||||
var outcome = await new PushChannelTestService(channels, firebase).TestAsync(settings, scenario == "missing-token", default);
|
||||
Assert.NotNull(outcome.Error);
|
||||
Assert.DoesNotContain("private-secret-do-not-echo", outcome.Error);
|
||||
await firebase.DidNotReceiveWithAnyArgs().CheckAuthorizationAsync(default!, default);
|
||||
await firebase.DidNotReceiveWithAnyArgs().SendTestAsync(default!, default!, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task RetryableProviderFailureReturnsSafeUnconfirmedResult()
|
||||
{
|
||||
var channels = Substitute.For<IPushChannelRepository>();
|
||||
var firebase = Client();
|
||||
firebase.SendTestAsync(Arg.Any<string>(), Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromException<PushDeliveryOutcome>(new PushRetryException(TimeSpan.FromSeconds(30))));
|
||||
var outcome = await new PushChannelTestService(channels, firebase).TestAsync(new PushChannelTestSettings
|
||||
{ FirebaseProjectId = "test-project", CredentialsJson = Credentials(), RecipientToken = "test-token" }, true, default);
|
||||
Assert.Contains("acceptance was not confirmed", outcome.Error);
|
||||
}
|
||||
|
||||
private static IFirebasePushClient Client()
|
||||
{
|
||||
var firebase = Substitute.For<IFirebasePushClient>();
|
||||
firebase.CheckAuthorizationAsync(Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PushDeliveryOutcome(null));
|
||||
firebase.SendTestAsync(Arg.Any<string>(), Arg.Any<FirebaseServiceAccountCredentials>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PushDeliveryOutcome(null));
|
||||
return firebase;
|
||||
}
|
||||
|
||||
private static string Credentials()
|
||||
{
|
||||
using var rsa = RSA.Create(2048);
|
||||
return JsonSerializer.Serialize(new
|
||||
{ type = "service_account", project_id = "test-project", client_email = "test@example.invalid", private_key = rsa.ExportPkcs8PrivateKeyPem() });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
namespace HrynCo.NotificationService.Services.Tests.Push;
|
||||
|
||||
using FirebaseAdmin.Messaging;
|
||||
using HrynCo.NotificationService.Contracts.Messages;
|
||||
using HrynCo.NotificationService.DAL.Abstract.Push;
|
||||
using HrynCo.NotificationService.Worker.Services.Push;
|
||||
using Hrynco.RabbitMq;
|
||||
using NSubstitute;
|
||||
|
||||
public sealed class PushDeliveryTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(MessagingErrorCode.Unregistered, true)]
|
||||
[InlineData(MessagingErrorCode.SenderIdMismatch, false)]
|
||||
[InlineData(MessagingErrorCode.InvalidArgument, false)]
|
||||
public void OnlyUnregisteredInvalidatesTheInstallation(MessagingErrorCode code, bool invalid)
|
||||
{
|
||||
var result = FirebasePushClient.Classify(code);
|
||||
Assert.Equal(invalid, result.InvalidRegistration);
|
||||
Assert.NotNull(result.Error);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(MessagingErrorCode.Internal)]
|
||||
[InlineData(MessagingErrorCode.Unavailable)]
|
||||
[InlineData(MessagingErrorCode.QuotaExceeded)]
|
||||
public void TransientFailuresPreserveRetryAfter(MessagingErrorCode code)
|
||||
{
|
||||
var exception = Assert.Throws<PushRetryException>(() => FirebasePushClient.Classify(code, TimeSpan.FromMinutes(3)));
|
||||
Assert.Equal(TimeSpan.FromMinutes(3), exception.RetryAfter);
|
||||
Assert.True(PushDeliveryService.RetryDelay(0, exception.RetryAfter) >= TimeSpan.FromMinutes(3));
|
||||
Assert.True(PushDeliveryService.RetryDelay(1, TimeSpan.Zero) >= TimeSpan.FromMinutes(2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ExhaustedReplyRetriesReplaySavedSuccessWithoutSendingAgain()
|
||||
{
|
||||
var repository = Substitute.For<IPushDeliveryRepository>();
|
||||
var sender = Substitute.For<IPushSender>();
|
||||
var publisher = Substitute.For<IRabbitMqPublisher>();
|
||||
repository.DeliverOnceAsync(Arg.Any<Guid>(), Arg.Any<string>(),
|
||||
Arg.Any<Func<CancellationToken, Task<PushDeliveryOutcome>>>(), Arg.Any<CancellationToken>())
|
||||
.Returns(new PushDeliveryOutcome(null));
|
||||
var service = new PushDeliveryService(repository, sender, publisher);
|
||||
var id = Guid.NewGuid();
|
||||
var message = new SendPushMessage
|
||||
{
|
||||
CorrelationContext = new CorrelationContext { CorrelationId = id.ToString(), ReplyTo = "results" },
|
||||
Data = new SendPushMessageData
|
||||
{
|
||||
RegistrationId = Guid.NewGuid(), BindingId = Guid.NewGuid(), UserId = Guid.NewGuid(),
|
||||
ExpiresAt = DateTimeOffset.UtcNow.AddMinutes(10), ServiceName = "Invemory", Title = "Invemory", Body = "Test", TokenHash = new string('a', 64)
|
||||
}
|
||||
};
|
||||
await service.CompleteFailureAsync(id, message, default);
|
||||
await publisher.Received(1).PublishAsync("results",
|
||||
Arg.Is<NotificationResultMessage>(x => x.Data.IsSuccess && x.Data.PushBindingId == message.Data.BindingId), default);
|
||||
await sender.DidNotReceive().SendAsync(Arg.Any<Guid>(), Arg.Any<SendPushMessageData>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
namespace HrynCo.NotificationService.Services.Tests.Push;
|
||||
|
||||
using HrynCo.NotificationService.Contracts.Messages;
|
||||
using HrynCo.NotificationService.Worker;
|
||||
using HrynCo.NotificationService.Worker.Services.Push;
|
||||
using Hrynco.RabbitMq;
|
||||
|
||||
public sealed class SendPushDeliveryValidatorTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData("Notification.SendPush.v1", true)]
|
||||
[InlineData("Notification.SendEmail.v1", false)]
|
||||
[InlineData("Notification.SendPush.v2", false)]
|
||||
[InlineData("notification.sendpush.v1", false)]
|
||||
[InlineData("Notification.SendPush.v1 ", false)]
|
||||
[InlineData("SendPushV1", false)]
|
||||
[InlineData("1", false)]
|
||||
[InlineData("", false)]
|
||||
[InlineData(null, false)]
|
||||
public void AcceptsOnlyTheExistingPushWireContract(string? wireType, bool expected)
|
||||
{
|
||||
var id = Guid.NewGuid().ToString("D");
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var message = new SendPushMessage
|
||||
{
|
||||
CorrelationContext = new CorrelationContext { CorrelationId = id },
|
||||
Data = new SendPushMessageData
|
||||
{
|
||||
RegistrationId = Guid.NewGuid(), BindingId = Guid.NewGuid(), UserId = Guid.NewGuid(),
|
||||
ServiceName = "Invemory", Title = "Test", Body = "Test", Token = "test-device-token",
|
||||
TokenHash = new string('a', 64), ExpiresAt = now.AddMinutes(10)
|
||||
}
|
||||
};
|
||||
var context = new RabbitMqMessageContext
|
||||
{
|
||||
QueueName = "notification.send-push", RoutingKey = "notification.send-push", Exchange = "",
|
||||
MessageId = id, MessageType = wireType
|
||||
};
|
||||
|
||||
Assert.Equal(expected, SendPushDeliveryValidator.IsValid(message, context));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user