From 69c6436fd35c317184e92b6255b84314b879c801 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 4 Mar 2025 11:28:02 -0500 Subject: [PATCH 1/9] Add RelayPush Notifications Tests --- .../Services/RelayPushNotificationService.cs | 7 +- .../RelayPushNotificationServiceTests.cs | 893 +++++++++++++++++- 2 files changed, 891 insertions(+), 9 deletions(-) diff --git a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs index 53f583532238..d0a9d48faf78 100644 --- a/src/Core/Platform/Push/Services/RelayPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/RelayPushNotificationService.cs @@ -27,13 +27,15 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti private readonly IDeviceRepository _deviceRepository; private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly TimeProvider _timeProvider; public RelayPushNotificationService( IHttpClientFactory httpFactory, IDeviceRepository deviceRepository, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) : base( httpFactory, globalSettings.PushRelayBaseUri, @@ -46,6 +48,7 @@ public RelayPushNotificationService( _deviceRepository = deviceRepository; _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; + _timeProvider = timeProvider; } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -147,7 +150,7 @@ public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = fals 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 SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); } diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index 9ae79f714299..e194c4fe55c9 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -1,45 +1,924 @@ -using Bit.Core.Platform.Push.Internal; +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; +using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; using NSubstitute; +using RichardSzalay.MockHttp; using Xunit; namespace Bit.Core.Test.Platform.Push.Services; public class RelayPushNotificationServiceTests { + private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3"); + private static readonly string _deviceIdentifier = "test_device_identifier"; + private readonly RelayPushNotificationService _sut; + private readonly MockHttpMessageHandler _mockPushClient = new(); + private readonly MockHttpMessageHandler _mockIdentityClient = new(); + private readonly IHttpClientFactory _httpFactory; private readonly IDeviceRepository _deviceRepository; private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; + private readonly FakeTimeProvider _fakeTimeProvider; public RelayPushNotificationServiceTests() { _httpFactory = Substitute.For(); + + // Mock HttpClient + _httpFactory.CreateClient("client") + .Returns(new HttpClient(_mockPushClient)); + + _httpFactory.CreateClient("identity") + .Returns(new HttpClient(_mockIdentityClient)); + _deviceRepository = Substitute.For(); _globalSettings = new GlobalSettings(); _httpContextAccessor = Substitute.For(); _logger = Substitute.For>(); + _globalSettings.PushRelayBaseUri = "https://localhost:7777"; + _globalSettings.Installation.Id = Guid.Parse("478c608a-99fd-452a-94f0-af271654e6ee"); + _globalSettings.Installation.IdentityUri = "https://localhost:8888"; + + _fakeTimeProvider = new FakeTimeProvider(); + + _fakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow); + _sut = new RelayPushNotificationService( _httpFactory, _deviceRepository, _globalSettings, _httpContextAccessor, - _logger + _logger, + _fakeTimeProvider + ); + } + + [Fact] + public async Task PushSyncCipherCreateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + OrganizationId = null, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 1, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + // Currently CollectionIds are not passed along from the method signature + // to the request body. + ["CollectionIds"] = null, + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + OrganizationId = null, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 0, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + // Currently CollectionIds are not passed along from the method signature + // to the request body. + ["CollectionIds"] = null, + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 2, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["CollectionIds"] = null, + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = folder.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 7, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = folder.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 8, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = folder.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 3, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderDeleteAsync(folder), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncCiphersAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 4, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCiphersAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncVaultAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 5, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncVaultAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrganizationsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 17, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationsAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncOrgKeysAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 6, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrgKeysAsync(userId), + expectedPayload + ); + } + + [Fact] + public async Task PushSyncSettingsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 10, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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(); + + JsonNode identifier = excludeCurrentContext ? _deviceIdentifier : null; + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = identifier, + ["Type"] = 11, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = send.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 12, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = send.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 13, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = send.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 14, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = authRequest.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 15, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = authRequest.UserId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 16, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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, + }; + + JsonNode installationId = global ? _globalSettings.Installation.Id : null; + + var expectedPayload = new JsonObject + { + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 20, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = null, + ["DeletedDate"] = null, + }, + ["ClientType"] = 0, + ["InstallationId"] = installationId?.DeepClone(), + }; + + 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, + }; + + JsonNode installationId = global ? _globalSettings.Installation.Id : null; + + var expectedPayload = new JsonObject + { + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["DeviceId"] = _deviceId, + ["Identifier"] = _deviceIdentifier, + ["Type"] = 21, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = notificationStatus.ReadDate, + ["DeletedDate"] = notificationStatus.DeletedDate, + }, + ["ClientType"] = 0, + ["InstallationId"] = installationId?.DeepClone(), + }; + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus), + expectedPayload ); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + [Fact] + public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + Enabled = true, + }; + + var expectedPayload = new JsonObject + { + ["UserId"] = null, + ["OrganizationId"] = organization.Id, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 18, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["Enabled"] = organization.Enabled, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 + { + ["UserId"] = null, + ["OrganizationId"] = organization.Id, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 19, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["LimitCollectionCreation"] = organization.LimitCollectionCreation, + ["LimitCollectionDeletion"] = organization.LimitCollectionDeletion, + ["LimitItemDeletion"] = organization.LimitItemDeletion, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization), + expectedPayload + ); + } + + [Fact] + public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["OrganizationId"] = null, + ["DeviceId"] = _deviceId, + ["Identifier"] = null, + ["Type"] = 22, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ClientType"] = null, + ["InstallationId"] = null, + }; + + 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 expectedRequestBody) + { + 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); + + _deviceRepository.GetByIdentifierAsync(_deviceIdentifier) + .Returns(new Device + { + Id = _deviceId, + }); + + var connectTokenRequest = _mockIdentityClient + .Expect(HttpMethod.Post, "https://localhost:8888/connect/token") + .Respond(HttpStatusCode.OK, JsonContent.Create(new + { + access_token = CreateAccessToken(DateTime.UtcNow.AddDays(1)), + })); + + var pushSendRequest = _mockPushClient + .Expect(HttpMethod.Post, "https://localhost:7777/push/send") + .With(request => + { + if (request.Content is not JsonContent jsonContent) + { + return false; + } + + // TODO: What options? + var actualString = JsonSerializer.Serialize(jsonContent.Value); + var actualNode = JsonNode.Parse(actualString); + + if (!JsonNode.DeepEquals(actualNode, expectedRequestBody)) + { + Assert.Equal(expectedRequestBody.ToJsonString(), actualNode.ToJsonString()); + return false; + } + + return true; + }) + .Respond(HttpStatusCode.OK); + + await test(_sut); + + Assert.Equal(1, _mockPushClient.GetMatchCount(pushSendRequest)); + } + + private static string CreateAccessToken(DateTime expirationTime) { - Assert.NotNull(_sut); + var tokenHandler = new JwtSecurityTokenHandler(); + var token = new JwtSecurityToken(expires: expirationTime); + return tokenHandler.WriteToken(token); } } From 1e13250292410b2972fe3385b320f4928c39d046 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Tue, 4 Mar 2025 15:53:31 -0500 Subject: [PATCH 2/9] Nullable Test Fixup --- .../RelayPushNotificationServiceTests.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index e194c4fe55c9..b0756815e164 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -1,4 +1,6 @@ -using System.IdentityModel.Tokens.Jwt; +#nullable enable + +using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Json; using System.Text.Json; @@ -441,7 +443,7 @@ public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentConte { var userId = Guid.NewGuid(); - JsonNode identifier = excludeCurrentContext ? _deviceIdentifier : null; + JsonNode? identifier = excludeCurrentContext ? _deviceIdentifier : null; var expectedPayload = new JsonObject { @@ -646,7 +648,7 @@ public async Task PushNotificationAsync_SendsExpectedResponse(bool global, strin RevisionDate = DateTime.UtcNow, }; - JsonNode installationId = global ? _globalSettings.Installation.Id : null; + JsonNode? installationId = global ? _globalSettings.Installation.Id : null; var expectedPayload = new JsonObject { @@ -707,7 +709,7 @@ public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, DeletedDate = DateTime.UtcNow, }; - JsonNode installationId = global ? _globalSettings.Installation.Id : null; + JsonNode? installationId = global ? _globalSettings.Installation.Id : null; var expectedPayload = new JsonObject { @@ -900,13 +902,7 @@ private async Task VerifyNotificationAsync(Func Date: Tue, 4 Mar 2025 15:53:55 -0500 Subject: [PATCH 3/9] 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 e61dd15f0def..5a6798d749dd 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 async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = fals 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 3025197c66f2..e57544b48a6c 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 @@ await sutProvider.GetDependency().Received(1) 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) { From 1d65e69200c8e7dbebecc434a4099556d534991c Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 6 Mar 2025 08:09:31 -0500 Subject: [PATCH 4/9] NotificationsHub Push Tests --- .../NotificationHubPushNotificationService.cs | 8 +- ...ficationHubPushNotificationServiceTests.cs | 635 ++++++++++++++++++ 2 files changed, 640 insertions(+), 3 deletions(-) diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index a28b21f4652e..c86ff20c9a67 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -32,20 +32,22 @@ public class NotificationHubPushNotificationService : IPushNotificationService private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; private readonly IGlobalSettings _globalSettings; + private readonly TimeProvider _timeProvider; public NotificationHubPushNotificationService( IInstallationDeviceRepository installationDeviceRepository, INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, ILogger logger, - IGlobalSettings globalSettings) + IGlobalSettings globalSettings, + TimeProvider timeProvider) { _installationDeviceRepository = installationDeviceRepository; _httpContextAccessor = httpContextAccessor; _notificationHubPool = notificationHubPool; _logger = logger; _globalSettings = globalSettings; - + _timeProvider = timeProvider; if (globalSettings.Installation.Id == Guid.Empty) { logger.LogWarning("Installation ID is not set. Push notifications for installations will not work."); @@ -152,7 +154,7 @@ public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = fals 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 SendPayloadToUserAsync(userId, type, message, excludeCurrentContext); } diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index 831d04822403..f4779b497879 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -1,15 +1,25 @@ #nullable enable using System.Text.Json; +using System.Text.Json.Nodes; +using Bit.Core.Auth.Entities; +using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; using Bit.Core.NotificationHub; using Bit.Core.Repositories; using Bit.Core.Settings; 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; @@ -19,6 +29,10 @@ namespace Bit.Core.Test.NotificationHub; [NotificationStatusCustomize] public class NotificationHubPushNotificationServiceTests { + private static readonly string _deviceIdentifier = "test_device_identifier"; + private static readonly DateTime _now = DateTime.UtcNow; + private static readonly Guid _installationId = Guid.Parse("da73177b-513f-4444-b582-595c890e1022"); + [Theory] [BitAutoData] [NotificationCustomize] @@ -496,6 +510,627 @@ await sutProvider.GetDependency() .UpsertAsync(Arg.Any()); } + [Fact] + public async Task PushSyncCipherCreateAsync_SendExpectedData() + { + var collectionId = Guid.NewGuid(); + + var userId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = cipher.OrganizationId, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]), + PushType.SyncCipherCreate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncCipherUpdateAsync_SendExpectedData() + { + var collectionId = Guid.NewGuid(); + + var userId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = cipher.OrganizationId, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]), + PushType.SyncCipherUpdate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncCipherDeleteAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = cipher.OrganizationId, + ["CollectionIds"] = null, + ["RevisionDate"] = cipher.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherDeleteAsync(cipher), + PushType.SyncLoginDelete, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncFolderCreateAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderCreateAsync(folder), + PushType.SyncFolderCreate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncFolderUpdateAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderUpdateAsync(folder), + PushType.SyncFolderUpdate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncSendCreateAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var send = new Send + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendCreateAsync(send), + PushType.SyncSendCreate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushAuthRequestAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = userId, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestAsync(authRequest), + PushType.AuthRequest, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushAuthRequestResponseAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = userId, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestResponseAsync(authRequest), + PushType.AuthRequestResponse, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncSendUpdateAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var send = new Send + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendUpdateAsync(send), + PushType.SyncSendUpdate, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncSendDeleteAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var send = new Send + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendDeleteAsync(send), + PushType.SyncSendDelete, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [Fact] + public async Task PushSyncCiphersAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCiphersAsync(userId), + PushType.SyncCiphers, + expectedPayload, + $"(template:payload_userId:{userId})" + ); + } + + [Fact] + public async Task PushSyncVaultAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncVaultAsync(userId), + PushType.SyncVault, + expectedPayload, + $"(template:payload_userId:{userId})" + ); + } + + [Fact] + public async Task PushSyncOrganizationsAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationsAsync(userId), + PushType.SyncOrganizations, + expectedPayload, + $"(template:payload_userId:{userId})" + ); + } + + [Fact] + public async Task PushSyncOrgKeysAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrgKeysAsync(userId), + PushType.SyncOrgKeys, + expectedPayload, + $"(template:payload_userId:{userId})" + ); + } + + [Fact] + public async Task PushSyncSettingsAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSettingsAsync(userId), + PushType.SyncSettings, + expectedPayload, + $"(template:payload_userId:{userId})" + ); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PushLogOutAsync_SendExpectedData(bool excludeCurrentContext) + { + var userId = Guid.NewGuid(); + + var expectedPayload = new JsonObject + { + ["UserId"] = userId, + ["Date"] = _now, + }; + + var expectedTag = excludeCurrentContext + ? $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + : $"(template:payload_userId:{userId})"; + + await VerifyNotificationAsync( + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + PushType.LogOut, + expectedPayload, + expectedTag + ); + } + + [Fact] + public async Task PushSyncFolderDeleteAsync_SendExpectedData() + { + var userId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = userId, + RevisionDate = DateTime.UtcNow, + }; + + var expectedPayload = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderDeleteAsync(folder), + PushType.SyncFolderDelete, + expectedPayload, + $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})" + ); + } + + [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_SendExpectedData(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, + }; + + JsonNode? installationId = global ? _installationId : null; + + var expectedPayload = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = null, + ["DeletedDate"] = null, + }; + + string expectedTag; + + if (global) + { + expectedTag = $"(template:payload && installationId:{_installationId} && !deviceIdentifier:{_deviceIdentifier})"; + } + else if (notification.OrganizationId.HasValue) + { + expectedTag = "(template:payload && organizationId:2f53ee32-edf9-4169-b276-760fe92e03bf && !deviceIdentifier:test_device_identifier)"; + } + else + { + expectedTag = $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})"; + } + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationAsync(notification), + PushType.Notification, + expectedPayload, + expectedTag + ); + } + + [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_SendExpectedData(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.AddDays(-1), + DeletedDate = DateTime.UtcNow, + }; + + JsonNode? installationId = global ? _installationId : null; + + var expectedPayload = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = notificationStatus.ReadDate, + ["DeletedDate"] = notificationStatus.DeletedDate, + }; + + string expectedTag; + + if (global) + { + expectedTag = $"(template:payload && installationId:{_installationId} && !deviceIdentifier:{_deviceIdentifier})"; + } + else if (notification.OrganizationId.HasValue) + { + expectedTag = "(template:payload && organizationId:2f53ee32-edf9-4169-b276-760fe92e03bf && !deviceIdentifier:test_device_identifier)"; + } + else + { + expectedTag = $"(template:payload_userId:{userId} && !deviceIdentifier:{_deviceIdentifier})"; + } + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus), + PushType.NotificationStatus, + expectedPayload, + expectedTag + ); + } + + private async Task VerifyNotificationAsync(Func test, + PushType type, JsonNode expectedPayload, string tag) + { + var installationDeviceRepository = Substitute.For(); + + var notificationHubPool = Substitute.For(); + + var notificationHubProxy = Substitute.For(); + + notificationHubPool.AllClients + .Returns(notificationHubProxy); + + 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(); + globalSettings.Installation.Id = _installationId; + + var fakeTimeProvider = new FakeTimeProvider(); + + fakeTimeProvider.SetUtcNow(_now); + + var sut = new NotificationHubPushNotificationService( + installationDeviceRepository, + notificationHubPool, + httpContextAccessor, + NullLogger.Instance, + globalSettings, + fakeTimeProvider + ); + + // Act + await test(sut); + + // Assert + var calls = notificationHubProxy.ReceivedCalls(); + var methodInfo = typeof(INotificationHubProxy).GetMethod(nameof(INotificationHubProxy.SendTemplateNotificationAsync)); + var call = Assert.Single(calls, c => c.GetMethodInfo() == methodInfo); + + var arguments = call.GetArguments(); + + var dictionaryArg = (Dictionary)arguments[0]!; + var tagArg = (string)arguments[1]!; + + Assert.Equal(2, dictionaryArg.Count); + Assert.True(dictionaryArg.TryGetValue("type", out var typeString)); + Assert.True(byte.TryParse(typeString, out var typeByte)); + Assert.Equal(type, (PushType)typeByte); + + Assert.True(dictionaryArg.TryGetValue("payload", out var payloadString)); + var actualPayloadNode = JsonNode.Parse(payloadString); + + Assert.True(JsonNode.DeepEquals(expectedPayload, actualPayloadNode)); + + Assert.Equal(tag, tagArg); + } + private static NotificationPushNotification ToNotificationPushNotification(Notification notification, NotificationStatus? notificationStatus, Guid? installationId) => new() From 40c48510a38fdee2bc60c272de9673948d1c7fdf Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:02:48 -0500 Subject: [PATCH 5/9] Make common base for API based notifications --- ...NotificationsApiPushNotificationService.cs | 7 +- ...icationsApiPushNotificationServiceTests.cs | 398 +++++++++++- .../Platform/Push/Services/PushTestBase.cs | 491 ++++++++++++++ .../RelayPushNotificationServiceTests.cs | 609 ++++-------------- 4 files changed, 980 insertions(+), 525 deletions(-) create mode 100644 test/Core.Test/Platform/Push/Services/PushTestBase.cs diff --git a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs index 53a0de9a275b..b7ba0c275bfd 100644 --- a/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs +++ b/src/Core/Platform/Push/Services/NotificationsApiPushNotificationService.cs @@ -24,12 +24,14 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService { private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly TimeProvider _timeProvider; public NotificationsApiPushNotificationService( IHttpClientFactory httpFactory, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger, + TimeProvider timeProvider) : base( httpFactory, globalSettings.BaseServiceUri.InternalNotifications, @@ -41,6 +43,7 @@ public NotificationsApiPushNotificationService( { _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; + _timeProvider = timeProvider; } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -148,7 +151,7 @@ private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrent var message = new UserPushNotification { UserId = userId, - Date = DateTime.UtcNow + Date = _timeProvider.GetUtcNow().UtcDateTime, }; await SendMessageAsync(type, message, excludeCurrentContext); diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index 07f348a5ba84..587afbbd2a13 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -1,41 +1,383 @@ -using Bit.Core.Platform.Push; -using Bit.Core.Settings; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; -using NSubstitute; -using Xunit; +using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Entities; +using Microsoft.Extensions.Logging.Abstractions; namespace Bit.Core.Test.Platform.Push.Services; -public class NotificationsApiPushNotificationServiceTests +public class NotificationsApiPushNotificationServiceTests : PushTestBase { - private readonly NotificationsApiPushNotificationService _sut; - - private readonly IHttpClientFactory _httpFactory; - private readonly GlobalSettings _globalSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILogger _logger; - public NotificationsApiPushNotificationServiceTests() { - _httpFactory = Substitute.For(); - _globalSettings = new GlobalSettings(); - _httpContextAccessor = Substitute.For(); - _logger = Substitute.For>(); + GlobalSettings.BaseServiceUri.InternalNotifications = "https://localhost:7777"; + GlobalSettings.BaseServiceUri.InternalIdentity = "https://localhost:8888"; + } - _sut = new NotificationsApiPushNotificationService( - _httpFactory, - _globalSettings, - _httpContextAccessor, - _logger + protected override string ExpectedClientUrl() => "https://localhost:7777/send"; + + protected override IPushNotificationService CreateService() + { + return new NotificationsApiPushNotificationService( + HttpClientFactory, + GlobalSettings, + HttpContextAccessor, + NullLogger.Instance, + FakeTimeProvider ); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionId) + { + return new JsonObject + { + ["Type"] = 1, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId) + { + return new JsonObject + { + ["Type"] = 0, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["CollectionIds"] = new JsonArray(collectionId), + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher) + { + return new JsonObject + { + ["Type"] = 2, + ["Payload"] = new JsonObject + { + ["Id"] = cipher.Id, + ["UserId"] = cipher.UserId, + ["OrganizationId"] = null, + ["CollectionIds"] = null, + ["RevisionDate"] = cipher.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder) + { + return new JsonObject + { + ["Type"] = 7, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder) + { + return new JsonObject + { + ["Type"] = 8, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder) + { + return new JsonObject + { + ["Type"] = 3, + ["Payload"] = new JsonObject + { + ["Id"] = folder.Id, + ["UserId"] = folder.UserId, + ["RevisionDate"] = folder.RevisionDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushSyncCiphersPayload(Guid userId) + { + return new JsonObject + { + ["Type"] = 4, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSyncVaultPayload(Guid userId) + { + return new JsonObject + { + ["Type"] = 5, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId) + { + return new JsonObject + { + ["Type"] = 17, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId) + { + return new JsonObject + { + ["Type"] = 6, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSyncSettingsPayload(Guid userId) + { + return new JsonObject + { + ["Type"] = 10, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) + { + JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null; + + return new JsonObject + { + ["Type"] = 11, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = contextId, + }; + } + + protected override JsonNode GetPushSendCreatePayload(Send send) + { + return new JsonObject + { + ["Type"] = 12, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSendUpdatePayload(Send send) + { + return new JsonObject + { + ["Type"] = 13, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSendDeletePayload(Send send) + { + return new JsonObject + { + ["Type"] = 14, + ["Payload"] = new JsonObject + { + ["Id"] = send.Id, + ["UserId"] = send.UserId, + ["RevisionDate"] = send.RevisionDate, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) + { + return new JsonObject + { + ["Type"] = 15, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest) + { + return new JsonObject + { + ["Type"] = 16, + ["Payload"] = new JsonObject + { + ["Id"] = authRequest.Id, + ["UserId"] = authRequest.UserId, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId) + { + JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; + + return new JsonObject + { + ["Type"] = 20, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = notification.Global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = null, + ["DeletedDate"] = null, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId) + { + JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; + + return new JsonObject + { + ["Type"] = 21, + ["Payload"] = new JsonObject + { + ["Id"] = notification.Id, + ["Priority"] = 3, + ["Global"] = notification.Global, + ["ClientType"] = 0, + ["UserId"] = notification.UserId, + ["OrganizationId"] = notification.OrganizationId, + ["InstallationId"] = installationId, + ["Title"] = notification.Title, + ["Body"] = notification.Body, + ["CreationDate"] = notification.CreationDate, + ["RevisionDate"] = notification.RevisionDate, + ["ReadDate"] = notificationStatus.ReadDate, + ["DeletedDate"] = notificationStatus.DeletedDate, + }, + ["ContextId"] = DeviceIdentifier, + }; + } + + protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization) + { + return new JsonObject + { + ["Type"] = 18, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["Enabled"] = organization.Enabled, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization) + { + return new JsonObject + { + ["Type"] = 19, + ["Payload"] = new JsonObject + { + ["OrganizationId"] = organization.Id, + ["LimitCollectionCreation"] = organization.LimitCollectionCreation, + ["LimitCollectionDeletion"] = organization.LimitCollectionDeletion, + ["LimitItemDeletion"] = organization.LimitItemDeletion, + }, + ["ContextId"] = null, + }; + } + + protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) { - Assert.NotNull(_sut); + return new JsonObject + { + ["Type"] = 22, + ["Payload"] = new JsonObject + { + ["UserId"] = userId, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, + }, + ["ContextId"] = null, + }; } } diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs new file mode 100644 index 000000000000..d9b77f998048 --- /dev/null +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -0,0 +1,491 @@ +// TODO: Move to own file +using System.IdentityModel.Tokens.Jwt; +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.NotificationCenter.Entities; +using Bit.Core.NotificationCenter.Enums; +using Bit.Core.Platform.Push; +using Bit.Core.Settings; +using Bit.Core.Tools.Entities; +using Bit.Core.Vault.Entities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using RichardSzalay.MockHttp; +using Xunit; + +public abstract class PushTestBase +{ + protected static readonly string DeviceIdentifier = "test_device_identifier"; + + protected readonly MockHttpMessageHandler MockClient = new(); + protected readonly MockHttpMessageHandler MockIdentityClient = new(); + + protected readonly IHttpClientFactory HttpClientFactory; + protected readonly GlobalSettings GlobalSettings; + protected readonly IHttpContextAccessor HttpContextAccessor; + protected readonly FakeTimeProvider FakeTimeProvider; + + public PushTestBase() + { + HttpClientFactory = Substitute.For(); + + // Mock HttpClient + HttpClientFactory.CreateClient("client") + .Returns(new HttpClient(MockClient)); + + HttpClientFactory.CreateClient("identity") + .Returns(new HttpClient(MockIdentityClient)); + + GlobalSettings = new GlobalSettings(); + HttpContextAccessor = Substitute.For(); + + FakeTimeProvider = new FakeTimeProvider(); + + FakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow); + } + + protected abstract IPushNotificationService CreateService(); + + protected abstract string ExpectedClientUrl(); + + protected abstract JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionId); + protected abstract JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId); + protected abstract JsonNode GetPushSyncCipherDeletePayload(Cipher cipher); + protected abstract JsonNode GetPushSyncFolderCreatePayload(Folder folder); + protected abstract JsonNode GetPushSyncFolderUpdatePayload(Folder folder); + protected abstract JsonNode GetPushSyncFolderDeletePayload(Folder folder); + protected abstract JsonNode GetPushSyncCiphersPayload(Guid userId); + protected abstract JsonNode GetPushSyncVaultPayload(Guid userId); + protected abstract JsonNode GetPushSyncOrganizationsPayload(Guid userId); + protected abstract JsonNode GetPushSyncOrgKeysPayload(Guid userId); + protected abstract JsonNode GetPushSyncSettingsPayload(Guid userId); + protected abstract JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext); + protected abstract JsonNode GetPushSendCreatePayload(Send send); + protected abstract JsonNode GetPushSendUpdatePayload(Send send); + protected abstract JsonNode GetPushSendDeletePayload(Send send); + protected abstract JsonNode GetPushAuthRequestPayload(AuthRequest authRequest); + protected abstract JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest); + protected abstract JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId); + protected abstract JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId); + protected abstract JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization); + protected abstract JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization); + protected abstract JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId); + + [Fact] + public async Task PushSyncCipherCreateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + OrganizationId = null, + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]), + GetPushSyncCipherCreatePayload(cipher, collectionId) + ); + } + + [Fact] + public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var cipher = new Cipher + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + OrganizationId = null, + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]), + GetPushSyncCipherUpdatePayload(cipher, collectionId) + ); + } + + [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, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCipherDeleteAsync(cipher), + GetPushSyncCipherDeletePayload(cipher) + ); + } + + [Fact] + public async Task PushSyncFolderCreateAsync_SendsExpectedResponse() + { + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderCreateAsync(folder), + GetPushSyncFolderCreatePayload(folder) + ); + } + + [Fact] + public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderUpdateAsync(folder), + GetPushSyncFolderUpdatePayload(folder) + ); + } + + [Fact] + public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse() + { + var collectionId = Guid.NewGuid(); + + var folder = new Folder + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncFolderDeleteAsync(folder), + GetPushSyncFolderDeletePayload(folder) + ); + } + + [Fact] + public async Task PushSyncCiphersAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushSyncCiphersAsync(userId), + GetPushSyncCiphersPayload(userId) + ); + } + + [Fact] + public async Task PushSyncVaultAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushSyncVaultAsync(userId), + GetPushSyncVaultPayload(userId) + ); + } + + [Fact] + public async Task PushSyncOrganizationsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationsAsync(userId), + GetPushSyncOrganizationsPayload(userId) + ); + } + + [Fact] + public async Task PushSyncOrgKeysAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrgKeysAsync(userId), + GetPushSyncOrgKeysPayload(userId) + ); + } + + [Fact] + public async Task PushSyncSettingsAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSettingsAsync(userId), + GetPushSyncSettingsPayload(userId) + ); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), + GetPushLogOutPayload(userId, excludeCurrentContext) + ); + } + + [Fact] + public async Task PushSyncSendCreateAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendCreateAsync(send), + GetPushSendCreatePayload(send) + ); + } + + [Fact] + public async Task PushSyncSendUpdateAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendUpdateAsync(send), + GetPushSendUpdatePayload(send) + ); + } + + [Fact] + public async Task PushSyncSendDeleteAsync_SendsExpectedResponse() + { + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + RevisionDate = DateTime.UtcNow, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncSendDeleteAsync(send), + GetPushSendDeletePayload(send) + ); + } + + [Fact] + public async Task PushAuthRequestAsync_SendsExpectedResponse() + { + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestAsync(authRequest), + GetPushAuthRequestPayload(authRequest) + ); + } + + [Fact] + public async Task PushAuthRequestResponseAsync_SendsExpectedResponse() + { + var authRequest = new AuthRequest + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + }; + + await VerifyNotificationAsync( + async sut => await sut.PushAuthRequestResponseAsync(authRequest), + GetPushAuthRequestResponsePayload(authRequest) + ); + } + + [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, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationAsync(notification), + GetPushNotificationResponsePayload(notification, notification.UserId, notification.OrganizationId) + ); + } + + [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, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus), + GetPushNotificationStatusResponsePayload(notification, notificationStatus, notification.UserId, notification.OrganizationId) + ); + } + + [Fact] + public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + Enabled = true, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationStatusAsync(organization), + GetPushSyncOrganizationStatusResponsePayload(organization) + ); + } + + [Fact] + public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse() + { + var organization = new Organization + { + Id = Guid.NewGuid(), + Enabled = true, + LimitCollectionCreation = true, + LimitCollectionDeletion = true, + LimitItemDeletion = true, + }; + + await VerifyNotificationAsync( + async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization), + GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(organization) + ); + } + + [Fact] + public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + { + var userId = Guid.NewGuid(); + + await VerifyNotificationAsync( + async sut => await sut.PushPendingSecurityTasksAsync(userId), + GetPushPendingSecurityTasksResponsePayload(userId) + ); + } + + private async Task VerifyNotificationAsync( + Func test, + JsonNode expectedRequestBody + ) + { + 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 connectTokenRequest = MockIdentityClient + .Expect(HttpMethod.Post, "https://localhost:8888/connect/token") + .Respond(HttpStatusCode.OK, JsonContent.Create(new + { + access_token = CreateAccessToken(DateTime.UtcNow.AddDays(1)), + })); + + var clientRequest = MockClient + .Expect(HttpMethod.Post, ExpectedClientUrl()) + .With(request => + { + if (request.Content is not JsonContent jsonContent) + { + return false; + } + + // TODO: What options? + var actualString = JsonSerializer.Serialize(jsonContent.Value); + var actualNode = JsonNode.Parse(actualString); + + return JsonNode.DeepEquals(actualNode, expectedRequestBody); + }) + .Respond(HttpStatusCode.OK); + + await test(CreateService()); + + Assert.Equal(1, MockClient.GetMatchCount(clientRequest)); + } + + protected static string CreateAccessToken(DateTime expirationTime) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var token = new JwtSecurityToken(expires: expirationTime); + return tokenHandler.WriteToken(token); + } +} diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index b0756815e164..c554484f98ad 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -1,102 +1,92 @@ #nullable enable -using System.IdentityModel.Tokens.Jwt; -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; using System.Text.Json.Nodes; using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.NotificationCenter.Entities; -using Bit.Core.NotificationCenter.Enums; using Bit.Core.Platform.Push.Internal; using Bit.Core.Repositories; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Time.Testing; using NSubstitute; -using RichardSzalay.MockHttp; using Xunit; namespace Bit.Core.Test.Platform.Push.Services; -public class RelayPushNotificationServiceTests +public class RelayPushNotificationServiceTests : PushTestBase { private static readonly Guid _deviceId = Guid.Parse("c4730f80-caaa-4772-97bd-5c0d23a2baa3"); - private static readonly string _deviceIdentifier = "test_device_identifier"; - - private readonly RelayPushNotificationService _sut; - - private readonly MockHttpMessageHandler _mockPushClient = new(); - private readonly MockHttpMessageHandler _mockIdentityClient = new(); - - private readonly IHttpClientFactory _httpFactory; private readonly IDeviceRepository _deviceRepository; - private readonly GlobalSettings _globalSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILogger _logger; - private readonly FakeTimeProvider _fakeTimeProvider; public RelayPushNotificationServiceTests() { - _httpFactory = Substitute.For(); - - // Mock HttpClient - _httpFactory.CreateClient("client") - .Returns(new HttpClient(_mockPushClient)); - - _httpFactory.CreateClient("identity") - .Returns(new HttpClient(_mockIdentityClient)); - _deviceRepository = Substitute.For(); - _globalSettings = new GlobalSettings(); - _httpContextAccessor = Substitute.For(); - _logger = Substitute.For>(); - - _globalSettings.PushRelayBaseUri = "https://localhost:7777"; - _globalSettings.Installation.Id = Guid.Parse("478c608a-99fd-452a-94f0-af271654e6ee"); - _globalSettings.Installation.IdentityUri = "https://localhost:8888"; - _fakeTimeProvider = new FakeTimeProvider(); + _deviceRepository.GetByIdentifierAsync(DeviceIdentifier) + .Returns(new Device + { + Id = _deviceId, + }); - _fakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow); + GlobalSettings.PushRelayBaseUri = "https://localhost:7777"; + GlobalSettings.Installation.Id = Guid.Parse("478c608a-99fd-452a-94f0-af271654e6ee"); + GlobalSettings.Installation.IdentityUri = "https://localhost:8888"; + } - _sut = new RelayPushNotificationService( - _httpFactory, + protected override RelayPushNotificationService CreateService() + { + return new RelayPushNotificationService( + HttpClientFactory, _deviceRepository, - _globalSettings, - _httpContextAccessor, - _logger, - _fakeTimeProvider + GlobalSettings, + HttpContextAccessor, + NullLogger.Instance, + FakeTimeProvider ); } + protected override string ExpectedClientUrl() => "https://localhost:7777/push/send"; + [Fact] - public async Task PushSyncCipherCreateAsync_SendsExpectedResponse() + public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException() { - var collectionId = Guid.NewGuid(); + var sut = CreateService(); + await Assert.ThrowsAsync( + async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new {}, null) + ); + } - var cipher = new Cipher - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - OrganizationId = null, - RevisionDate = DateTime.UtcNow, - }; + [Fact] + public async Task SendPayloadToUserAsync_ThrowsNotImplementedException() + { + var sut = CreateService(); + await Assert.ThrowsAsync( + async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new {}, null) + ); + } - var expectedPayload = new JsonObject + [Fact] + public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException() + { + var sut = CreateService(); + await Assert.ThrowsAsync( + async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new {}, null) + ); + } + + protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionIds) + { + return new JsonObject { ["UserId"] = cipher.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 1, ["Payload"] = new JsonObject { @@ -111,32 +101,16 @@ public async Task PushSyncCipherCreateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncCipherCreateAsync(cipher, [collectionId]), - expectedPayload - ); } - [Fact] - public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionIds) { - var collectionId = Guid.NewGuid(); - - var cipher = new Cipher - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - OrganizationId = null, - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = cipher.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 0, ["Payload"] = new JsonObject { @@ -151,32 +125,16 @@ public async Task PushSyncCipherUpdateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncCipherUpdateAsync(cipher, [collectionId]), - expectedPayload - ); } - [Fact] - public async Task PushSyncCipherDeleteAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher) { - var collectionId = Guid.NewGuid(); - - var cipher = new Cipher - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - OrganizationId = null, - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = cipher.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 2, ["Payload"] = new JsonObject { @@ -189,31 +147,16 @@ public async Task PushSyncCipherDeleteAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncCipherDeleteAsync(cipher), - expectedPayload - ); } - [Fact] - public async Task PushSyncFolderCreateAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder) { - var collectionId = Guid.NewGuid(); - - var folder = new Folder - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = folder.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 7, ["Payload"] = new JsonObject { @@ -224,31 +167,16 @@ public async Task PushSyncFolderCreateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncFolderCreateAsync(folder), - expectedPayload - ); } - [Fact] - public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder) { - var collectionId = Guid.NewGuid(); - - var folder = new Folder - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = folder.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 8, ["Payload"] = new JsonObject { @@ -259,31 +187,16 @@ public async Task PushSyncFolderUpdateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncFolderUpdateAsync(folder), - expectedPayload - ); } - [Fact] - public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder) { - var collectionId = Guid.NewGuid(); - - var folder = new Folder - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = folder.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 3, ["Payload"] = new JsonObject { @@ -294,19 +207,11 @@ public async Task PushSyncFolderDeleteAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncFolderDeleteAsync(folder), - expectedPayload - ); } - [Fact] - public async Task PushSyncCiphersAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncCiphersPayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -316,24 +221,16 @@ public async Task PushSyncCiphersAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncCiphersAsync(userId), - expectedPayload - ); } - [Fact] - public async Task PushSyncVaultAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncVaultPayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -343,24 +240,16 @@ public async Task PushSyncVaultAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncVaultAsync(userId), - expectedPayload - ); } - [Fact] - public async Task PushSyncOrganizationsAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -370,24 +259,16 @@ public async Task PushSyncOrganizationsAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrganizationsAsync(userId), - expectedPayload - ); } - [Fact] - public async Task PushSyncOrgKeysAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -397,24 +278,16 @@ public async Task PushSyncOrgKeysAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrgKeysAsync(userId), - expectedPayload - ); } - [Fact] - public async Task PushSyncSettingsAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncSettingsPayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -424,28 +297,18 @@ public async Task PushSyncSettingsAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncSettingsAsync(userId), - expectedPayload - ); } - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentContext) + protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) { - var userId = Guid.NewGuid(); + JsonNode? identifier = excludeCurrentContext ? DeviceIdentifier : null; - JsonNode? identifier = excludeCurrentContext ? _deviceIdentifier : null; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -455,34 +318,20 @@ public async Task PushLogOutAsync_SendsExpectedResponse(bool excludeCurrentConte ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushLogOutAsync(userId, excludeCurrentContext), - expectedPayload - ); } - - [Fact] - public async Task PushSyncSendCreateAsync_SendsExpectedResponse() + protected override JsonNode GetPushSendCreatePayload(Send send) { - var send = new Send - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = send.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 12, ["Payload"] = new JsonObject { @@ -493,29 +342,15 @@ public async Task PushSyncSendCreateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncSendCreateAsync(send), - expectedPayload - ); } - - [Fact] - public async Task PushSyncSendUpdateAsync_SendsExpectedResponse() + protected override JsonNode GetPushSendUpdatePayload(Send send) { - var send = new Send - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = send.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 13, ["Payload"] = new JsonObject { @@ -526,29 +361,15 @@ public async Task PushSyncSendUpdateAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncSendUpdateAsync(send), - expectedPayload - ); } - - [Fact] - public async Task PushSyncSendDeleteAsync_SendsExpectedResponse() + protected override JsonNode GetPushSendDeletePayload(Send send) { - var send = new Send - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - RevisionDate = DateTime.UtcNow, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = send.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 14, ["Payload"] = new JsonObject { @@ -559,28 +380,15 @@ public async Task PushSyncSendDeleteAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncSendDeleteAsync(send), - expectedPayload - ); } - - [Fact] - public async Task PushAuthRequestAsync_SendsExpectedResponse() + protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) { - var authRequest = new AuthRequest - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = authRequest.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, +["Identifier"] = DeviceIdentifier, ["Type"] = 15, ["Payload"] = new JsonObject { @@ -590,28 +398,15 @@ public async Task PushAuthRequestAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushAuthRequestAsync(authRequest), - expectedPayload - ); } - - [Fact] - public async Task PushAuthRequestResponseAsync_SendsExpectedResponse() + protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest) { - var authRequest = new AuthRequest - { - Id = Guid.NewGuid(), - UserId = Guid.NewGuid(), - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = authRequest.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 16, ["Payload"] = new JsonObject { @@ -621,47 +416,23 @@ public async Task PushAuthRequestResponseAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - 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) + protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? 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, - }; - - JsonNode? installationId = global ? _globalSettings.Installation.Id : null; + JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 20, ["Payload"] = new JsonObject { ["Id"] = notification.Id, ["Priority"] = 3, - ["Global"] = global, + ["Global"] = notification.Global, ["ClientType"] = 0, ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, @@ -676,53 +447,23 @@ public async Task PushNotificationAsync_SendsExpectedResponse(bool global, strin ["ClientType"] = 0, ["InstallationId"] = installationId?.DeepClone(), }; - - 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) + protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? 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, - }; - - JsonNode? installationId = global ? _globalSettings.Installation.Id : null; + JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, ["DeviceId"] = _deviceId, - ["Identifier"] = _deviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 21, ["Payload"] = new JsonObject { ["Id"] = notification.Id, ["Priority"] = 3, - ["Global"] = global, + ["Global"] = notification.Global, ["ClientType"] = 0, ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, @@ -737,23 +478,10 @@ public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, ["ClientType"] = 0, ["InstallationId"] = installationId?.DeepClone(), }; - - await VerifyNotificationAsync( - async sut => await sut.PushNotificationStatusAsync(notification, notificationStatus), - expectedPayload - ); } - - [Fact] - public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization) { - var organization = new Organization - { - Id = Guid.NewGuid(), - Enabled = true, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = null, ["OrganizationId"] = organization.Id, @@ -768,26 +496,10 @@ public async Task PushSyncOrganizationStatusAsync_SendsExpectedResponse() ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrganizationStatusAsync(organization), - expectedPayload - ); } - - [Fact] - public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExpectedResponse() + protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization) { - var organization = new Organization - { - Id = Guid.NewGuid(), - Enabled = true, - LimitCollectionCreation = true, - LimitCollectionDeletion = true, - LimitItemDeletion = true, - }; - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = null, ["OrganizationId"] = organization.Id, @@ -804,19 +516,11 @@ public async Task PushSyncOrganizationCollectionManagementSettingsAsync_SendsExp ["ClientType"] = null, ["InstallationId"] = null, }; - - await VerifyNotificationAsync( - async sut => await sut.PushSyncOrganizationCollectionManagementSettingsAsync(organization), - expectedPayload - ); } - [Fact] - public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() + protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) { - var userId = Guid.NewGuid(); - - var expectedPayload = new JsonObject + return new JsonObject { ["UserId"] = userId, ["OrganizationId"] = null, @@ -826,95 +530,10 @@ public async Task PushPendingSecurityTasksAsync_SendsExpectedResponse() ["Payload"] = new JsonObject { ["UserId"] = userId, - ["Date"] = _fakeTimeProvider.GetUtcNow().UtcDateTime, + ["Date"] = FakeTimeProvider.GetUtcNow().UtcDateTime, }, ["ClientType"] = null, ["InstallationId"] = null, }; - - 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 expectedRequestBody) - { - 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); - - _deviceRepository.GetByIdentifierAsync(_deviceIdentifier) - .Returns(new Device - { - Id = _deviceId, - }); - - var connectTokenRequest = _mockIdentityClient - .Expect(HttpMethod.Post, "https://localhost:8888/connect/token") - .Respond(HttpStatusCode.OK, JsonContent.Create(new - { - access_token = CreateAccessToken(DateTime.UtcNow.AddDays(1)), - })); - - var pushSendRequest = _mockPushClient - .Expect(HttpMethod.Post, "https://localhost:7777/push/send") - .With(request => - { - if (request.Content is not JsonContent jsonContent) - { - return false; - } - - // TODO: What options? - var actualString = JsonSerializer.Serialize(jsonContent.Value); - var actualNode = JsonNode.Parse(actualString); - - return JsonNode.DeepEquals(actualNode, expectedRequestBody); - }) - .Respond(HttpStatusCode.OK); - - await test(_sut); - - Assert.Equal(1, _mockPushClient.GetMatchCount(pushSendRequest)); - } - - private static string CreateAccessToken(DateTime expirationTime) - { - var tokenHandler = new JwtSecurityTokenHandler(); - var token = new JwtSecurityToken(expires: expirationTime); - return tokenHandler.WriteToken(token); } } From 9944154b0817698b1266fc07b5341db96e4aa90f Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:16:52 -0500 Subject: [PATCH 6/9] Register TimeProvider just in case --- src/SharedWeb/Utilities/ServiceCollectionExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 144ea1f036a8..5340a88aef59 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -67,6 +67,7 @@ using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -279,6 +280,8 @@ public static void AddDefaultServices(this IServiceCollection services, GlobalSe services.AddSingleton(); } + services.TryAddSingleton(TimeProvider.System); + services.AddSingleton(); if (globalSettings.SelfHosted) { From 93b834e1d0ecbab6a57cffe20eda31ec9a1ad891 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:19:33 -0500 Subject: [PATCH 7/9] Format --- ...ficationHubPushNotificationServiceTests.cs | 4 +- ...icationsApiPushNotificationServiceTests.cs | 42 +++++++++---------- .../Platform/Push/Services/PushTestBase.cs | 2 +- .../RelayPushNotificationServiceTests.cs | 8 ++-- 4 files changed, 28 insertions(+), 28 deletions(-) diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index f4779b497879..9ea3a0d3c408 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -759,7 +759,7 @@ await VerifyNotificationAsync( ); } - [Fact] + [Fact] public async Task PushSyncSendDeleteAsync_SendExpectedData() { var userId = Guid.NewGuid(); @@ -1063,7 +1063,7 @@ await VerifyNotificationAsync( ); } - private async Task VerifyNotificationAsync(Func test, + private async Task VerifyNotificationAsync(Func test, PushType type, JsonNode expectedPayload, string tag) { var installationDeviceRepository = Substitute.For(); diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index 587afbbd2a13..e92fd90735c9 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -46,7 +46,7 @@ protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid c ["ContextId"] = DeviceIdentifier, }; } - protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId) + protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid collectionId) { return new JsonObject { @@ -62,7 +62,7 @@ protected override JsonNode GetPushSyncCipherUpdatePayload(Cipher cipher, Guid c ["ContextId"] = DeviceIdentifier, }; } - protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher) + protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher) { return new JsonObject { @@ -79,7 +79,7 @@ protected override JsonNode GetPushSyncCipherDeletePayload(Cipher cipher) }; } - protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder) + protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder) { return new JsonObject { @@ -94,7 +94,7 @@ protected override JsonNode GetPushSyncFolderCreatePayload(Folder folder) }; } - protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder) + protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder) { return new JsonObject { @@ -109,7 +109,7 @@ protected override JsonNode GetPushSyncFolderUpdatePayload(Folder folder) }; } - protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder) + protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder) { return new JsonObject { @@ -124,7 +124,7 @@ protected override JsonNode GetPushSyncFolderDeletePayload(Folder folder) }; } - protected override JsonNode GetPushSyncCiphersPayload(Guid userId) + protected override JsonNode GetPushSyncCiphersPayload(Guid userId) { return new JsonObject { @@ -138,7 +138,7 @@ protected override JsonNode GetPushSyncCiphersPayload(Guid userId) }; } - protected override JsonNode GetPushSyncVaultPayload(Guid userId) + protected override JsonNode GetPushSyncVaultPayload(Guid userId) { return new JsonObject { @@ -152,7 +152,7 @@ protected override JsonNode GetPushSyncVaultPayload(Guid userId) }; } - protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId) + protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId) { return new JsonObject { @@ -166,7 +166,7 @@ protected override JsonNode GetPushSyncOrganizationsPayload(Guid userId) }; } - protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId) + protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId) { return new JsonObject { @@ -180,7 +180,7 @@ protected override JsonNode GetPushSyncOrgKeysPayload(Guid userId) }; } - protected override JsonNode GetPushSyncSettingsPayload(Guid userId) + protected override JsonNode GetPushSyncSettingsPayload(Guid userId) { return new JsonObject { @@ -194,7 +194,7 @@ protected override JsonNode GetPushSyncSettingsPayload(Guid userId) }; } - protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) + protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurrentContext) { JsonNode? contextId = excludeCurrentContext ? DeviceIdentifier : null; @@ -210,7 +210,7 @@ protected override JsonNode GetPushLogOutPayload(Guid userId, bool excludeCurren }; } - protected override JsonNode GetPushSendCreatePayload(Send send) + protected override JsonNode GetPushSendCreatePayload(Send send) { return new JsonObject { @@ -225,7 +225,7 @@ protected override JsonNode GetPushSendCreatePayload(Send send) }; } - protected override JsonNode GetPushSendUpdatePayload(Send send) + protected override JsonNode GetPushSendUpdatePayload(Send send) { return new JsonObject { @@ -240,7 +240,7 @@ protected override JsonNode GetPushSendUpdatePayload(Send send) }; } - protected override JsonNode GetPushSendDeletePayload(Send send) + protected override JsonNode GetPushSendDeletePayload(Send send) { return new JsonObject { @@ -255,7 +255,7 @@ protected override JsonNode GetPushSendDeletePayload(Send send) }; } - protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) + protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) { return new JsonObject { @@ -269,7 +269,7 @@ protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) }; } - protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest) + protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRequest) { return new JsonObject { @@ -283,7 +283,7 @@ protected override JsonNode GetPushAuthRequestResponsePayload(AuthRequest authRe }; } - protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId) + protected override JsonNode GetPushNotificationResponsePayload(Notification notification, Guid? userId, Guid? organizationId) { JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; @@ -310,7 +310,7 @@ protected override JsonNode GetPushNotificationResponsePayload(Notification noti }; } - protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId) + protected override JsonNode GetPushNotificationStatusResponsePayload(Notification notification, NotificationStatus notificationStatus, Guid? userId, Guid? organizationId) { JsonNode? installationId = notification.Global ? GlobalSettings.Installation.Id : null; @@ -337,7 +337,7 @@ protected override JsonNode GetPushNotificationStatusResponsePayload(Notificatio }; } - protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization) + protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organization organization) { return new JsonObject { @@ -351,7 +351,7 @@ protected override JsonNode GetPushSyncOrganizationStatusResponsePayload(Organiz }; } - protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization) + protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsResponsePayload(Organization organization) { return new JsonObject { @@ -367,7 +367,7 @@ protected override JsonNode GetPushSyncOrganizationCollectionManagementSettingsR }; } - protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) + protected override JsonNode GetPushPendingSecurityTasksResponsePayload(Guid userId) { return new JsonObject { diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs index d9b77f998048..2b584aafb47b 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -1,4 +1,4 @@ -// TODO: Move to own file +// TODO: Move to own file using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Json; diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index c554484f98ad..4a1fd483767b 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -57,7 +57,7 @@ public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException() { var sut = CreateService(); await Assert.ThrowsAsync( - async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new {}, null) + async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new { }, null) ); } @@ -66,7 +66,7 @@ public async Task SendPayloadToUserAsync_ThrowsNotImplementedException() { var sut = CreateService(); await Assert.ThrowsAsync( - async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new {}, null) + async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new { }, null) ); } @@ -75,7 +75,7 @@ public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException() { var sut = CreateService(); await Assert.ThrowsAsync( - async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new {}, null) + async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new { }, null) ); } @@ -388,7 +388,7 @@ protected override JsonNode GetPushAuthRequestPayload(AuthRequest authRequest) ["UserId"] = authRequest.UserId, ["OrganizationId"] = null, ["DeviceId"] = _deviceId, -["Identifier"] = DeviceIdentifier, + ["Identifier"] = DeviceIdentifier, ["Type"] = 15, ["Payload"] = new JsonObject { From 7b266366446a61ce8882dc2920a3bcee8fa8c752 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:50:42 -0400 Subject: [PATCH 8/9] React to TaskId --- .../NotificationHubPushNotificationServiceTests.cs | 3 +++ .../NotificationsApiPushNotificationServiceTests.cs | 2 ++ test/Core.Test/Platform/Push/Services/PushTestBase.cs | 10 +++++++++- .../Push/Services/RelayPushNotificationServiceTests.cs | 6 ++++-- 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index a5751633b80c..6f4ea9ca1250 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -947,6 +947,7 @@ public async Task PushNotificationAsync_SendExpectedData(bool global, string? us ClientType = ClientType.All, UserId = userId != null ? Guid.Parse(userId) : null, OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + TaskId = Guid.NewGuid(), Title = "My Title", Body = "My Body", CreationDate = DateTime.UtcNow.AddDays(-1), @@ -963,6 +964,7 @@ public async Task PushNotificationAsync_SendExpectedData(bool global, string? us ["ClientType"] = 0, ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, + ["TaskId"] = notification.TaskId, ["InstallationId"] = installationId, ["Title"] = notification.Title, ["Body"] = notification.Body, @@ -1031,6 +1033,7 @@ public async Task PushNotificationStatusAsync_SendExpectedData(bool global, stri ["ClientType"] = 0, ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, + ["TaskId"] = notification.TaskId, ["InstallationId"] = installationId, ["Title"] = notification.Title, ["Body"] = notification.Body, diff --git a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs index e92fd90735c9..d206d96d44e5 100644 --- a/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/NotificationsApiPushNotificationServiceTests.cs @@ -298,6 +298,7 @@ protected override JsonNode GetPushNotificationResponsePayload(Notification noti ["ClientType"] = 0, ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, + ["TaskId"] = notification.TaskId, ["InstallationId"] = installationId, ["Title"] = notification.Title, ["Body"] = notification.Body, @@ -326,6 +327,7 @@ protected override JsonNode GetPushNotificationStatusResponsePayload(Notificatio ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, ["InstallationId"] = installationId, + ["TaskId"] = notification.TaskId, ["Title"] = notification.Title, ["Body"] = notification.Body, ["CreationDate"] = notification.CreationDate, diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs index 2b584aafb47b..0e53c8f8ed35 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -348,6 +348,7 @@ public async Task PushNotificationAsync_SendsExpectedResponse(bool global, strin ClientType = ClientType.All, UserId = userId != null ? Guid.Parse(userId) : null, OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + TaskId = Guid.NewGuid(), Title = "My Title", Body = "My Body", CreationDate = DateTime.UtcNow.AddDays(-1), @@ -374,6 +375,7 @@ public async Task PushNotificationStatusAsync_SendsExpectedResponse(bool global, ClientType = ClientType.All, UserId = userId != null ? Guid.Parse(userId) : null, OrganizationId = organizationId != null ? Guid.Parse(organizationId) : null, + TaskId = Guid.NewGuid(), Title = "My Title", Body = "My Body", CreationDate = DateTime.UtcNow.AddDays(-1), @@ -460,6 +462,8 @@ JsonNode expectedRequestBody access_token = CreateAccessToken(DateTime.UtcNow.AddDays(1)), })); + JsonNode actualNode = null; + var clientRequest = MockClient .Expect(HttpMethod.Post, ExpectedClientUrl()) .With(request => @@ -471,7 +475,7 @@ JsonNode expectedRequestBody // TODO: What options? var actualString = JsonSerializer.Serialize(jsonContent.Value); - var actualNode = JsonNode.Parse(actualString); + actualNode = JsonNode.Parse(actualString); return JsonNode.DeepEquals(actualNode, expectedRequestBody); }) @@ -479,6 +483,10 @@ JsonNode expectedRequestBody await test(CreateService()); + Assert.NotNull(actualNode); + + Assert.Equal(expectedRequestBody, actualNode, EqualityComparer.Create(JsonNode.DeepEquals)); + Assert.Equal(1, MockClient.GetMatchCount(clientRequest)); } diff --git a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs index 4a1fd483767b..faa6b5dfa75d 100644 --- a/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Platform/Push/Services/RelayPushNotificationServiceTests.cs @@ -434,8 +434,9 @@ protected override JsonNode GetPushNotificationResponsePayload(Notification noti ["Priority"] = 3, ["Global"] = notification.Global, ["ClientType"] = 0, - ["UserId"] = notification.UserId, - ["OrganizationId"] = notification.OrganizationId, + ["UserId"] = userId, + ["OrganizationId"] = organizationId, + ["TaskId"] = notification.TaskId, ["InstallationId"] = installationId, ["Title"] = notification.Title, ["Body"] = notification.Body, @@ -468,6 +469,7 @@ protected override JsonNode GetPushNotificationStatusResponsePayload(Notificatio ["UserId"] = notification.UserId, ["OrganizationId"] = notification.OrganizationId, ["InstallationId"] = installationId, + ["TaskId"] = notification.TaskId, ["Title"] = notification.Title, ["Body"] = notification.Body, ["CreationDate"] = notification.CreationDate, From e6c3bf79022bd03b4b248903c8818b4cc4148eee Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 14 Apr 2025 12:51:35 -0400 Subject: [PATCH 9/9] Remove completed TODO --- test/Core.Test/Platform/Push/Services/PushTestBase.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/Core.Test/Platform/Push/Services/PushTestBase.cs b/test/Core.Test/Platform/Push/Services/PushTestBase.cs index 0e53c8f8ed35..111df7ca2694 100644 --- a/test/Core.Test/Platform/Push/Services/PushTestBase.cs +++ b/test/Core.Test/Platform/Push/Services/PushTestBase.cs @@ -1,5 +1,4 @@ -// TODO: Move to own file -using System.IdentityModel.Tokens.Jwt; +using System.IdentityModel.Tokens.Jwt; using System.Net; using System.Net.Http.Json; using System.Text.Json;