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); + } }