From 49fe7c93fd5eb6fd5df680194403cf4b2beabace Mon Sep 17 00:00:00 2001 From: Maciej Zieniuk Date: Mon, 25 Nov 2024 18:05:29 +0000 Subject: [PATCH] PM-15084: Push notifications to installation id. This enables the Notification Center created global notifications to be sent to affected devices of the same server installation. All clients connected to any of the server instance of that installation id would receive it. This is useful for notifying all clients of an installation about upcoming maintenance. This works both for Self-Hosted, but also for Cloud, assuming an installation id is set. --- src/Api/Controllers/PushController.cs | 32 ++- .../Request/PushRegistrationRequestModel.cs | 16 +- .../Api/Request/PushSendRequestModel.cs | 8 +- src/Core/Models/PushNotification.cs | 1 + .../NotificationHubPushNotificationService.cs | 59 +++++- .../NotificationHubPushRegistrationService.cs | 22 ++- src/Core/Services/IPushNotificationService.cs | 3 + src/Core/Services/IPushRegistrationService.cs | 2 +- .../AzureQueuePushNotificationService.cs | 13 +- .../Services/Implementations/DeviceService.cs | 9 +- .../MultiServicePushNotificationService.cs | 8 + ...NotificationsApiPushNotificationService.cs | 14 ++ .../RelayPushNotificationService.cs | 49 ++++- .../RelayPushRegistrationService.cs | 5 +- .../NoopPushNotificationService.cs | 6 +- .../NoopPushRegistrationService.cs | 2 +- src/Notifications/HubHelpers.cs | 8 +- src/Notifications/NotificationsHub.cs | 29 +++ .../Utilities/ServiceCollectionExtensions.cs | 10 +- .../Controllers/PushControllerTests.cs | 183 +++++++++++++++--- .../RepeatingPatternBitAutoDataAttribute.cs | 62 ++++++ .../Api/Request/PushSendRequestModelTests.cs | 72 ++++++- ...ficationHubPushNotificationServiceTests.cs | 179 ++++++++++++++--- ...ficationHubPushRegistrationServiceTests.cs | 64 +++--- .../AzureQueuePushNotificationServiceTests.cs | 69 ++++++- test/Core.Test/Services/DeviceServiceTests.cs | 17 +- ...ultiServicePushNotificationServiceTests.cs | 18 ++ 27 files changed, 808 insertions(+), 152 deletions(-) create mode 100644 test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs diff --git a/src/Api/Controllers/PushController.cs b/src/Api/Controllers/PushController.cs index eb2976f9c0..68f49e32df 100644 --- a/src/Api/Controllers/PushController.cs +++ b/src/Api/Controllers/PushController.cs @@ -18,14 +18,14 @@ public class PushController : Controller private readonly IPushNotificationService _pushNotificationService; private readonly IWebHostEnvironment _environment; private readonly ICurrentContext _currentContext; - private readonly GlobalSettings _globalSettings; + private readonly IGlobalSettings _globalSettings; public PushController( IPushRegistrationService pushRegistrationService, IPushNotificationService pushNotificationService, IWebHostEnvironment environment, ICurrentContext currentContext, - GlobalSettings globalSettings) + IGlobalSettings globalSettings) { _currentContext = currentContext; _environment = environment; @@ -35,22 +35,23 @@ public class PushController : Controller } [HttpPost("register")] - public async Task PostRegister([FromBody] PushRegistrationRequestModel model) + public async Task RegisterAsync([FromBody] PushRegistrationRequestModel model) { CheckUsage(); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(model.PushToken, Prefix(model.DeviceId), - Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix)); + Prefix(model.UserId), Prefix(model.Identifier), model.Type, model.OrganizationIds.Select(Prefix), + model.InstallationId); } [HttpPost("delete")] - public async Task PostDelete([FromBody] PushDeviceRequestModel model) + public async Task DeleteAsync([FromBody] PushDeviceRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id)); } [HttpPut("add-organization")] - public async Task PutAddOrganization([FromBody] PushUpdateRequestModel model) + public async Task AddOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.AddUserRegistrationOrganizationAsync( @@ -59,7 +60,7 @@ public class PushController : Controller } [HttpPut("delete-organization")] - public async Task PutDeleteOrganization([FromBody] PushUpdateRequestModel model) + public async Task DeleteOrganizationAsync([FromBody] PushUpdateRequestModel model) { CheckUsage(); await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( @@ -68,11 +69,22 @@ public class PushController : Controller } [HttpPost("send")] - public async Task PostSend([FromBody] PushSendRequestModel model) + public async Task SendAsync([FromBody] PushSendRequestModel model) { CheckUsage(); - if (!string.IsNullOrWhiteSpace(model.UserId)) + if (!string.IsNullOrWhiteSpace(model.InstallationId)) + { + if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!) + { + throw new BadRequestException("InstallationId does not match current context."); + } + + await _pushNotificationService.SendPayloadToInstallationAsync( + _currentContext.InstallationId.Value.ToString(), model.Type, model.Payload, Prefix(model.Identifier), + Prefix(model.DeviceId), model.ClientType); + } + else if (!string.IsNullOrWhiteSpace(model.UserId)) { await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId), model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType); @@ -91,7 +103,7 @@ public class PushController : Controller return null; } - return $"{_currentContext.InstallationId.Value}_{value}"; + return $"{_currentContext.InstallationId!.Value}_{value}"; } private void CheckUsage() diff --git a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs index ee787dd083..f02879b849 100644 --- a/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs +++ b/src/Core/Models/Api/Request/PushRegistrationRequestModel.cs @@ -5,15 +5,11 @@ namespace Bit.Core.Models.Api; public class PushRegistrationRequestModel { - [Required] - public string DeviceId { get; set; } - [Required] - public string PushToken { get; set; } - [Required] - public string UserId { get; set; } - [Required] - public DeviceType Type { get; set; } - [Required] - public string Identifier { get; set; } + [Required] public string DeviceId { get; set; } + [Required] public string PushToken { get; set; } + [Required] public string UserId { get; set; } + [Required] public DeviceType Type { get; set; } + [Required] public string Identifier { get; set; } public IEnumerable OrganizationIds { get; set; } + public string InstallationId { get; set; } } diff --git a/src/Core/Models/Api/Request/PushSendRequestModel.cs b/src/Core/Models/Api/Request/PushSendRequestModel.cs index 7247e6d25f..0ef7e999e3 100644 --- a/src/Core/Models/Api/Request/PushSendRequestModel.cs +++ b/src/Core/Models/Api/Request/PushSendRequestModel.cs @@ -13,12 +13,16 @@ public class PushSendRequestModel : IValidatableObject public required PushType Type { get; set; } public required object Payload { get; set; } public ClientType? ClientType { get; set; } + public string? InstallationId { get; set; } public IEnumerable Validate(ValidationContext validationContext) { - if (string.IsNullOrWhiteSpace(UserId) && string.IsNullOrWhiteSpace(OrganizationId)) + if (string.IsNullOrWhiteSpace(UserId) && + string.IsNullOrWhiteSpace(OrganizationId) && + string.IsNullOrWhiteSpace(InstallationId)) { - yield return new ValidationResult($"{nameof(UserId)} or {nameof(OrganizationId)} is required."); + yield return new ValidationResult( + $"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required."); } } } diff --git a/src/Core/Models/PushNotification.cs b/src/Core/Models/PushNotification.cs index d6fac8b381..de46a598ba 100644 --- a/src/Core/Models/PushNotification.cs +++ b/src/Core/Models/PushNotification.cs @@ -51,6 +51,7 @@ public class SyncNotificationPushNotification public Guid Id { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? InstallationId { get; set; } public ClientType ClientType { get; set; } public DateTime RevisionDate { get; set; } public DateTime? ReadDate { get; set; } diff --git a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs index 90030ad209..4b46944eb8 100644 --- a/src/Core/NotificationHub/NotificationHubPushNotificationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushNotificationService.cs @@ -9,6 +9,7 @@ using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Microsoft.AspNetCore.Http; @@ -17,6 +18,11 @@ using Notification = Bit.Core.NotificationCenter.Entities.Notification; namespace Bit.Core.NotificationHub; +/// +/// Sends mobile push notifications to the Azure Notification Hub. +/// Used by Cloud-Hosted environments. +/// Received by Firebase for Android or APNS for iOS. +/// public class NotificationHubPushNotificationService : IPushNotificationService { private readonly IInstallationDeviceRepository _installationDeviceRepository; @@ -24,17 +30,20 @@ public class NotificationHubPushNotificationService : IPushNotificationService private readonly bool _enableTracing = false; private readonly INotificationHubPool _notificationHubPool; private readonly ILogger _logger; + private readonly IGlobalSettings _globalSettings; public NotificationHubPushNotificationService( IInstallationDeviceRepository installationDeviceRepository, INotificationHubPool notificationHubPool, IHttpContextAccessor httpContextAccessor, - ILogger logger) + ILogger logger, + IGlobalSettings globalSettings) { _installationDeviceRepository = installationDeviceRepository; _httpContextAccessor = httpContextAccessor; _notificationHubPool = notificationHubPool; _logger = logger; + _globalSettings = globalSettings; } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -184,48 +193,70 @@ public class NotificationHubPushNotificationService : IPushNotificationService public async Task PushSyncNotificationCreateAsync(Notification notification) { + Guid? installationId = notification.Global && _globalSettings.Installation.Id != default + ? _globalSettings.Installation.Id + : null; + var message = new SyncNotificationPushNotification { Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = installationId, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate }; - if (notification.UserId.HasValue) + if (notification.Global && installationId.HasValue) + { + await SendPayloadToInstallationAsync(installationId.Value, PushType.SyncNotificationCreate, message, true, + notification.ClientType); + } + else if (notification.UserId.HasValue) { await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationCreate, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationCreate, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationCreate, + message, true, notification.ClientType); } } public async Task PushSyncNotificationUpdateAsync(Notification notification, NotificationStatus? notificationStatus) { + Guid? installationId = notification.Global && _globalSettings.Installation.Id != default + ? _globalSettings.Installation.Id + : null; + var message = new SyncNotificationPushNotification { Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = installationId, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate, ReadDate = notificationStatus?.ReadDate, DeletedDate = notificationStatus?.DeletedDate }; - if (notification.UserId.HasValue) + if (notification.Global && installationId.HasValue) + { + await SendPayloadToInstallationAsync(installationId.Value, PushType.SyncNotificationUpdate, message, true, + notification.ClientType); + } + else if (notification.UserId.HasValue) { await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationUpdate, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationUpdate, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationUpdate, + message, true, notification.ClientType); } } @@ -237,6 +268,13 @@ public class NotificationHubPushNotificationService : IPushNotificationService await SendPayloadToUserAsync(authRequest.UserId, type, message, true); } + private async Task SendPayloadToInstallationAsync(Guid installationId, PushType type, object payload, + bool excludeCurrentContext, ClientType? clientType = null) + { + await SendPayloadToInstallationAsync(installationId.ToString(), type, payload, + GetContextIdentifier(excludeCurrentContext), clientType: clientType); + } + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, ClientType? clientType = null) { @@ -251,6 +289,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService GetContextIdentifier(excludeCurrentContext), clientType: clientType); } + public async Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, + string? identifier, string? deviceId = null, ClientType? clientType = null) + { + var tag = BuildTag($"template:payload && installationId:{installationId}", identifier, clientType); + await SendPayloadAsync(tag, type, payload); + if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) + { + await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); + } + } + public async Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs index 30c3070577..37fdf6ebde 100644 --- a/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs +++ b/src/Core/NotificationHub/NotificationHubPushRegistrationService.cs @@ -21,7 +21,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, string installationId) { if (string.IsNullOrWhiteSpace(pushToken)) { @@ -50,13 +50,18 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService installation.Tags.Add($"organizationId:{organizationId}"); } + if (!string.IsNullOrWhiteSpace(installationId)) + { + installation.Tags.Add($"installationId:{installationId}"); + } + string payloadTemplate = null, messageTemplate = null, badgeMessageTemplate = null; switch (type) { case DeviceType.Android: payloadTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\",\"payload\":\"$(payload)\"}}}"; messageTemplate = "{\"message\":{\"data\":{\"type\":\"$(type)\"}," + - "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; + "\"notification\":{\"title\":\"$(title)\",\"body\":\"$(message)\"}}}"; installation.Platform = NotificationPlatform.FcmV1; break; case DeviceType.iOS: @@ -80,11 +85,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } BuildInstallationTemplate(installation, "payload", payloadTemplate, userId, identifier, clientType, - organizationIdsList); + organizationIdsList, installationId); BuildInstallationTemplate(installation, "message", messageTemplate, userId, identifier, clientType, - organizationIdsList); + organizationIdsList, installationId); BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, - userId, identifier, clientType, organizationIdsList); + userId, identifier, clientType, organizationIdsList, installationId); await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation); if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) @@ -94,7 +99,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService } private void BuildInstallationTemplate(Installation installation, string templateId, string templateBody, - string userId, string identifier, ClientType clientType, List organizationIds) + string userId, string identifier, ClientType clientType, List organizationIds, string installationId) { if (templateBody == null) { @@ -122,6 +127,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService template.Tags.Add($"organizationId:{organizationId}"); } + if (!string.IsNullOrWhiteSpace(installationId)) + { + template.Tags.Add($"installationId:{installationId}"); + } + installation.Templates.Add(fullTemplateId, template); } diff --git a/src/Core/Services/IPushNotificationService.cs b/src/Core/Services/IPushNotificationService.cs index fe26e9a843..c9c6353a91 100644 --- a/src/Core/Services/IPushNotificationService.cs +++ b/src/Core/Services/IPushNotificationService.cs @@ -29,6 +29,9 @@ public interface IPushNotificationService Task PushAuthRequestAsync(AuthRequest authRequest); Task PushAuthRequestResponseAsync(AuthRequest authRequest); + Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null); + Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null); diff --git a/src/Core/Services/IPushRegistrationService.cs b/src/Core/Services/IPushRegistrationService.cs index 304387581b..1bfe4203cc 100644 --- a/src/Core/Services/IPushRegistrationService.cs +++ b/src/Core/Services/IPushRegistrationService.cs @@ -5,7 +5,7 @@ namespace Bit.Core.Services; public interface IPushRegistrationService { Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds); + string identifier, DeviceType type, IEnumerable organizationIds, string installationId); Task DeleteRegistrationAsync(string deviceId); Task AddUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); Task DeleteUserRegistrationOrganizationAsync(IEnumerable deviceIds, string organizationId); diff --git a/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs b/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs index 3dd93e5d2d..7649ba3bc3 100644 --- a/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs +++ b/src/Core/Services/Implementations/AzureQueuePushNotificationService.cs @@ -6,6 +6,7 @@ using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; +using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Utilities; using Bit.Core.Vault.Entities; @@ -18,13 +19,16 @@ public class AzureQueuePushNotificationService : IPushNotificationService { private readonly QueueClient _queueClient; private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IGlobalSettings _globalSettings; public AzureQueuePushNotificationService( [FromKeyedServices("notifications")] QueueClient queueClient, - IHttpContextAccessor httpContextAccessor) + IHttpContextAccessor httpContextAccessor, + IGlobalSettings globalSettings) { _queueClient = queueClient; _httpContextAccessor = httpContextAccessor; + _globalSettings = globalSettings; } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) @@ -172,6 +176,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate }; @@ -186,6 +191,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate, ReadDate = notificationStatus?.ReadDate, @@ -230,6 +236,11 @@ public class AzureQueuePushNotificationService : IPushNotificationService return currentContext?.DeviceIdentifier; } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + // Noop + Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Services/Implementations/DeviceService.cs b/src/Core/Services/Implementations/DeviceService.cs index cc45bf62a0..0534d4e08d 100644 --- a/src/Core/Services/Implementations/DeviceService.cs +++ b/src/Core/Services/Implementations/DeviceService.cs @@ -4,6 +4,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.Settings; namespace Bit.Core.Services; @@ -12,15 +13,18 @@ public class DeviceService : IDeviceService private readonly IDeviceRepository _deviceRepository; private readonly IPushRegistrationService _pushRegistrationService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IGlobalSettings _globalSettings; public DeviceService( IDeviceRepository deviceRepository, IPushRegistrationService pushRegistrationService, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + IGlobalSettings globalSettings) { _deviceRepository = deviceRepository; _pushRegistrationService = pushRegistrationService; _organizationUserRepository = organizationUserRepository; + _globalSettings = globalSettings; } public async Task SaveAsync(Device device) @@ -41,7 +45,8 @@ public class DeviceService : IDeviceService .Select(ou => ou.OrganizationId.ToString()); await _pushRegistrationService.CreateOrUpdateRegistrationAsync(device.PushToken, device.Id.ToString(), - device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString); + device.UserId.ToString(), device.Identifier, device.Type, organizationIdsString, + _globalSettings.Installation.Id.ToString()); } public async Task ClearTokenAsync(Device device) diff --git a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs index 4d266a538a..4d274ed75d 100644 --- a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs +++ b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs @@ -132,6 +132,14 @@ public class MultiServicePushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) + { + PushToServices((s) => + s.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType)); + return Task.CompletedTask; + } + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs index dd70a72539..c699660a5e 100644 --- a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs +++ b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs @@ -12,8 +12,14 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Services; +/// +/// Sends non-mobile push notifications to the Azure Queue Api, later received by Notifications Api. +/// Used by Cloud-Hosted environments. +/// Received by AzureQueueHostedService message receiver in Notifications project. +/// public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService { + private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public NotificationsApiPushNotificationService( @@ -30,6 +36,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService globalSettings.InternalIdentityKey, logger) { + _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; } @@ -179,6 +186,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate }; @@ -193,6 +201,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate, ReadDate = notificationStatus?.ReadDate, @@ -236,6 +245,11 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService return currentContext?.DeviceIdentifier; } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + // Noop + Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Services/Implementations/RelayPushNotificationService.cs b/src/Core/Services/Implementations/RelayPushNotificationService.cs index 6c1600da48..73d7c529ce 100644 --- a/src/Core/Services/Implementations/RelayPushNotificationService.cs +++ b/src/Core/Services/Implementations/RelayPushNotificationService.cs @@ -15,9 +15,15 @@ using Microsoft.Extensions.Logging; namespace Bit.Core.Services; +/// +/// Sends mobile push notifications to the Bitwarden Cloud API, then relayed to Azure Notification Hub. +/// Used by Self-Hosted environments. +/// Received by PushController endpoint in Api project. +/// public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService { private readonly IDeviceRepository _deviceRepository; + private readonly IGlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; public RelayPushNotificationService( @@ -36,6 +42,7 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti logger) { _deviceRepository = deviceRepository; + _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; } @@ -197,18 +204,25 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate }; - if (notification.UserId.HasValue) + if (notification.Global) + { + await SendPayloadToInstallationAsync(PushType.SyncNotificationCreate, message, true, + notification.ClientType); + } + else if (notification.UserId.HasValue) { await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationCreate, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationCreate, message, + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationCreate, + message, true, notification.ClientType); } } @@ -220,24 +234,45 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti Id = notification.Id, UserId = notification.UserId, OrganizationId = notification.OrganizationId, + InstallationId = notification.Global ? _globalSettings.Installation.Id : null, ClientType = notification.ClientType, RevisionDate = notification.RevisionDate, ReadDate = notificationStatus?.ReadDate, DeletedDate = notificationStatus?.DeletedDate }; - if (notification.UserId.HasValue) + if (notification.Global) + { + await SendPayloadToInstallationAsync(PushType.SyncNotificationUpdate, message, true, + notification.ClientType); + } + else if (notification.UserId.HasValue) { await SendPayloadToUserAsync(notification.UserId.Value, PushType.SyncNotificationUpdate, message, true, notification.ClientType); } else if (notification.OrganizationId.HasValue) { - await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationUpdate, message, - true, notification.ClientType); + await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.SyncNotificationUpdate, + message, true, notification.ClientType); } } + private async Task SendPayloadToInstallationAsync(PushType type, object payload, bool excludeCurrentContext, + ClientType? clientType = null) + { + var request = new PushSendRequestModel + { + InstallationId = _globalSettings.Installation.Id.ToString(), + Type = type, + Payload = payload, + ClientType = clientType + }; + + await AddCurrentContextAsync(request, excludeCurrentContext); + await SendAsync(HttpMethod.Post, "push/send", request); + } + private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext, ClientType? clientType = null) { @@ -287,6 +322,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti } } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => + throw new NotImplementedException(); + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { diff --git a/src/Core/Services/Implementations/RelayPushRegistrationService.cs b/src/Core/Services/Implementations/RelayPushRegistrationService.cs index 3000f1545b..dce0581fcb 100644 --- a/src/Core/Services/Implementations/RelayPushRegistrationService.cs +++ b/src/Core/Services/Implementations/RelayPushRegistrationService.cs @@ -25,7 +25,7 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, string installationId) { var requestModel = new PushRegistrationRequestModel { @@ -34,7 +34,8 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi PushToken = pushToken, Type = type, UserId = userId, - OrganizationIds = organizationIds + OrganizationIds = organizationIds, + InstallationId = installationId }; await SendAsync(HttpMethod.Post, "push/register", requestModel); } diff --git a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs index 548c6a159e..5cbbd31c69 100644 --- a/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushNotificationService.cs @@ -100,6 +100,9 @@ public class NoopPushNotificationService : IPushNotificationService return Task.FromResult(0); } + public Task SendPayloadToInstallationAsync(string installationId, PushType type, object payload, string? identifier, + string? deviceId = null, ClientType? clientType = null) => Task.CompletedTask; + public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string? identifier, string? deviceId = null, ClientType? clientType = null) { @@ -108,5 +111,6 @@ public class NoopPushNotificationService : IPushNotificationService public Task PushSyncNotificationCreateAsync(Notification notification) => Task.CompletedTask; - public Task PushSyncNotificationUpdateAsync(Notification notification, NotificationStatus? notificationStatus) => Task.CompletedTask; + public Task PushSyncNotificationUpdateAsync(Notification notification, NotificationStatus? notificationStatus) => + Task.CompletedTask; } diff --git a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs index 4db799580b..6a36e04d13 100644 --- a/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs +++ b/src/Core/Services/NoopImplementations/NoopPushRegistrationService.cs @@ -10,7 +10,7 @@ public class NoopPushRegistrationService : IPushRegistrationService } public Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, - string identifier, DeviceType type, IEnumerable organizationIds) + string identifier, DeviceType type, IEnumerable organizationIds, string installationId) { return Task.FromResult(0); } diff --git a/src/Notifications/HubHelpers.cs b/src/Notifications/HubHelpers.cs index 7047db0734..c307a1b62f 100644 --- a/src/Notifications/HubHelpers.cs +++ b/src/Notifications/HubHelpers.cs @@ -94,7 +94,13 @@ public static class HubHelpers var syncNotification = JsonSerializer.Deserialize>( notificationJson, _deserializerOptions); - if (syncNotification.Payload.UserId.HasValue) + if (syncNotification.Payload.InstallationId.HasValue) + { + await hubContext.Clients.Group(NotificationsHub.GetInstallationGroup( + syncNotification.Payload.InstallationId.Value, syncNotification.Payload.ClientType)) + .SendAsync(_receiveMessageMethod, syncNotification, cancellationToken); + } + else if (syncNotification.Payload.UserId.HasValue) { if (syncNotification.Payload.ClientType == ClientType.All) { diff --git a/src/Notifications/NotificationsHub.cs b/src/Notifications/NotificationsHub.cs index 27cd19c0a0..ba328de7af 100644 --- a/src/Notifications/NotificationsHub.cs +++ b/src/Notifications/NotificationsHub.cs @@ -29,6 +29,17 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub await Groups.AddToGroupAsync(Context.ConnectionId, GetUserGroup(currentContext.UserId.Value, clientType)); } + if (currentContext.InstallationId.HasValue) + { + await Groups.AddToGroupAsync(Context.ConnectionId, + GetInstallationGroup(currentContext.InstallationId.Value)); + if (clientType != ClientType.All) + { + await Groups.AddToGroupAsync(Context.ConnectionId, + GetInstallationGroup(currentContext.InstallationId.Value, clientType)); + } + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) @@ -57,6 +68,17 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub GetUserGroup(currentContext.UserId.Value, clientType)); } + if (currentContext.InstallationId.HasValue) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, + GetInstallationGroup(currentContext.InstallationId.Value)); + if (clientType != ClientType.All) + { + await Groups.RemoveFromGroupAsync(Context.ConnectionId, + GetInstallationGroup(currentContext.InstallationId.Value, clientType)); + } + } + if (currentContext.Organizations != null) { foreach (var org in currentContext.Organizations) @@ -73,6 +95,13 @@ public class NotificationsHub : Microsoft.AspNetCore.SignalR.Hub await base.OnDisconnectedAsync(exception); } + public static string GetInstallationGroup(Guid installationId, ClientType? clientType = null) + { + return clientType is null or ClientType.All + ? $"Installation_{installationId}" + : $"Installation_ClientType_{installationId}_{clientType}"; + } + public static string GetUserGroup(Guid userId, ClientType clientType) { return $"UserClientType_{userId}_{clientType}"; diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index e99181b83c..1054bde8cd 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -272,9 +272,13 @@ public static class ServiceCollectionExtensions services.AddSingleton(); if (globalSettings.SelfHosted) { + if (globalSettings.Installation.Id == default) + { + throw new InvalidOperationException("Installation Id must be set for self-hosted installations."); + } + if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) && - globalSettings.Installation?.Id != null && - CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) + CoreHelpers.SettingHasValue(globalSettings.Installation.Key)) { services.AddKeyedSingleton("implementation"); services.AddSingleton(); @@ -290,7 +294,7 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("implementation"); } } - else if (!globalSettings.SelfHosted) + else { services.AddSingleton(); services.AddSingleton(); diff --git a/test/Api.Test/Controllers/PushControllerTests.cs b/test/Api.Test/Controllers/PushControllerTests.cs index 8420a6bd23..f8751d8bef 100644 --- a/test/Api.Test/Controllers/PushControllerTests.cs +++ b/test/Api.Test/Controllers/PushControllerTests.cs @@ -5,11 +5,11 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Api; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -using GlobalSettings = Bit.Core.Settings.GlobalSettings; namespace Bit.Api.Test.Controllers; @@ -24,18 +24,19 @@ public class PushControllerTests public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted, SutProvider sutProvider, Guid installationId, Guid userId, Guid organizationId) { - sutProvider.GetDependency().SelfHosted = selfHosted; + sutProvider.GetDependency().SelfHosted = selfHosted; if (haveInstallationId) { sutProvider.GetDependency().InstallationId.Returns(installationId); } var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.PostSend(new PushSendRequestModel + sutProvider.Sut.SendAsync(new PushSendRequestModel { Type = PushType.SyncNotificationCreate, UserId = userId.ToString(), OrganizationId = organizationId.ToString(), + InstallationId = installationId.ToString(), Payload = "test-payload" })); @@ -47,21 +48,25 @@ public class PushControllerTests await sutProvider.GetDependency().Received(0) .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); } [Theory] [BitAutoData] - public async Task SendAsync_UserIdAndOrganizationIdEmpty_NoPushNotificationSent( + public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent( SutProvider sutProvider, Guid installationId) { - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().SelfHosted = false; sutProvider.GetDependency().InstallationId.Returns(installationId); - await sutProvider.Sut.PostSend(new PushSendRequestModel + await sutProvider.Sut.SendAsync(new PushSendRequestModel { Type = PushType.SyncNotificationCreate, UserId = null, OrganizationId = null, + InstallationId = null, Payload = "test-payload" }); @@ -71,33 +76,30 @@ public class PushControllerTests await sutProvider.GetDependency().Received(0) .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); } [Theory] - [BitAutoData(true, true, false)] - [BitAutoData(true, true, true)] - [BitAutoData(true, false, true)] - [BitAutoData(true, false, false)] - [BitAutoData(false, true, true)] - [BitAutoData(false, true, false)] - [BitAutoData(false, false, true)] - [BitAutoData(false, false, false)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId, bool haveOrganizationId, SutProvider sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId) { - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().SelfHosted = false; sutProvider.GetDependency().InstallationId.Returns(installationId); var expectedUserId = $"{installationId}_{userId}"; var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; - await sutProvider.Sut.PostSend(new PushSendRequestModel + await sutProvider.Sut.SendAsync(new PushSendRequestModel { Type = PushType.SyncNotificationCreate, UserId = userId.ToString(), OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null, + InstallationId = null, Payload = "test-payload", DeviceId = haveDeviceId ? deviceId.ToString() : null, Identifier = haveIdentifier ? identifier.ToString() : null, @@ -110,29 +112,30 @@ public class PushControllerTests await sutProvider.GetDependency().Received(0) .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); } [Theory] - [BitAutoData(true, true)] - [BitAutoData(true, false)] - [BitAutoData(false, true)] - [BitAutoData(false, false)] + [RepeatingPatternBitAutoData([false, true], [false, true])] public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId, SutProvider sutProvider, Guid installationId, Guid organizationId, Guid identifier, Guid deviceId) { - sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().SelfHosted = false; sutProvider.GetDependency().InstallationId.Returns(installationId); var expectedOrganizationId = $"{installationId}_{organizationId}"; var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; - await sutProvider.Sut.PostSend(new PushSendRequestModel + await sutProvider.Sut.SendAsync(new PushSendRequestModel { Type = PushType.SyncNotificationCreate, UserId = null, OrganizationId = organizationId.ToString(), + InstallationId = null, Payload = "test-payload", DeviceId = haveDeviceId ? deviceId.ToString() : null, Identifier = haveIdentifier ? identifier.ToString() : null, @@ -145,5 +148,141 @@ public class PushControllerTests await sutProvider.GetDependency().Received(0) .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], [false, true])] + public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId, + SutProvider sutProvider, Guid installationId, Guid identifier, Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null; + var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null; + + await sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.SyncNotificationCreate, + UserId = null, + OrganizationId = null, + InstallationId = installationId.ToString(), + Payload = "test-payload", + DeviceId = haveDeviceId ? deviceId.ToString() : null, + Identifier = haveIdentifier ? identifier.ToString() : null, + ClientType = ClientType.All, + }); + + await sutProvider.GetDependency().Received(1) + .SendPayloadToInstallationAsync(installationId.ToString(), PushType.SyncNotificationCreate, "test-payload", + expectedIdentifier, expectedDeviceId, ClientType.All); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider sutProvider, + Guid installationId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendAsync(new PushSendRequestModel + { + Type = PushType.SyncNotificationCreate, + UserId = null, + OrganizationId = null, + InstallationId = Guid.NewGuid().ToString(), + Payload = "test-payload", + DeviceId = null, + Identifier = null, + ClientType = ClientType.All, + })); + + Assert.Equal("InstallationId does not match current context.", exception.Message); + + await sutProvider.GetDependency().Received(0) + .SendPayloadToInstallationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToOrganizationAsync(Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency().Received(0) + .SendPayloadToUserAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(false, true)] + [BitAutoData(false, false)] + [BitAutoData(true, true)] + public async Task RegisterAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, + bool selfHosted, + SutProvider sutProvider, Guid installationId, Guid userId, Guid identifier, Guid deviceId) + { + sutProvider.GetDependency().SelfHosted = selfHosted; + if (haveInstallationId) + { + sutProvider.GetDependency().InstallationId.Returns(installationId); + } + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel + { + DeviceId = deviceId.ToString(), + PushToken = "test-push-token", + UserId = userId.ToString(), + Type = DeviceType.Android, + Identifier = identifier.ToString() + })); + + Assert.Equal("Not correctly configured for push relays.", exception.Message); + + await sutProvider.GetDependency().Received(0) + .CreateOrUpdateRegistrationAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), + Arg.Any(), Arg.Any>(), Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task? RegisterAsync_ValidModel_CreatedOrUpdatedRegistration(SutProvider sutProvider, + Guid installationId, Guid userId, Guid identifier, Guid deviceId, Guid organizationId) + { + sutProvider.GetDependency().SelfHosted = false; + sutProvider.GetDependency().InstallationId.Returns(installationId); + + var expectedUserId = $"{installationId}_{userId}"; + var expectedIdentifier = $"{installationId}_{identifier}"; + var expectedDeviceId = $"{installationId}_{deviceId}"; + var expectedOrganizationId = $"{installationId}_{organizationId}"; + + await sutProvider.Sut.RegisterAsync(new PushRegistrationRequestModel + { + DeviceId = deviceId.ToString(), + PushToken = "test-push-token", + UserId = userId.ToString(), + Type = DeviceType.Android, + Identifier = identifier.ToString(), + OrganizationIds = [organizationId.ToString()], + InstallationId = installationId.ToString() + }); + + await sutProvider.GetDependency().Received(1) + .CreateOrUpdateRegistrationAsync("test-push-token", expectedDeviceId, expectedUserId, + expectedIdentifier, DeviceType.Android, Arg.Do>(organizationIds => + { + var organizationIdsList = organizationIds.ToList(); + Assert.Contains(expectedOrganizationId, organizationIdsList); + Assert.Single(organizationIdsList); + }), installationId.ToString()); } } diff --git a/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs new file mode 100644 index 0000000000..d3721c37cc --- /dev/null +++ b/test/Common/AutoFixture/Attributes/RepeatingPatternBitAutoDataAttribute.cs @@ -0,0 +1,62 @@ +using System.Reflection; + +namespace Bit.Test.Common.AutoFixture.Attributes; + +public class RepeatingPatternBitAutoDataAttribute : BitAutoDataAttribute +{ + private readonly List _repeatingDataList; + + public RepeatingPatternBitAutoDataAttribute(object[] first) + { + _repeatingDataList = AllValues([first]); + } + + public RepeatingPatternBitAutoDataAttribute(object[] first, object[] second) + { + _repeatingDataList = AllValues([first, second]); + } + + public RepeatingPatternBitAutoDataAttribute(object[] first, object[] second, object[] third) + { + _repeatingDataList = AllValues([first, second, third]); + } + + public override IEnumerable GetData(MethodInfo testMethod) + { + foreach (var repeatingData in _repeatingDataList) + { + var bitData = base.GetData(testMethod).First(); + for (var i = 0; i < repeatingData.Length; i++) + { + bitData[i] = repeatingData[i]; + } + + yield return bitData; + } + } + + private static List AllValues(object[][] parameterToPatternValues) + { + var result = new List(); + GenerateCombinations(parameterToPatternValues, new object[parameterToPatternValues.Length], 0, result); + return result; + } + + private static void GenerateCombinations(object[][] parameterToPatternValues, object[] current, int index, + List result) + { + if (index == current.Length) + { + result.Add((object[])current.Clone()); + return; + } + + var patternValues = parameterToPatternValues[index]; + + foreach (var value in patternValues) + { + current[index] = value; + GenerateCombinations(parameterToPatternValues, current, index + 1, result); + } + } +} diff --git a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs index 41a6c25bf2..2d3dbffcf6 100644 --- a/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs +++ b/test/Core.Test/Models/Api/Request/PushSendRequestModelTests.cs @@ -12,19 +12,15 @@ namespace Bit.Core.Test.Models.Api.Request; public class PushSendRequestModelTests { [Theory] - [InlineData(null, null)] - [InlineData(null, "")] - [InlineData(null, " ")] - [InlineData("", null)] - [InlineData(" ", null)] - [InlineData("", "")] - [InlineData(" ", " ")] - public void Validate_UserIdOrganizationIdNullOrEmpty_Invalid(string? userId, string? organizationId) + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "], [null, "", " "])] + public void Validate_UserIdOrganizationIdInstallationIdNullOrEmpty_Invalid(string? userId, string? organizationId, + string? installationId) { var model = new PushSendRequestModel { UserId = userId, OrganizationId = organizationId, + InstallationId = installationId, Type = PushType.SyncCiphers, Payload = "test" }; @@ -32,7 +28,65 @@ public class PushSendRequestModelTests var results = Validate(model); Assert.Single(results); - Assert.Contains(results, result => result.ErrorMessage == "UserId or OrganizationId is required."); + Assert.Contains(results, + result => result.ErrorMessage == "UserId or OrganizationId or InstallationId is required."); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_UserIdProvidedOrganizationIdInstallationIdNullOrEmpty_Valid(string? organizationId, + string? installationId) + { + var model = new PushSendRequestModel + { + UserId = Guid.NewGuid().ToString(), + OrganizationId = organizationId, + InstallationId = installationId, + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_OrganizationIdProvidedUserIdInstallationIdNullOrEmpty_Valid(string? userId, + string? installationId) + { + var model = new PushSendRequestModel + { + UserId = userId, + OrganizationId = Guid.NewGuid().ToString(), + InstallationId = installationId, + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); + } + + [Theory] + [RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])] + public void Validate_InstallationIdProvidedUserIdOrganizationIdNullOrEmpty_Valid(string? userId, + string? organizationId) + { + var model = new PushSendRequestModel + { + UserId = userId, + OrganizationId = organizationId, + InstallationId = Guid.NewGuid().ToString(), + Type = PushType.SyncCiphers, + Payload = "test" + }; + + var results = Validate(model); + + Assert.Empty(results); } [Theory] diff --git a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs index e9b5557d46..b46e18389a 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushNotificationServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Models.Data; using Bit.Core.NotificationCenter.Entities; using Bit.Core.NotificationHub; using Bit.Core.Repositories; +using Bit.Core.Settings; using Bit.Core.Test.NotificationCenter.AutoFixture; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -21,9 +22,11 @@ public class NotificationHubPushNotificationServiceTests [Theory] [BitAutoData] [NotificationCustomize] - public async Task PushSyncNotificationCreateAsync_Global_NotSent( + public async Task PushSyncNotificationCreateAsync_GlobalInstallationIdDefault_NotSent( SutProvider sutProvider, Notification notification) { + sutProvider.GetDependency().Installation.Id = default; + await sutProvider.Sut.PushSyncNotificationCreateAsync(notification); await sutProvider.GetDependency() @@ -36,6 +39,53 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } + [Theory] + [BitAutoData] + [NotificationCustomize] + public async Task PushSyncNotificationCreateAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( + SutProvider sutProvider, Notification notification, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = ClientType.All; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification, null); + expectedSyncNotification.InstallationId = installationId; + + await sutProvider.Sut.PushSyncNotificationCreateAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationCreate, + expectedSyncNotification, + $"(template:payload && installationId:{installationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Web)] + [BitAutoData(ClientType.Mobile)] + [NotificationCustomize] + public async Task + PushSyncNotificationCreateAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( + ClientType clientType, SutProvider sutProvider, + Notification notification, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = clientType; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification, null); + expectedSyncNotification.InstallationId = installationId; + + await sutProvider.Sut.PushSyncNotificationCreateAsync(notification); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationCreate, + expectedSyncNotification, + $"(template:payload && installationId:{installationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + [Theory] [BitAutoData(false)] [BitAutoData(true)] @@ -158,10 +208,12 @@ public class NotificationHubPushNotificationServiceTests [BitAutoData(false)] [BitAutoData(true)] [NotificationCustomize] - public async Task PushSyncNotificationUpdateAsync_Global_NotSent(bool notificationStatusNull, + public async Task PushSyncNotificationUpdateAsync_GlobalInstallationIdDefault_NotSent(bool notificationStatusNull, SutProvider sutProvider, Notification notification, NotificationStatus notificationStatus) { + sutProvider.GetDependency().Installation.Id = default; + await sutProvider.Sut.PushSyncNotificationUpdateAsync(notification, notificationStatusNull ? null : notificationStatus); @@ -176,10 +228,59 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize] + public async Task PushSyncNotificationUpdateAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId( + bool notificationStatusNull, SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = ClientType.All; + + var expectedNotificationStatus = notificationStatusNull ? null : notificationStatus; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification, expectedNotificationStatus); + expectedSyncNotification.InstallationId = installationId; + + await sutProvider.Sut.PushSyncNotificationUpdateAsync(notification, expectedNotificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationUpdate, + expectedSyncNotification, + $"(template:payload && installationId:{installationId})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], + [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Mobile])] + [NotificationCustomize] + public async Task + PushSyncNotificationUpdateAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType( + bool notificationStatusNull, ClientType clientType, + SutProvider sutProvider, + Notification notification, NotificationStatus notificationStatus, Guid installationId) + { + sutProvider.GetDependency().Installation.Id = installationId; + notification.ClientType = clientType; + + var expectedNotificationStatus = notificationStatusNull ? null : notificationStatus; + var expectedSyncNotification = ToSyncNotificationPushNotification(notification, expectedNotificationStatus); + expectedSyncNotification.InstallationId = installationId; + + await sutProvider.Sut.PushSyncNotificationUpdateAsync(notification, expectedNotificationStatus); + + await AssertSendTemplateNotificationAsync(sutProvider, PushType.SyncNotificationUpdate, + expectedSyncNotification, + $"(template:payload && installationId:{installationId} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [RepeatingPatternBitAutoData([false, true], [false, true])] [NotificationCustomize(false)] public async Task PushSyncNotificationUpdateAsync_UserIdProvidedClientTypeAll_SentToUser( bool organizationIdNull, bool notificationStatusNull, @@ -206,14 +307,8 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [BitAutoData(false, ClientType.Browser)] - [BitAutoData(false, ClientType.Desktop)] - [BitAutoData(false, ClientType.Web)] - [BitAutoData(false, ClientType.Mobile)] - [BitAutoData(true, ClientType.Browser)] - [BitAutoData(true, ClientType.Desktop)] - [BitAutoData(true, ClientType.Web)] - [BitAutoData(true, ClientType.Mobile)] + [RepeatingPatternBitAutoData([false, true], + [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Mobile])] [NotificationCustomize(false)] public async Task PushSyncNotificationUpdateAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser( bool notificationStatusNull, ClientType clientType, @@ -236,14 +331,8 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [BitAutoData(false, ClientType.Browser)] - [BitAutoData(false, ClientType.Desktop)] - [BitAutoData(false, ClientType.Web)] - [BitAutoData(false, ClientType.Mobile)] - [BitAutoData(true, ClientType.Browser)] - [BitAutoData(true, ClientType.Desktop)] - [BitAutoData(true, ClientType.Web)] - [BitAutoData(true, ClientType.Mobile)] + [RepeatingPatternBitAutoData([false, true], + [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Mobile])] [NotificationCustomize(false)] public async Task PushSyncNotificationUpdateAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser( bool notificationStatusNull, ClientType clientType, @@ -288,14 +377,8 @@ public class NotificationHubPushNotificationServiceTests } [Theory] - [BitAutoData(false, ClientType.Browser)] - [BitAutoData(false, ClientType.Desktop)] - [BitAutoData(false, ClientType.Web)] - [BitAutoData(false, ClientType.Mobile)] - [BitAutoData(true, ClientType.Browser)] - [BitAutoData(true, ClientType.Desktop)] - [BitAutoData(true, ClientType.Web)] - [BitAutoData(true, ClientType.Mobile)] + [RepeatingPatternBitAutoData([false, true], + [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Mobile])] [NotificationCustomize(false)] public async Task PushSyncNotificationUpdateAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization( @@ -390,6 +473,42 @@ public class NotificationHubPushNotificationServiceTests .UpsertAsync(Arg.Any()); } + [Theory] + [BitAutoData([null])] + [BitAutoData(ClientType.All)] + public async Task SendPayloadToInstallationAsync_ClientTypeNullOrAll_SentToInstallation(ClientType? clientType, + SutProvider sutProvider, Guid installationId, PushType pushType, + string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + + [Theory] + [BitAutoData(ClientType.Browser)] + [BitAutoData(ClientType.Desktop)] + [BitAutoData(ClientType.Mobile)] + [BitAutoData(ClientType.Web)] + public async Task SendPayloadToInstallationAsync_ClientTypeExplicit_SentToInstallationAndClientType( + ClientType clientType, SutProvider sutProvider, Guid installationId, + PushType pushType, string payload, string identifier) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId.ToString(), pushType, payload, identifier, + null, clientType); + + await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload, + $"(template:payload && installationId:{installationId} && !deviceIdentifier:{identifier} && clientType:{clientType})"); + await sutProvider.GetDependency() + .Received(0) + .UpsertAsync(Arg.Any()); + } + private static SyncNotificationPushNotification ToSyncNotificationPushNotification(Notification notification, NotificationStatus? notificationStatus) => new() diff --git a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs index d51df9c882..b03d3ba97c 100644 --- a/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs +++ b/test/Core.Test/NotificationHub/NotificationHubPushRegistrationServiceTests.cs @@ -14,15 +14,13 @@ namespace Bit.Core.Test.NotificationHub; public class NotificationHubPushRegistrationServiceTests { [Theory] - [BitAutoData([null])] - [BitAutoData("")] - [BitAutoData(" ")] + [RepeatingPatternBitAutoData([null, "", " "])] public async Task CreateOrUpdateRegistrationAsync_PushTokenNullOrEmpty_InstallationNotCreated(string? pushToken, SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, - Guid organizationId) + Guid organizationId, Guid installationId) { await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), - identifier.ToString(), DeviceType.Android, [organizationId.ToString()]); + identifier.ToString(), DeviceType.Android, [organizationId.ToString()], installationId.ToString()); sutProvider.GetDependency() .Received(0) @@ -30,13 +28,11 @@ public class NotificationHubPushRegistrationServiceTests } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroid_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid? identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid? identifier, + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -45,7 +41,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.Android, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? null : installationId.ToString()); sutProvider.GetDependency() .Received(1) @@ -60,6 +57,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -73,6 +71,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -86,6 +85,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -99,17 +99,16 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeIOS_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -118,7 +117,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.iOS, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? null : installationId.ToString()); sutProvider.GetDependency() .Received(1) @@ -133,6 +133,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -146,6 +147,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -159,6 +161,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -172,17 +175,16 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } [Theory] - [BitAutoData(false, false)] - [BitAutoData(false, true)] - [BitAutoData(true, false)] - [BitAutoData(true, true)] + [RepeatingPatternBitAutoData([false, true], [false, true], [false, true])] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeAndroidAmazon_InstallationCreated(bool identifierNull, - bool partOfOrganizationId, SutProvider sutProvider, Guid deviceId, - Guid userId, Guid identifier, Guid organizationId) + bool partOfOrganizationId, bool installationIdNull, + SutProvider sutProvider, Guid deviceId, + Guid userId, Guid identifier, Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -191,7 +193,8 @@ public class NotificationHubPushRegistrationServiceTests await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), identifierNull ? null : identifier.ToString(), DeviceType.AndroidAmazon, - partOfOrganizationId ? [organizationId.ToString()] : []); + partOfOrganizationId ? [organizationId.ToString()] : [], + installationIdNull ? null : installationId.ToString()); sutProvider.GetDependency() .Received(1) @@ -206,6 +209,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains("clientType:Mobile") && (identifierNull || installation.Tags.Contains($"deviceIdentifier:{identifier}")) && (!partOfOrganizationId || installation.Tags.Contains($"organizationId:{organizationId}")) && + (installationIdNull || installation.Tags.Contains($"installationId:{installationId}")) && installation.Templates.Count == 3)); await notificationHubClient .Received(1) @@ -219,6 +223,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:payload_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -232,6 +237,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:message_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); await notificationHubClient .Received(1) @@ -245,6 +251,7 @@ public class NotificationHubPushRegistrationServiceTests "clientType:Mobile", identifierNull ? null : $"template:badgeMessage_deviceIdentifier:{identifier}", partOfOrganizationId ? $"organizationId:{organizationId}" : null, + installationIdNull ? null : $"installationId:{installationId}", }))); } @@ -254,7 +261,7 @@ public class NotificationHubPushRegistrationServiceTests [BitAutoData(DeviceType.MacOsDesktop)] public async Task CreateOrUpdateRegistrationAsync_DeviceTypeNotMobile_InstallationCreated(DeviceType deviceType, SutProvider sutProvider, Guid deviceId, Guid userId, Guid identifier, - Guid organizationId) + Guid organizationId, Guid installationId) { var notificationHubClient = Substitute.For(); sutProvider.GetDependency().ClientFor(Arg.Any()).Returns(notificationHubClient); @@ -262,7 +269,7 @@ public class NotificationHubPushRegistrationServiceTests var pushToken = "test push token"; await sutProvider.Sut.CreateOrUpdateRegistrationAsync(pushToken, deviceId.ToString(), userId.ToString(), - identifier.ToString(), deviceType, [organizationId.ToString()]); + identifier.ToString(), deviceType, [organizationId.ToString()], installationId.ToString()); sutProvider.GetDependency() .Received(1) @@ -276,6 +283,7 @@ public class NotificationHubPushRegistrationServiceTests installation.Tags.Contains($"clientType:{DeviceTypes.ToClientType(deviceType)}") && installation.Tags.Contains($"deviceIdentifier:{identifier}") && installation.Tags.Contains($"organizationId:{organizationId}") && + installation.Tags.Contains($"installationId:{installationId}") && installation.Templates.Count == 0)); } diff --git a/test/Core.Test/Services/AzureQueuePushNotificationServiceTests.cs b/test/Core.Test/Services/AzureQueuePushNotificationServiceTests.cs index 3907d34c50..70d5e95252 100644 --- a/test/Core.Test/Services/AzureQueuePushNotificationServiceTests.cs +++ b/test/Core.Test/Services/AzureQueuePushNotificationServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Enums; using Bit.Core.Models; using Bit.Core.NotificationCenter.Entities; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.CurrentContextFixtures; using Bit.Core.Test.NotificationCenter.AutoFixture; @@ -25,19 +26,43 @@ public class AzureQueuePushNotificationServiceTests [BitAutoData] [NotificationCustomize] [CurrentContextCustomize] - public async Task PushSyncNotificationCreateAsync_Notification_Sent( + public async Task PushSyncNotificationCreateAsync_NotificationGlobal_Sent( SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext) + ICurrentContext currentContext, Guid installationId) { currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); sutProvider.GetDependency().HttpContext!.RequestServices .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; await sutProvider.Sut.PushSyncNotificationCreateAsync(notification); await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => - MatchMessage(PushType.SyncNotificationCreate, message, new SyncNotificationEquals(notification, null), + MatchMessage(PushType.SyncNotificationCreate, message, + new SyncNotificationEquals(notification, null, installationId), + deviceIdentifier.ToString()))); + } + + [Theory] + [BitAutoData] + [NotificationCustomize(false)] + [CurrentContextCustomize] + public async Task PushSyncNotificationCreateAsync_NotificationNotGlobal_Sent( + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, Guid installationId) + { + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; + + await sutProvider.Sut.PushSyncNotificationCreateAsync(notification); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.SyncNotificationCreate, message, + new SyncNotificationEquals(notification, null, null), deviceIdentifier.ToString()))); } @@ -47,21 +72,47 @@ public class AzureQueuePushNotificationServiceTests [NotificationCustomize] [NotificationStatusCustomize] [CurrentContextCustomize] - public async Task PushSyncNotificationUpdateAsync_Notification_Sent(bool notificationStatusNull, + public async Task PushSyncNotificationUpdateAsync_NotificationGlobal_Sent(bool notificationStatusNull, SutProvider sutProvider, Notification notification, Guid deviceIdentifier, - ICurrentContext currentContext, NotificationStatus notificationStatus) + ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) { var expectedNotificationStatus = notificationStatusNull ? null : notificationStatus; currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); sutProvider.GetDependency().HttpContext!.RequestServices .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; await sutProvider.Sut.PushSyncNotificationUpdateAsync(notification, expectedNotificationStatus); await sutProvider.GetDependency().Received(1) .SendMessageAsync(Arg.Is(message => MatchMessage(PushType.SyncNotificationUpdate, message, - new SyncNotificationEquals(notification, expectedNotificationStatus), + new SyncNotificationEquals(notification, expectedNotificationStatus, installationId), + deviceIdentifier.ToString()))); + } + + [Theory] + [BitAutoData(false)] + [BitAutoData(true)] + [NotificationCustomize(false)] + [NotificationStatusCustomize] + [CurrentContextCustomize] + public async Task PushSyncNotificationUpdateAsync_NotificationNotGlobal_Sent(bool notificationStatusNull, + SutProvider sutProvider, Notification notification, Guid deviceIdentifier, + ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId) + { + var expectedNotificationStatus = notificationStatusNull ? null : notificationStatus; + currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString()); + sutProvider.GetDependency().HttpContext!.RequestServices + .GetService(Arg.Any()).Returns(currentContext); + sutProvider.GetDependency().Installation.Id = installationId; + + await sutProvider.Sut.PushSyncNotificationUpdateAsync(notification, expectedNotificationStatus); + + await sutProvider.GetDependency().Received(1) + .SendMessageAsync(Arg.Is(message => + MatchMessage(PushType.SyncNotificationUpdate, message, + new SyncNotificationEquals(notification, expectedNotificationStatus, null), deviceIdentifier.ToString()))); } @@ -76,7 +127,10 @@ public class AzureQueuePushNotificationServiceTests pushNotificationData.ContextId == contextId; } - private class SyncNotificationEquals(Notification notification, NotificationStatus? notificationStatus) + private class SyncNotificationEquals( + Notification notification, + NotificationStatus? notificationStatus, + Guid? installationId) : IEquatable { public bool Equals(SyncNotificationPushNotification? other) @@ -85,6 +139,7 @@ public class AzureQueuePushNotificationServiceTests other.Id == notification.Id && other.UserId == notification.UserId && other.OrganizationId == notification.OrganizationId && + other.InstallationId == installationId && other.ClientType == notification.ClientType && other.RevisionDate == notification.RevisionDate && other.ReadDate == notificationStatus?.ReadDate && diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index 10aa8761fa..ded994fa1c 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Settings; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -19,7 +20,7 @@ public class DeviceServiceTests [Theory] [BitAutoData] public async Task SaveAsync_IdProvided_UpdatedRevisionDateAndPushRegistration(Guid id, Guid userId, - Guid organizationId1, Guid organizationId2, + Guid organizationId1, Guid organizationId2, Guid installationId, OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) { @@ -31,7 +32,9 @@ public class DeviceServiceTests var organizationUserRepository = Substitute.For(); organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); - var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); + var globalSettings = Substitute.For(); + globalSettings.Installation.Id.Returns(installationId); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); var device = new Device { @@ -53,13 +56,13 @@ public class DeviceServiceTests Assert.Equal(2, organizationIdsList.Count); Assert.Contains(organizationId1.ToString(), organizationIdsList); Assert.Contains(organizationId2.ToString(), organizationIdsList); - })); + }), installationId.ToString()); } [Theory] [BitAutoData] public async Task SaveAsync_IdNotProvided_CreatedAndPushRegistration(Guid userId, Guid organizationId1, - Guid organizationId2, + Guid organizationId2, Guid installationId, OrganizationUserOrganizationDetails organizationUserOrganizationDetails1, OrganizationUserOrganizationDetails organizationUserOrganizationDetails2) { @@ -71,7 +74,9 @@ public class DeviceServiceTests var organizationUserRepository = Substitute.For(); organizationUserRepository.GetManyDetailsByUserAsync(Arg.Any(), Arg.Any()) .Returns([organizationUserOrganizationDetails1, organizationUserOrganizationDetails2]); - var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository); + var globalSettings = Substitute.For(); + globalSettings.Installation.Id.Returns(installationId); + var deviceService = new DeviceService(deviceRepo, pushRepo, organizationUserRepository, globalSettings); var device = new Device { @@ -91,7 +96,7 @@ public class DeviceServiceTests Assert.Equal(2, organizationIdsList.Count); Assert.Contains(organizationId1.ToString(), organizationIdsList); Assert.Contains(organizationId2.ToString(), organizationIdsList); - })); + }), installationId.ToString()); } /// diff --git a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs index bd8bfeeba4..db6661899f 100644 --- a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs @@ -79,4 +79,22 @@ public class MultiServicePushNotificationServiceTests .Received(1) .SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId, clientType); } + + [Theory] + [BitAutoData([null, null])] + [BitAutoData(ClientType.All, null)] + [BitAutoData([null, "test device id"])] + [BitAutoData(ClientType.All, "test device id")] + public async Task SendPayloadToInstallationAsync_Message_Sent(ClientType? clientType, string? deviceId, + string installationId, PushType type, object payload, string identifier, + SutProvider sutProvider) + { + await sutProvider.Sut.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, + clientType); + + await sutProvider.GetDependency>() + .First() + .Received(1) + .SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType); + } }