From 17da1c496eea8621af3f24a74a6db2366d3a4d73 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:53:55 -0500 Subject: [PATCH] Azure Queue Notifications Tests --- .../AzureQueuePushNotificationService.cs | 8 +- .../AzureQueuePushNotificationServiceTests.cs | 775 ++++++++++++++++++ 2 files changed, 780 insertions(+), 3 deletions(-) diff --git a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs index e61dd15f0d..5a6798d749 100644 --- a/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs +++ b/src/Core/Platform/Push/Services/AzureQueuePushNotificationService.cs @@ -22,17 +22,19 @@ public class AzureQueuePushNotificationService : IPushNotificationService private readonly QueueClient _queueClient; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IGlobalSettings _globalSettings; + private readonly TimeProvider _timeProvider; public AzureQueuePushNotificationService( [FromKeyedServices("notifications")] QueueClient queueClient, IHttpContextAccessor httpContextAccessor, IGlobalSettings globalSettings, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) { _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; _globalSettings = globalSettings; - + _timeProvider = timeProvider; if (globalSettings.Installation.Id == Guid.Empty) { logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); @@ -140,7 +142,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false) { - var message = new UserPushNotification { UserId = userId, Date = DateTime.UtcNow }; + var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime }; await SendMessageAsync(type, message, excludeCurrentContext); } diff --git a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs index 3025197c66..e57544b48a 100644 --- a/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/AzureQueuePushNotificationServiceTests.cs @@ -1,18 +1,27 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; using Azure.Storage.Queues; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; using Bit.Core.Platform.Push.Internal; using Bit.Core.Settings; using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.CurrentContextFixtures; using Bit.Core.Test.NotificationCenter.AutoFixture; +using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; @@ -22,6 +31,17 @@ namespace Bit.Core.Test.Platform.Push.Services; [SutProviderCustomize] public class AzureQueuePushNotificationServiceTests { + private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3"); + private static readonly string _deviceIdentifier = "test_device_identifier"; + private readonly FakeTimeProvider _fakeTimeProvider; + private readonly Core.Settings.GlobalSettings _globalSettings = new(); + + public AzureQueuePushNotificationServiceTests() + { + _fakeTimeProvider = new(); + _fakeTimeProvider.SetUtcNow(DateTime.UtcNow); + } + [Theory] [BitAutoData] [NotificationCustomize] @@ -112,6 +132,761 @@ public class AzureQueuePushNotificationServiceTests deviceIdentifier.ToString()))); } + [Theory] + [InlineData("6a5bbe1b-cf16-49a6-965f-5c2eac56a531", null)] + [InlineData(null, "b9a3fcb4-2447-45c1-aad2-24de43c88c44")] + public async Task PushSyncCipherCreateAsync_SendsExpectedResponse(string? userId, string? organizationId) + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = userId != null ? Guid.Parse(userId) : null, + OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 1, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = cipher.OrganizationId, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + if (!cipher.UserId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("UserId"); + } + + if (!cipher.OrganizationId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("OrganizationId"); + expectedPayload["Payload"]!.AsObject().Remove("CollectionIds"); + } + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]), + expectedPayload + ); + } + + [Theory] + [InlineData("6a5bbe1b-cf16-49a6-965f-5c2eac56a531", null)] + [InlineData(null, "b9a3fcb4-2447-45c1-aad2-24de43c88c44")] + public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse(string? userId, string? organizationId) + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = userId != null ? Guid.Parse(userId) : null, + OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 0, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = cipher.OrganizationId, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + if (!cipher.UserId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("UserId"); + } + + if (!cipher.OrganizationId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("OrganizationId"); + expectedPayload["Payload"]!.AsObject().Remove("CollectionIds"); + } + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncCipherDeleteAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + OrganizationId = null, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 2, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherDeleteAsync(cipher), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncFolderCreateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 7, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderCreateAsync(folder), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 8, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderUpdateAsync(folder), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 3, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderDeleteAsync(folder), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncCiphersAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 4, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCiphersAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncVaultAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 5, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncVaultAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrganizationsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 17, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationsAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrgKeysAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 6, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrgKeysAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncSettingsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 10, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSettingsAsync(userId), + expectedPayload + ); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 11, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + if (excludeCurrentContext) + { + expectedPayload["ContextId"] = _deviceIdentifier; + } + + await VerifyNotificationAsync( + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncSendCreateAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 12, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendCreateAsync(send), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncSendUpdateAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 13, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendUpdateAsync(send), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncSendDeleteAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 14, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendDeleteAsync(send), + expectedPayload + ); + } + + [Fact] + public async Task PushAuthRequestAsync_SendsExpectedResponse() + { + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 15, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestAsync(authRequest), + expectedPayload + ); + } + + [Fact] + public async Task PushAuthRequestResponseAsync_SendsExpectedResponse() + { + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 16, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ContextId"] = _deviceIdentifier, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestResponseAsync(authRequest), + expectedPayload + ); + } + + [Theory] + [InlineData(true, null, null)] + [InlineData(false, "e8e08ce8-8a26-4a65-913a-ba1d8c478b2f", null)] + [InlineData(false, null, "2f53ee32-edf9-4169-b276-760fe92e03bf")] + public async Task PushNotificationAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId) + { + var notification = new Notification + { + Id = Guid.NewGuid(), + Priority = Priority.High, + Global = global, + ClientType = ClientType.All, + UserId = userId != null ? Guid.Parse(userId) : null, + OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + Title = "My Title", + Body = "My Body", + CreationDate = DateTime.UtcNow.AddDays(-1), + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 20, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = _globalSettings.Installation.Id, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + if (!global) + { + expectedPayload["Payload"]!.AsObject().Remove("InstallationId"); + } + + if (!notification.UserId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("UserId"); + } + + if (!notification.OrganizationId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("OrganizationId"); + } + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationAsync(notification), + expectedPayload + ); + } + + [Theory] + [InlineData(true, null, null)] + [InlineData(false, "e8e08ce8-8a26-4a65-913a-ba1d8c478b2f", null)] + [InlineData(false, null, "2f53ee32-edf9-4169-b276-760fe92e03bf")] + public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, string? userId, string? organizationId) + { + var notification = new Notification + { + Id = Guid.NewGuid(), + Priority = Priority.High, + Global = global, + ClientType = ClientType.All, + UserId = userId != null ? Guid.Parse(userId) : null, + OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + Title = "My Title", + Body = "My Body", + CreationDate = DateTime.UtcNow.AddDays(-1), + RevisionDate = DateTime.UtcNow, + }; + + var notificationStatus = new NotificationStatus + { + ReadDate = DateTime.UtcNow, + DeletedDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 21, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = _globalSettings.Installation.Id, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = notificationStatus.ReadDate, + ["DeletedDate"] = notificationStatus.DeletedDate, + }, + ["ContextId"] = _deviceIdentifier, + }; + + if (!global) + { + expectedPayload["Payload"]!.AsObject().Remove("InstallationId"); + } + + if (!notification.UserId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("UserId"); + } + + if (!notification.OrganizationId.HasValue) + { + expectedPayload["Payload"]!.AsObject().Remove("OrganizationId"); + } + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + Enabled = true, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 18, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["Enabled"] = organization.Enabled, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationStatusAsync(organization), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + Enabled = true, + LimitCollectionCreation = true, + LimitCollectionDeletion = true, + LimitItemDeletion = true, + }; + + var expectedPayload = new JsonObject + { + ["Type"] = 19, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["LimitCollectionCreation"] = organization.LimitCollectionCreation, + ["LimitCollectionDeletion"] = organization.LimitCollectionDeletion, + ["LimitItemDeletion"] = organization.LimitItemDeletion, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization), + expectedPayload + ); + } + + [Fact] + public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["Type"] = 22, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushPendingSecurityTasksAsync(userId), + expectedPayload + ); + } + + // [Fact] + // public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException() + // { + // await Assert.ThrowsAsync( + // async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new {}, null) + // ); + // } + + // [Fact] + // public async Task SendPayloadToUserAsync_ThrowsNotImplementedException() + // { + // await Assert.ThrowsAsync( + // async () => await _sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new {}, null) + // ); + // } + + // [Fact] + // public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException() + // { + // await Assert.ThrowsAsync( + // async () => await _sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new {}, null) + // ); + // } + + private async Task VerifyNotificationAsync(Func test, JsonNode expectedMessage) + { + var queueClient = Substitute.For(); + + var httpContextAccessor = Substitute.For(); + + var httpContext = new DefaultHttpContext(); + + var serviceCollection = new ServiceCollection(); + var currentContext = Substitute.For(); + currentContext.DeviceIdentifier = _deviceIdentifier; + serviceCollection.AddSingleton(currentContext); + + httpContext.RequestServices = serviceCollection.BuildServiceProvider(); + + httpContextAccessor.HttpContext + .Returns(httpContext); + + var globalSettings = new Core.Settings.GlobalSettings(); + + var sut = new AzureQueuePushNotificationService( + queueClient, + httpContextAccessor, + globalSettings, + NullLogger.Instance, + _fakeTimeProvider + ); + + await test(sut); + + // Hoist equality checker outside the expression so that we + // can more easily place a breakpoint + var checkEquality = (string actual) => + { + var actualNode = JsonNode.Parse(actual); + return JsonNode.DeepEquals(actualNode, expectedMessage); + }; + + await queueClient + .Received(1) + .SendMessageAsync(Arg.Is((actual) => checkEquality(actual))); + } + private static bool MatchMessage(PushType pushType, string message, IEquatable expectedPayloadEquatable, string contextId) {