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(); var firebase = Client(); var id = Guid.NewGuid(); channels.GetByIdAsync(id, Arg.Any()).Returns(new PushChannel { Id = id, ServiceId = Guid.NewGuid(), FirebaseProjectId = "test-project", IsActive = false }); channels.GetCredentialsAsync(id, Arg.Any()).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(), Arg.Any()); await firebase.Received(send ? 0 : 1).CheckAuthorizationAsync(Arg.Any(), Arg.Any()); await channels.DidNotReceiveWithAnyArgs().SaveAsync(default!, default!, default, default); } [Fact] public async Task UnsavedCredentialsOverrideSavedConfiguration() { var channels = Substitute.For(); 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(), Arg.Any()); } [Theory] [InlineData("changed-project")] [InlineData("invalid-json")] [InlineData("missing-token")] [InlineData("unknown-channel")] public async Task InvalidInputNeverContactsFirebase(string scenario) { var channels = Substitute.For(); var firebase = Client(); var id = Guid.NewGuid(); if (scenario != "unknown-channel") channels.GetByIdAsync(id, Arg.Any()).Returns(new PushChannel { Id = id, ServiceId = Guid.NewGuid(), FirebaseProjectId = "test-project" }); channels.GetCredentialsAsync(id, Arg.Any()).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(); var firebase = Client(); firebase.SendTestAsync(Arg.Any(), Arg.Any(), Arg.Any()) .Returns(Task.FromException(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(); firebase.CheckAuthorizationAsync(Arg.Any(), Arg.Any()) .Returns(new PushDeliveryOutcome(null)); firebase.SendTestAsync(Arg.Any(), Arg.Any(), Arg.Any()) .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() }); } }