1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-18 01:53:49 -05:00

[PM-18555] Main part of notifications refactor (#5757)

* More tests

* More  tests

* Add non-guid tests

* Introduce slimmer services

* Implement IPushEngine on services

* Implement IPushEngine

* Fix tests

* Format

* Switch to `Guid` on `PushSendRequestModel`

* Remove TODOs
This commit is contained in:
Justin Baur 2025-06-17 13:30:56 -04:00 committed by GitHub
parent 6dc26f4be6
commit 6800bc57f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with 1271 additions and 2408 deletions

View File

@ -1,8 +1,11 @@
using Bit.Core.Context;
using System.Diagnostics;
using System.Text.Json;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
@ -20,14 +23,14 @@ namespace Bit.Api.Platform.Push;
public class PushController : Controller
{
private readonly IPushRegistrationService _pushRegistrationService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRelayer _pushRelayer;
private readonly IWebHostEnvironment _environment;
private readonly ICurrentContext _currentContext;
private readonly IGlobalSettings _globalSettings;
public PushController(
IPushRegistrationService pushRegistrationService,
IPushNotificationService pushNotificationService,
IPushRelayer pushRelayer,
IWebHostEnvironment environment,
ICurrentContext currentContext,
IGlobalSettings globalSettings)
@ -35,7 +38,7 @@ public class PushController : Controller
_currentContext = currentContext;
_environment = environment;
_pushRegistrationService = pushRegistrationService;
_pushNotificationService = pushNotificationService;
_pushRelayer = pushRelayer;
_globalSettings = globalSettings;
}
@ -74,31 +77,50 @@ public class PushController : Controller
}
[HttpPost("send")]
public async Task SendAsync([FromBody] PushSendRequestModel model)
public async Task SendAsync([FromBody] PushSendRequestModel<JsonElement> model)
{
CheckUsage();
if (!string.IsNullOrWhiteSpace(model.InstallationId))
NotificationTarget target;
Guid targetId;
if (model.InstallationId.HasValue)
{
if (_currentContext.InstallationId!.Value.ToString() != model.InstallationId!)
if (_currentContext.InstallationId!.Value != model.InstallationId.Value)
{
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);
target = NotificationTarget.Installation;
targetId = _currentContext.InstallationId.Value;
}
else if (!string.IsNullOrWhiteSpace(model.UserId))
else if (model.UserId.HasValue)
{
await _pushNotificationService.SendPayloadToUserAsync(Prefix(model.UserId),
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
target = NotificationTarget.User;
targetId = model.UserId.Value;
}
else if (!string.IsNullOrWhiteSpace(model.OrganizationId))
else if (model.OrganizationId.HasValue)
{
await _pushNotificationService.SendPayloadToOrganizationAsync(Prefix(model.OrganizationId),
model.Type, model.Payload, Prefix(model.Identifier), Prefix(model.DeviceId), model.ClientType);
target = NotificationTarget.Organization;
targetId = model.OrganizationId.Value;
}
else
{
throw new UnreachableException("Model validation should have prevented getting here.");
}
var notification = new RelayedNotification
{
Type = model.Type,
Target = target,
TargetId = targetId,
Payload = model.Payload,
Identifier = model.Identifier,
DeviceId = model.DeviceId,
ClientType = model.ClientType,
};
await _pushRelayer.RelayAsync(_currentContext.InstallationId.Value, notification);
}
private string Prefix(string value)

View File

@ -4,22 +4,22 @@ using Bit.Core.Enums;
namespace Bit.Core.Models.Api;
public class PushSendRequestModel : IValidatableObject
public class PushSendRequestModel<T> : IValidatableObject
{
public string? UserId { get; set; }
public string? OrganizationId { get; set; }
public string? DeviceId { get; set; }
public Guid? UserId { get; set; }
public Guid? OrganizationId { get; set; }
public Guid? DeviceId { get; set; }
public string? Identifier { get; set; }
public required PushType Type { get; set; }
public required object Payload { get; set; }
public required T Payload { get; set; }
public ClientType? ClientType { get; set; }
public string? InstallationId { get; set; }
public Guid? InstallationId { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrWhiteSpace(UserId) &&
string.IsNullOrWhiteSpace(OrganizationId) &&
string.IsNullOrWhiteSpace(InstallationId))
if (!UserId.HasValue &&
!OrganizationId.HasValue &&
!InstallationId.HasValue)
{
yield return new ValidationResult(
$"{nameof(UserId)} or {nameof(OrganizationId)} or {nameof(InstallationId)} is required.");

View File

@ -1,21 +1,17 @@
#nullable enable
using System.Text.Json;
using System.Text.RegularExpressions;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Notification = Bit.Core.NotificationCenter.Entities.Notification;
namespace Bit.Core.NotificationHub;
@ -26,52 +22,32 @@ namespace Bit.Core.NotificationHub;
/// Used by Cloud-Hosted environments.
/// Received by Firebase for Android or APNS for iOS.
/// </summary>
public class NotificationHubPushNotificationService : IPushNotificationService
public class NotificationHubPushNotificationService : IPushEngine, IPushRelayer
{
private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly bool _enableTracing = false;
private readonly INotificationHubPool _notificationHubPool;
private readonly ILogger _logger;
private readonly IGlobalSettings _globalSettings;
private readonly TimeProvider _timeProvider;
public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository,
INotificationHubPool notificationHubPool,
IHttpContextAccessor httpContextAccessor,
ILogger<NotificationHubPushNotificationService> logger,
IGlobalSettings globalSettings,
TimeProvider timeProvider)
IGlobalSettings globalSettings)
{
_installationDeviceRepository = installationDeviceRepository;
_httpContextAccessor = httpContextAccessor;
_notificationHubPool = notificationHubPool;
_logger = logger;
_globalSettings = globalSettings;
_timeProvider = timeProvider;
if (globalSettings.Installation.Id == Guid.Empty)
{
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
}
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@ -93,311 +69,17 @@ public class NotificationHubPushNotificationService : IPushNotificationService
CollectionIds = collectionIds,
};
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
await PushAsync(new PushNotification<SyncCipherPushNotification>
{
Type = type,
Target = NotificationTarget.User,
TargetId = cipher.UserId.Value,
Payload = message,
ExcludeCurrentContext = true,
});
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
await SendPayloadToUserAsync(folder.UserId, type, message, true);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrganizationsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrganizations);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncSettings);
}
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
{
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
}
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);
}
public async Task PushSyncSendUpdateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendUpdate);
}
public async Task PushSyncSendDeleteAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendDelete);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
{
var message = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate
};
await SendPayloadToUserAsync(message.UserId, type, message, true);
}
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
public async Task PushNotificationAsync(Notification notification)
{
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
? _globalSettings.Installation.Id
: null;
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = installationId,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};
if (notification.Global)
{
if (installationId.HasValue)
{
await SendPayloadToInstallationAsync(installationId.Value, PushType.Notification, message, true,
notification.ClientType);
}
else
{
_logger.LogWarning(
"Invalid global notification id {NotificationId} push notification. No installation id provided.",
notification.Id);
}
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
}
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
Guid? installationId = notification.Global && _globalSettings.Installation.Id != Guid.Empty
? _globalSettings.Installation.Id
: null;
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = installationId,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
if (notification.Global)
{
if (installationId.HasValue)
{
await SendPayloadToInstallationAsync(installationId.Value, PushType.NotificationStatus, message, true,
notification.ClientType);
}
else
{
_logger.LogWarning(
"Invalid global notification status id {NotificationId} push notification. No installation id provided.",
notification.Id);
}
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus,
message, true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
}
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
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)
{
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext),
clientType: clientType);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
await SendPayloadToOrganizationAsync(orgId.ToString(), type, payload,
GetContextIdentifier(excludeCurrentContext), clientType: clientType);
}
public async Task PushPendingSecurityTasksAsync(Guid userId)
{
await PushUserAsync(userId, PushType.PendingSecurityTasks);
}
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)
{
var tag = BuildTag($"template:payload_userId:{SanitizeTagInput(userId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
public async Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
var tag = BuildTag($"template:payload && organizationId:{SanitizeTagInput(orgId)}", identifier, clientType);
await SendPayloadAsync(tag, type, payload);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
}
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
{
var message = new OrganizationStatusPushNotification
{
OrganizationId = organization.Id,
Enabled = organization.Enabled
};
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
}
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
await SendPayloadToOrganizationAsync(
organization.Id,
PushType.SyncOrganizationCollectionSettingChanged,
new OrganizationCollectionManagementPushNotification
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
},
false
);
private string? GetContextIdentifier(bool excludeCurrentContext)
{
if (!excludeCurrentContext)
@ -425,13 +107,73 @@ public class NotificationHubPushNotificationService : IPushNotificationService
return $"({tag})";
}
private async Task SendPayloadAsync(string tag, PushType type, object payload)
public async Task PushAsync<T>(PushNotification<T> pushNotification)
where T : class
{
var initialTag = pushNotification.Target switch
{
NotificationTarget.User => $"template:payload_userId:{pushNotification.TargetId}",
NotificationTarget.Organization => $"template:payload && organizationId:{pushNotification.TargetId}",
NotificationTarget.Installation => $"template:payload && installationId:{pushNotification.TargetId}",
_ => throw new InvalidOperationException($"Push notification target '{pushNotification.Target}' is not valid."),
};
await PushCoreAsync(
initialTag,
GetContextIdentifier(pushNotification.ExcludeCurrentContext),
pushNotification.Type,
pushNotification.ClientType,
pushNotification.Payload
);
}
public async Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification)
{
// Relayed notifications need identifiers prefixed with the installation they are from and a underscore
var initialTag = relayedNotification.Target switch
{
NotificationTarget.User => $"template:payload_userId:{fromInstallation}_{relayedNotification.TargetId}",
NotificationTarget.Organization => $"template:payload && organizationId:{fromInstallation}_{relayedNotification.TargetId}",
NotificationTarget.Installation => $"template:payload && installationId:{fromInstallation}",
_ => throw new InvalidOperationException($"Invalid Notification target {relayedNotification.Target}"),
};
await PushCoreAsync(
initialTag,
relayedNotification.Identifier,
relayedNotification.Type,
relayedNotification.ClientType,
relayedNotification.Payload
);
if (relayedNotification.DeviceId.HasValue)
{
await _installationDeviceRepository.UpsertAsync(
new InstallationDeviceEntity(fromInstallation, relayedNotification.DeviceId.Value)
);
}
else
{
_logger.LogWarning(
"A related notification of type '{Type}' came through without a device id from installation {Installation}",
relayedNotification.Type,
fromInstallation
);
}
}
private async Task PushCoreAsync<T>(string initialTag, string? contextId, PushType pushType, ClientType? clientType, T payload)
{
var finalTag = BuildTag(initialTag, contextId, clientType);
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
new Dictionary<string, string>
{
{ "type", ((byte)type).ToString() }, { "payload", JsonSerializer.Serialize(payload) }
}, tag);
{ "type", ((byte)pushType).ToString() },
{ "payload", JsonSerializer.Serialize(payload) },
},
finalTag
);
if (_enableTracing)
{
@ -444,7 +186,7 @@ public class NotificationHubPushNotificationService : IPushNotificationService
_logger.LogInformation(
"Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
outcome.TrackingId, pushType, outcome.Success, outcome.Failure, payload, outcome.Results);
}
}
}

View File

@ -1,14 +1,10 @@
#nullable enable
using System.Text.Json;
using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
@ -17,12 +13,10 @@ using Microsoft.Extensions.Logging;
namespace Bit.Core.Platform.Push.Internal;
public class AzureQueuePushNotificationService : IPushNotificationService
public class AzureQueuePushNotificationService : IPushEngine
{
private readonly QueueClient _queueClient;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IGlobalSettings _globalSettings;
private readonly TimeProvider _timeProvider;
public AzureQueuePushNotificationService(
[FromKeyedServices("notifications")] QueueClient queueClient,
@ -33,30 +27,13 @@ public class AzureQueuePushNotificationService : IPushNotificationService
{
_queueClient = queueClient;
_httpContextAccessor = httpContextAccessor;
_globalSettings = globalSettings;
_timeProvider = timeProvider;
if (globalSettings.Installation.Id == Guid.Empty)
{
logger.LogWarning("Installation ID is not set. Push notifications for installations will not work.");
}
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@ -83,166 +60,6 @@ public class AzureQueuePushNotificationService : IPushNotificationService
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
await SendMessageAsync(type, message, true);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrganizationsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrganizations);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncSettings);
}
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
{
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
}
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
await SendMessageAsync(type, message, excludeCurrentContext);
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendMessageAsync(type, message, true);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);
}
public async Task PushSyncSendUpdateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendUpdate);
}
public async Task PushSyncSendDeleteAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendDelete);
}
public async Task PushNotificationAsync(Notification notification)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};
await SendMessageAsync(PushType.Notification, message, true);
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
await SendMessageAsync(PushType.NotificationStatus, message, true);
}
public async Task PushPendingSecurityTasksAsync(Guid userId)
{
await PushUserAsync(userId, PushType.PendingSecurityTasks);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
{
var message = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate
};
await SendMessageAsync(type, message, true);
}
}
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
{
var contextId = GetContextIdentifier(excludeCurrentContext);
@ -263,42 +80,9 @@ 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)
public async Task PushAsync<T>(PushNotification<T> pushNotification)
where T : class
{
// Noop
return Task.FromResult(0);
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
{
var message = new OrganizationStatusPushNotification
{
OrganizationId = organization.Id,
Enabled = organization.Enabled
};
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
}
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
new OrganizationCollectionManagementPushNotification
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
}, false);
}

View File

@ -0,0 +1,13 @@
#nullable enable
using Bit.Core.Enums;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Platform.Push;
public interface IPushEngine
{
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
Task PushAsync<T>(PushNotification<T> pushNotification)
where T : class;
}

View File

@ -2,41 +2,410 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Platform.Push;
public interface IPushNotificationService
{
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds);
Task PushSyncCipherDeleteAsync(Cipher cipher);
Task PushSyncFolderCreateAsync(Folder folder);
Task PushSyncFolderUpdateAsync(Folder folder);
Task PushSyncFolderDeleteAsync(Folder folder);
Task PushSyncCiphersAsync(Guid userId);
Task PushSyncVaultAsync(Guid userId);
Task PushSyncOrganizationsAsync(Guid userId);
Task PushSyncOrgKeysAsync(Guid userId);
Task PushSyncSettingsAsync(Guid userId);
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false);
Task PushSyncSendCreateAsync(Send send);
Task PushSyncSendUpdateAsync(Send send);
Task PushSyncSendDeleteAsync(Send send);
Task PushNotificationAsync(Notification notification);
Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus);
Task PushAuthRequestAsync(AuthRequest authRequest);
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
Task PushSyncOrganizationStatusAsync(Organization organization);
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization);
Guid InstallationId { get; }
TimeProvider TimeProvider { get; }
ILogger Logger { get; }
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);
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null);
Task PushPendingSecurityTasksAsync(Guid userId);
#region Legacy method, to be removed soon.
Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
=> PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
=> PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
Task PushSyncCipherDeleteAsync(Cipher cipher)
=> PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
Task PushSyncFolderCreateAsync(Folder folder)
=> PushAsync(new PushNotification<SyncFolderPushNotification>
{
Type = PushType.SyncFolderCreate,
Target = NotificationTarget.User,
TargetId = folder.UserId,
Payload = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate,
},
ExcludeCurrentContext = true,
});
Task PushSyncFolderUpdateAsync(Folder folder)
=> PushAsync(new PushNotification<SyncFolderPushNotification>
{
Type = PushType.SyncFolderUpdate,
Target = NotificationTarget.User,
TargetId = folder.UserId,
Payload = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate,
},
ExcludeCurrentContext = true,
});
Task PushSyncFolderDeleteAsync(Folder folder)
=> PushAsync(new PushNotification<SyncFolderPushNotification>
{
Type = PushType.SyncFolderDelete,
Target = NotificationTarget.User,
TargetId = folder.UserId,
Payload = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate,
},
ExcludeCurrentContext = true,
});
Task PushSyncCiphersAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.SyncCiphers,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
Task PushSyncVaultAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.SyncVault,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
Task PushSyncOrganizationsAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.SyncOrganizations,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
Task PushSyncOrgKeysAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.SyncOrgKeys,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
Task PushSyncSettingsAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.SyncSettings,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
Task PushLogOutAsync(Guid userId, bool excludeCurrentContextFromPush = false)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.LogOut,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = excludeCurrentContextFromPush,
});
Task PushSyncSendCreateAsync(Send send)
{
if (send.UserId.HasValue)
{
return PushAsync(new PushNotification<SyncSendPushNotification>
{
Type = PushType.SyncSendCreate,
Target = NotificationTarget.User,
TargetId = send.UserId.Value,
Payload = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate,
},
ExcludeCurrentContext = true,
});
}
return Task.CompletedTask;
}
Task PushSyncSendUpdateAsync(Send send)
{
if (send.UserId.HasValue)
{
return PushAsync(new PushNotification<SyncSendPushNotification>
{
Type = PushType.SyncSendUpdate,
Target = NotificationTarget.User,
TargetId = send.UserId.Value,
Payload = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate,
},
ExcludeCurrentContext = true,
});
}
return Task.CompletedTask;
}
Task PushSyncSendDeleteAsync(Send send)
{
if (send.UserId.HasValue)
{
return PushAsync(new PushNotification<SyncSendPushNotification>
{
Type = PushType.SyncSendDelete,
Target = NotificationTarget.User,
TargetId = send.UserId.Value,
Payload = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate,
},
ExcludeCurrentContext = true,
});
}
return Task.CompletedTask;
}
Task PushNotificationAsync(Notification notification)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? InstallationId : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
};
NotificationTarget target;
Guid targetId;
if (notification.Global)
{
// TODO: Think about this a bit more
target = NotificationTarget.Installation;
targetId = InstallationId;
}
else if (notification.UserId.HasValue)
{
target = NotificationTarget.User;
targetId = notification.UserId.Value;
}
else if (notification.OrganizationId.HasValue)
{
target = NotificationTarget.Organization;
targetId = notification.OrganizationId.Value;
}
else
{
Logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
return Task.CompletedTask;
}
return PushAsync(new PushNotification<NotificationPushNotification>
{
Type = PushType.Notification,
Target = target,
TargetId = targetId,
Payload = message,
ExcludeCurrentContext = true,
ClientType = notification.ClientType,
});
}
Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? InstallationId : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate,
};
NotificationTarget target;
Guid targetId;
if (notification.Global)
{
// TODO: Think about this a bit more
target = NotificationTarget.Installation;
targetId = InstallationId;
}
else if (notification.UserId.HasValue)
{
target = NotificationTarget.User;
targetId = notification.UserId.Value;
}
else if (notification.OrganizationId.HasValue)
{
target = NotificationTarget.Organization;
targetId = notification.OrganizationId.Value;
}
else
{
Logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
return Task.CompletedTask;
}
return PushAsync(new PushNotification<NotificationPushNotification>
{
Type = PushType.NotificationStatus,
Target = target,
TargetId = targetId,
Payload = message,
ExcludeCurrentContext = true,
ClientType = notification.ClientType,
});
}
Task PushAuthRequestAsync(AuthRequest authRequest)
=> PushAsync(new PushNotification<AuthRequestPushNotification>
{
Type = PushType.AuthRequest,
Target = NotificationTarget.User,
TargetId = authRequest.UserId,
Payload = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId,
},
ExcludeCurrentContext = true,
});
Task PushAuthRequestResponseAsync(AuthRequest authRequest)
=> PushAsync(new PushNotification<AuthRequestPushNotification>
{
Type = PushType.AuthRequestResponse,
Target = NotificationTarget.User,
TargetId = authRequest.UserId,
Payload = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId,
},
ExcludeCurrentContext = true,
});
Task PushSyncOrganizationStatusAsync(Organization organization)
=> PushAsync(new PushNotification<OrganizationStatusPushNotification>
{
Type = PushType.SyncOrganizationStatusChanged,
Target = NotificationTarget.Organization,
TargetId = organization.Id,
Payload = new OrganizationStatusPushNotification
{
OrganizationId = organization.Id,
Enabled = organization.Enabled,
},
ExcludeCurrentContext = false,
});
Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
=> PushAsync(new PushNotification<OrganizationCollectionManagementPushNotification>
{
Type = PushType.SyncOrganizationCollectionSettingChanged,
Target = NotificationTarget.Organization,
TargetId = organization.Id,
Payload = new OrganizationCollectionManagementPushNotification
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion,
},
ExcludeCurrentContext = false,
});
Task PushPendingSecurityTasksAsync(Guid userId)
=> PushAsync(new PushNotification<UserPushNotification>
{
Type = PushType.PendingSecurityTasks,
Target = NotificationTarget.User,
TargetId = userId,
Payload = new UserPushNotification
{
UserId = userId,
Date = TimeProvider.GetUtcNow().UtcDateTime,
},
ExcludeCurrentContext = false,
});
#endregion
Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds);
Task PushAsync<T>(PushNotification<T> pushNotification)
where T : class;
}

View File

@ -0,0 +1,44 @@
#nullable enable
using System.Text.Json;
using Bit.Core.Enums;
namespace Bit.Core.Platform.Push.Internal;
/// <summary>
/// An object encapsulating the information that is available in a notification
/// given to us from a self-hosted installation.
/// </summary>
public class RelayedNotification
{
/// <inheritdoc cref="PushNotification{T}.Type"/>
public required PushType Type { get; init; }
/// <inheritdoc cref="PushNotification{T}.Target"/>
public required NotificationTarget Target { get; init; }
/// <inheritdoc cref="PushNotification{T}.TargetId"/>
public required Guid TargetId { get; init; }
/// <inheritdoc cref="PushNotification{T}.Payload"/>
public required JsonElement Payload { get; init; }
/// <inheritdoc cref="PushNotification{T}.ClientType"/>
public required ClientType? ClientType { get; init; }
public required Guid? DeviceId { get; init; }
public required string? Identifier { get; init; }
}
/// <summary>
/// A service for taking a notification that was relayed to us from a self-hosted installation and
/// will be injested into our infrastructure so that we can get the notification to devices that require
/// cloud interaction.
/// </summary>
/// <remarks>
/// This interface should be treated as internal and not consumed by other teams.
/// </remarks>
public interface IPushRelayer
{
/// <summary>
/// Relays a notification that was received from an authenticated installation into our cloud push notification infrastructure.
/// </summary>
/// <param name="fromInstallation">The authenticated installation this notification came from.</param>
/// <param name="relayedNotification">The information received from the self-hosted installation.</param>
Task RelayAsync(Guid fromInstallation, RelayedNotification relayedNotification);
}

View File

@ -1,202 +1,77 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Platform.Push.Internal;
public class MultiServicePushNotificationService : IPushNotificationService
{
private readonly IEnumerable<IPushNotificationService> _services;
private readonly ILogger<MultiServicePushNotificationService> _logger;
private readonly IEnumerable<IPushEngine> _services;
public Guid InstallationId { get; }
public TimeProvider TimeProvider { get; }
public ILogger Logger { get; }
public MultiServicePushNotificationService(
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
IEnumerable<IPushEngine> services,
ILogger<MultiServicePushNotificationService> logger,
GlobalSettings globalSettings)
GlobalSettings globalSettings,
TimeProvider timeProvider)
{
_services = services;
_logger = logger;
_logger.LogInformation("Hub services: {Services}", _services.Count());
Logger = logger;
Logger.LogInformation("Hub services: {Services}", _services.Count());
globalSettings.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
{
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
Logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
});
InstallationId = globalSettings.Installation.Id;
TimeProvider = timeProvider;
}
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
PushToServices((s) => s.PushSyncCipherCreateAsync(cipher, collectionIds));
return Task.FromResult(0);
}
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
PushToServices((s) => s.PushSyncCipherUpdateAsync(cipher, collectionIds));
return Task.FromResult(0);
}
public Task PushSyncCipherDeleteAsync(Cipher cipher)
{
PushToServices((s) => s.PushSyncCipherDeleteAsync(cipher));
return Task.FromResult(0);
}
public Task PushSyncFolderCreateAsync(Folder folder)
{
PushToServices((s) => s.PushSyncFolderCreateAsync(folder));
return Task.FromResult(0);
}
public Task PushSyncFolderUpdateAsync(Folder folder)
{
PushToServices((s) => s.PushSyncFolderUpdateAsync(folder));
return Task.FromResult(0);
}
public Task PushSyncFolderDeleteAsync(Folder folder)
{
PushToServices((s) => s.PushSyncFolderDeleteAsync(folder));
return Task.FromResult(0);
}
public Task PushSyncCiphersAsync(Guid userId)
{
PushToServices((s) => s.PushSyncCiphersAsync(userId));
return Task.FromResult(0);
}
public Task PushSyncVaultAsync(Guid userId)
{
PushToServices((s) => s.PushSyncVaultAsync(userId));
return Task.FromResult(0);
}
public Task PushSyncOrganizationsAsync(Guid userId)
{
PushToServices((s) => s.PushSyncOrganizationsAsync(userId));
return Task.FromResult(0);
}
public Task PushSyncOrgKeysAsync(Guid userId)
{
PushToServices((s) => s.PushSyncOrgKeysAsync(userId));
return Task.FromResult(0);
}
public Task PushSyncSettingsAsync(Guid userId)
{
PushToServices((s) => s.PushSyncSettingsAsync(userId));
return Task.FromResult(0);
}
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
{
PushToServices((s) => s.PushLogOutAsync(userId, excludeCurrentContext));
return Task.FromResult(0);
}
public Task PushSyncSendCreateAsync(Send send)
{
PushToServices((s) => s.PushSyncSendCreateAsync(send));
return Task.FromResult(0);
}
public Task PushSyncSendUpdateAsync(Send send)
{
PushToServices((s) => s.PushSyncSendUpdateAsync(send));
return Task.FromResult(0);
}
public Task PushAuthRequestAsync(AuthRequest authRequest)
{
PushToServices((s) => s.PushAuthRequestAsync(authRequest));
return Task.FromResult(0);
}
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
PushToServices((s) => s.PushAuthRequestResponseAsync(authRequest));
return Task.FromResult(0);
}
public Task PushSyncSendDeleteAsync(Send send)
{
PushToServices((s) => s.PushSyncSendDeleteAsync(send));
return Task.FromResult(0);
}
public Task PushSyncOrganizationStatusAsync(Organization organization)
{
PushToServices((s) => s.PushSyncOrganizationStatusAsync(organization));
return Task.FromResult(0);
}
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization)
{
PushToServices(s => s.PushSyncOrganizationCollectionManagementSettingsAsync(organization));
return Task.CompletedTask;
}
public Task PushNotificationAsync(Notification notification)
{
PushToServices((s) => s.PushNotificationAsync(notification));
return Task.CompletedTask;
}
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
PushToServices((s) => s.PushNotificationStatusAsync(notification, notificationStatus));
return Task.CompletedTask;
}
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)
{
PushToServices((s) => s.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType));
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
PushToServices((s) => s.SendPayloadToOrganizationAsync(orgId, type, payload, identifier, deviceId, clientType));
return Task.FromResult(0);
}
public Task PushPendingSecurityTasksAsync(Guid userId)
{
PushToServices((s) => s.PushPendingSecurityTasksAsync(userId));
return Task.CompletedTask;
}
private void PushToServices(Func<IPushNotificationService, Task> pushFunc)
private Task PushToServices(Func<IPushEngine, Task> pushFunc)
{
if (!_services.Any())
{
_logger.LogWarning("No services found to push notification");
return;
Logger.LogWarning("No services found to push notification");
return Task.CompletedTask;
}
#if DEBUG
var tasks = new List<Task>();
#endif
foreach (var service in _services)
{
_logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
Logger.LogDebug("Pushing notification to service {ServiceName}", service.GetType().Name);
#if DEBUG
var task =
#endif
pushFunc(service);
#if DEBUG
tasks.Add(task);
#endif
}
#if DEBUG
return Task.WhenAll(tasks);
#else
return Task.CompletedTask;
#endif
}
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)
{
return PushToServices((s) => s.PushCipherAsync(cipher, pushType, collectionIds));
}
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
{
return PushToServices((s) => s.PushAsync(pushNotification));
}
}

View File

@ -1,129 +1,12 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Platform.Push.Internal;
public class NoopPushNotificationService : IPushNotificationService
internal class NoopPushNotificationService : IPushEngine
{
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
return Task.FromResult(0);
}
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds) => Task.CompletedTask;
public Task PushSyncCipherDeleteAsync(Cipher cipher)
{
return Task.FromResult(0);
}
public Task PushSyncCiphersAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
return Task.FromResult(0);
}
public Task PushSyncFolderCreateAsync(Folder folder)
{
return Task.FromResult(0);
}
public Task PushSyncFolderDeleteAsync(Folder folder)
{
return Task.FromResult(0);
}
public Task PushSyncFolderUpdateAsync(Folder folder)
{
return Task.FromResult(0);
}
public Task PushSyncOrganizationsAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushSyncOrgKeysAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushSyncSettingsAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushSyncVaultAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
{
return Task.FromResult(0);
}
public Task PushSyncSendCreateAsync(Send send)
{
return Task.FromResult(0);
}
public Task PushSyncSendDeleteAsync(Send send)
{
return Task.FromResult(0);
}
public Task PushSyncSendUpdateAsync(Send send)
{
return Task.FromResult(0);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
return Task.FromResult(0);
}
public Task PushSyncOrganizationStatusAsync(Organization organization)
{
return Task.FromResult(0);
}
public Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) => Task.CompletedTask;
public Task PushAuthRequestAsync(AuthRequest authRequest)
{
return Task.FromResult(0);
}
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
return Task.FromResult(0);
}
public Task PushNotificationAsync(Notification notification) => Task.CompletedTask;
public Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus) =>
Task.CompletedTask;
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)
{
return Task.FromResult(0);
}
public Task PushPendingSecurityTasksAsync(Guid userId)
{
return Task.FromResult(0);
}
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class => Task.CompletedTask;
}

View File

@ -1,13 +1,9 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
@ -20,18 +16,15 @@ namespace Bit.Core.Platform.Push;
/// Used by Cloud-Hosted environments.
/// Received by AzureQueueHostedService message receiver in Notifications project.
/// </summary>
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushEngine
{
private readonly IGlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly TimeProvider _timeProvider;
public NotificationsApiPushNotificationService(
IHttpClientFactory httpFactory,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor,
ILogger<NotificationsApiPushNotificationService> logger,
TimeProvider timeProvider)
ILogger<NotificationsApiPushNotificationService> logger)
: base(
httpFactory,
globalSettings.BaseServiceUri.InternalNotifications,
@ -41,27 +34,10 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
globalSettings.InternalIdentityKey,
logger)
{
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
_timeProvider = timeProvider;
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@ -89,174 +65,6 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
await SendMessageAsync(type, message, true);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrganizationsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrganizations);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncSettings);
}
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext)
{
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
}
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification
{
UserId = userId,
Date = _timeProvider.GetUtcNow().UtcDateTime,
};
await SendMessageAsync(type, message, excludeCurrentContext);
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
await SendMessageAsync(type, message, true);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);
}
public async Task PushSyncSendUpdateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendUpdate);
}
public async Task PushSyncSendDeleteAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendDelete);
}
public async Task PushNotificationAsync(Notification notification)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};
await SendMessageAsync(PushType.Notification, message, true);
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
await SendMessageAsync(PushType.NotificationStatus, message, true);
}
public async Task PushPendingSecurityTasksAsync(Guid userId)
{
await PushUserAsync(userId, PushType.PendingSecurityTasks);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
{
var message = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate
};
await SendMessageAsync(type, message, false);
}
}
private async Task SendMessageAsync<T>(PushType type, T payload, bool excludeCurrentContext)
{
var contextId = GetContextIdentifier(excludeCurrentContext);
@ -276,43 +84,8 @@ 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)
public async Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
{
// Noop
return Task.FromResult(0);
await SendMessageAsync(pushNotification.Type, pushNotification.Payload, pushNotification.ExcludeCurrentContext);
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
// Noop
return Task.FromResult(0);
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
{
var message = new OrganizationStatusPushNotification
{
OrganizationId = organization.Id,
Enabled = organization.Enabled
};
await SendMessageAsync(PushType.SyncOrganizationStatusChanged, message, false);
}
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
await SendMessageAsync(PushType.SyncOrganizationCollectionSettingChanged,
new OrganizationCollectionManagementPushNotification
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
}, false);
}

View File

@ -0,0 +1,78 @@
#nullable enable
using Bit.Core.Enums;
namespace Bit.Core.Platform.Push;
/// <summary>
/// Contains constants for all the available targets for a given notification.
/// </summary>
public enum NotificationTarget
{
/// <summary>
/// The target for the notification is a single user.
/// </summary>
User,
/// <summary>
/// The target for the notification are all the users in an organization.
/// </summary>
Organization,
/// <summary>
/// The target for the notification are all the organizations,
/// and all the users in that organization for a installation.
/// </summary>
Installation,
}
/// <summary>
/// An object containing all the information required for getting a notification
/// to an end users device and the information you want available to that device.
/// </summary>
/// <typeparam name="T">The type of the payload. This type is expected to be able to be roundtripped as JSON.</typeparam>
public record PushNotification<T>
where T : class
{
/// <summary>
/// The <see cref="PushType"/> to be associated with the notification. This is used to route
/// the notification to the correct handler on the client side. Be sure to use the correct payload
/// type for the associated <see cref="PushType"/>.
/// </summary>
public required PushType Type { get; init; }
/// <summary>
/// The target entity type for the notification.
/// </summary>
/// <remarks>
/// When the target type is <see cref="NotificationTarget.User"/> the <see cref="TargetId"/>
/// property is expected to be a users ID. When it is <see cref="NotificationTarget.Organization"/>
/// it should be an organizations id. When it is a <see cref="NotificationTarget.Installation"/>
/// it should be an installation id.
/// </remarks>
public required NotificationTarget Target { get; init; }
/// <summary>
/// The indentifier for the given <see cref="Target"/>.
/// </summary>
public required Guid TargetId { get; init; }
/// <summary>
/// The payload to be sent with the notification. This object will be JSON serialized.
/// </summary>
public required T Payload { get; init; }
/// <summary>
/// When <see langword="true"/> the notification will not include the current context identifier on it, this
/// means that the notification may get handled on the device that this notification could have originated from.
/// </summary>
public required bool ExcludeCurrentContext { get; init; }
/// <summary>
/// The type of clients the notification should be sent to, if <see langword="null"/> then
/// <see cref="ClientType.All"/> is inferred.
/// </summary>
public ClientType? ClientType { get; init; }
internal Guid? GetTargetWhen(NotificationTarget notificationTarget)
{
return Target == notificationTarget ? TargetId : null;
}
}

View File

@ -1,18 +1,15 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.IdentityServer;
using Bit.Core.Models;
using Bit.Core.Models.Api;
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;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Platform.Push.Internal;
@ -22,20 +19,18 @@ namespace Bit.Core.Platform.Push.Internal;
/// Used by Self-Hosted environments.
/// Received by PushController endpoint in Api project.
/// </summary>
public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService
public class RelayPushNotificationService : BaseIdentityClientService, IPushEngine
{
private readonly IDeviceRepository _deviceRepository;
private readonly IGlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly TimeProvider _timeProvider;
public RelayPushNotificationService(
IHttpClientFactory httpFactory,
IDeviceRepository deviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor,
ILogger<RelayPushNotificationService> logger,
TimeProvider timeProvider)
ILogger<RelayPushNotificationService> logger)
: base(
httpFactory,
globalSettings.PushRelayBaseUri,
@ -46,27 +41,10 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
logger)
{
_deviceRepository = deviceRepository;
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor;
_timeProvider = timeProvider;
}
public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherCreate, collectionIds);
}
public async Task PushSyncCipherUpdateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
{
await PushCipherAsync(cipher, PushType.SyncCipherUpdate, collectionIds);
}
public async Task PushSyncCipherDeleteAsync(Cipher cipher)
{
await PushCipherAsync(cipher, PushType.SyncLoginDelete, null);
}
private async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
public async Task PushCipherAsync(Cipher cipher, PushType type, IEnumerable<Guid>? collectionIds)
{
if (cipher.OrganizationId.HasValue)
{
@ -87,306 +65,45 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
RevisionDate = cipher.RevisionDate,
};
await SendPayloadToUserAsync(cipher.UserId.Value, type, message, true);
}
}
public async Task PushSyncFolderCreateAsync(Folder folder)
await PushAsync(new PushNotification<SyncCipherPushNotification>
{
await PushFolderAsync(folder, PushType.SyncFolderCreate);
}
public async Task PushSyncFolderUpdateAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderUpdate);
}
public async Task PushSyncFolderDeleteAsync(Folder folder)
{
await PushFolderAsync(folder, PushType.SyncFolderDelete);
}
private async Task PushFolderAsync(Folder folder, PushType type)
{
var message = new SyncFolderPushNotification
{
Id = folder.Id,
UserId = folder.UserId,
RevisionDate = folder.RevisionDate
};
await SendPayloadToUserAsync(folder.UserId, type, message, true);
}
public async Task PushSyncCiphersAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncCiphers);
}
public async Task PushSyncVaultAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncVault);
}
public async Task PushSyncOrganizationsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrganizations);
}
public async Task PushSyncOrgKeysAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncOrgKeys);
}
public async Task PushSyncSettingsAsync(Guid userId)
{
await PushUserAsync(userId, PushType.SyncSettings);
}
public async Task PushLogOutAsync(Guid userId, bool excludeCurrentContext = false)
{
await PushUserAsync(userId, PushType.LogOut, excludeCurrentContext);
}
private async Task PushUserAsync(Guid userId, PushType type, bool excludeCurrentContext = false)
{
var message = new UserPushNotification { UserId = userId, Date = _timeProvider.GetUtcNow().UtcDateTime };
await SendPayloadToUserAsync(userId, type, message, excludeCurrentContext);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);
}
public async Task PushSyncSendUpdateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendUpdate);
}
public async Task PushSyncSendDeleteAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendDelete);
}
private async Task PushSendAsync(Send send, PushType type)
{
if (send.UserId.HasValue)
{
var message = new SyncSendPushNotification
{
Id = send.Id,
UserId = send.UserId.Value,
RevisionDate = send.RevisionDate
};
await SendPayloadToUserAsync(message.UserId, type, message, true);
}
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification { Id = authRequest.Id, UserId = authRequest.UserId };
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
public async Task PushNotificationAsync(Notification notification)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate
};
if (notification.Global)
{
await SendPayloadToInstallationAsync(PushType.Notification, message, true, notification.ClientType);
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.Notification, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.Notification, message,
true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification id {NotificationId} push notification", notification.Id);
}
}
public async Task PushNotificationStatusAsync(Notification notification, NotificationStatus notificationStatus)
{
var message = new NotificationPushNotification
{
Id = notification.Id,
Priority = notification.Priority,
Global = notification.Global,
ClientType = notification.ClientType,
UserId = notification.UserId,
OrganizationId = notification.OrganizationId,
InstallationId = notification.Global ? _globalSettings.Installation.Id : null,
TaskId = notification.TaskId,
Title = notification.Title,
Body = notification.Body,
CreationDate = notification.CreationDate,
RevisionDate = notification.RevisionDate,
ReadDate = notificationStatus.ReadDate,
DeletedDate = notificationStatus.DeletedDate
};
if (notification.Global)
{
await SendPayloadToInstallationAsync(PushType.NotificationStatus, message, true, notification.ClientType);
}
else if (notification.UserId.HasValue)
{
await SendPayloadToUserAsync(notification.UserId.Value, PushType.NotificationStatus, message, true,
notification.ClientType);
}
else if (notification.OrganizationId.HasValue)
{
await SendPayloadToOrganizationAsync(notification.OrganizationId.Value, PushType.NotificationStatus, message,
true, notification.ClientType);
}
else
{
_logger.LogWarning("Invalid notification status id {NotificationId} push notification", notification.Id);
}
}
public async Task PushSyncOrganizationStatusAsync(Organization organization)
{
var message = new OrganizationStatusPushNotification
{
OrganizationId = organization.Id,
Enabled = organization.Enabled
};
await SendPayloadToOrganizationAsync(organization.Id, PushType.SyncOrganizationStatusChanged, message, false);
}
public async Task PushSyncOrganizationCollectionManagementSettingsAsync(Organization organization) =>
await SendPayloadToOrganizationAsync(
organization.Id,
PushType.SyncOrganizationCollectionSettingChanged,
new OrganizationCollectionManagementPushNotification
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
},
false
);
public async Task PushPendingSecurityTasksAsync(Guid userId)
{
await PushUserAsync(userId, PushType.PendingSecurityTasks);
}
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
Target = NotificationTarget.User,
TargetId = cipher.UserId.Value,
Payload = message,
ExcludeCurrentContext = true,
});
}
}
public async Task PushAsync<T>(PushNotification<T> pushNotification)
where T : class
{
var deviceIdentifier = _httpContextAccessor.HttpContext
?.RequestServices.GetService<ICurrentContext>()
?.DeviceIdentifier;
Guid? deviceId = null;
if (!string.IsNullOrEmpty(deviceIdentifier))
{
var device = await _deviceRepository.GetByIdentifierAsync(deviceIdentifier);
deviceId = device?.Id;
}
var payload = new PushSendRequestModel<T>
{
Type = pushNotification.Type,
UserId = pushNotification.GetTargetWhen(NotificationTarget.User),
OrganizationId = pushNotification.GetTargetWhen(NotificationTarget.Organization),
InstallationId = pushNotification.GetTargetWhen(NotificationTarget.Installation),
Payload = pushNotification.Payload,
Identifier = pushNotification.ExcludeCurrentContext ? deviceIdentifier : null,
// We set the device id regardless of if they want to exclude the current context or not
DeviceId = deviceId,
ClientType = pushNotification.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)
{
var request = new PushSendRequestModel
{
UserId = userId.ToString(),
Type = type,
Payload = payload,
ClientType = clientType
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task SendPayloadToOrganizationAsync(Guid orgId, PushType type, object payload,
bool excludeCurrentContext, ClientType? clientType = null)
{
var request = new PushSendRequestModel
{
OrganizationId = orgId.ToString(),
Type = type,
Payload = payload,
ClientType = clientType
};
await AddCurrentContextAsync(request, excludeCurrentContext);
await SendAsync(HttpMethod.Post, "push/send", request);
}
private async Task AddCurrentContextAsync(PushSendRequestModel request, bool addIdentifier)
{
var currentContext =
_httpContextAccessor.HttpContext?.RequestServices.GetService(typeof(ICurrentContext)) as ICurrentContext;
if (!string.IsNullOrWhiteSpace(currentContext?.DeviceIdentifier))
{
var device = await _deviceRepository.GetByIdentifierAsync(currentContext.DeviceIdentifier);
if (device != null)
{
request.DeviceId = device.Id.ToString();
}
if (addIdentifier)
{
request.Identifier = currentContext.DeviceIdentifier;
}
}
}
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)
{
throw new NotImplementedException();
}
public Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string? identifier,
string? deviceId = null, ClientType? clientType = null)
{
throw new NotImplementedException();
await SendAsync(HttpMethod.Post, "push/send", payload);
}
}

View File

@ -288,7 +288,7 @@ public static class ServiceCollectionExtensions
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
CoreHelpers.SettingHasValue(globalSettings.Installation.Key))
{
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, RelayPushNotificationService>());
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
}
else
@ -299,20 +299,20 @@ public static class ServiceCollectionExtensions
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
{
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationsApiPushNotificationService>());
}
}
else
{
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, NotificationHubPushNotificationService>());
services.TryAddSingleton<IPushRelayer, NotificationHubPushNotificationService>();
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{
services.AddKeyedSingleton("notifications",
(_, _) => new QueueClient(globalSettings.Notifications.ConnectionString, "notifications"));
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>(
"implementation");
services.TryAddEnumerable(ServiceDescriptor.Singleton<IPushEngine, AzureQueuePushNotificationService>());
}
}
@ -366,7 +366,6 @@ public static class ServiceCollectionExtensions
{
services.AddSingleton<IMailService, NoopMailService>();
services.AddSingleton<IMailDeliveryService, NoopMailDeliveryService>();
services.AddSingleton<IPushNotificationService, NoopPushNotificationService>();
services.AddSingleton<IPushRegistrationService, NoopPushRegistrationService>();
services.AddSingleton<IAttachmentStorageService, NoopAttachmentStorageService>();
services.AddSingleton<ILicensingService, NoopLicensingService>();

View File

@ -28,6 +28,8 @@ public class ApiApplicationFactory : WebApplicationFactoryBase<Startup>
_identityApplicationFactory.ManagesDatabase = false;
}
public IdentityApplicationFactory Identity => _identityApplicationFactory;
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
base.ConfigureWebHost(builder);

View File

@ -0,0 +1,449 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Text.Json.Nodes;
using Azure.Storage.Queues;
using Bit.Api.IntegrationTest.Factories;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using NSubstitute;
using Xunit;
using static Bit.Core.Settings.GlobalSettings;
namespace Bit.Api.IntegrationTest.Platform.Controllers;
public class PushControllerTests
{
private static readonly Guid _userId = Guid.NewGuid();
private static readonly Guid _organizationId = Guid.NewGuid();
private static readonly Guid _deviceId = Guid.NewGuid();
public static IEnumerable<object[]> SendData()
{
static object[] Typed<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall = true)
{
return [pushSendRequestModel, expectedHubTagExpression, expectHubCall];
}
static object[] UserTyped(PushType pushType)
{
return Typed(new PushSendRequestModel<UserPushNotification>
{
Type = pushType,
UserId = _userId,
DeviceId = _deviceId,
Payload = new UserPushNotification
{
Date = DateTime.UtcNow,
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
}
// User cipher
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherUpdate,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
// Organization cipher, an org cipher would not naturally be synced from our
// code but it is technically possible to be submitted to the endpoint.
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherUpdate,
OrganizationId = _organizationId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
OrganizationId = _organizationId,
},
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherCreate,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
// Organization cipher, an org cipher would not naturally be synced from our
// code but it is technically possible to be submitted to the endpoint.
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherCreate,
OrganizationId = _organizationId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
OrganizationId = _organizationId,
},
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherDelete,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
// Organization cipher, an org cipher would not naturally be synced from our
// code but it is technically possible to be submitted to the endpoint.
yield return Typed(new PushSendRequestModel<SyncCipherPushNotification>
{
Type = PushType.SyncCipherDelete,
OrganizationId = _organizationId,
DeviceId = _deviceId,
Payload = new SyncCipherPushNotification
{
Id = Guid.NewGuid(),
OrganizationId = _organizationId,
},
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
{
Type = PushType.SyncFolderDelete,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncFolderPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
{
Type = PushType.SyncFolderCreate,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncFolderPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<SyncFolderPushNotification>
{
Type = PushType.SyncFolderCreate,
UserId = _userId,
DeviceId = _deviceId,
Payload = new SyncFolderPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return UserTyped(PushType.SyncCiphers);
yield return UserTyped(PushType.SyncVault);
yield return UserTyped(PushType.SyncOrganizations);
yield return UserTyped(PushType.SyncOrgKeys);
yield return UserTyped(PushType.SyncSettings);
yield return UserTyped(PushType.LogOut);
yield return UserTyped(PushType.PendingSecurityTasks);
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
{
Type = PushType.AuthRequest,
UserId = _userId,
DeviceId = _deviceId,
Payload = new AuthRequestPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<AuthRequestPushNotification>
{
Type = PushType.AuthRequestResponse,
UserId = _userId,
DeviceId = _deviceId,
Payload = new AuthRequestPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
{
Type = PushType.Notification,
UserId = _userId,
DeviceId = _deviceId,
Payload = new NotificationPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
{
Type = PushType.Notification,
UserId = _userId,
DeviceId = _deviceId,
ClientType = ClientType.All,
Payload = new NotificationPushNotification
{
Id = Guid.NewGuid(),
Global = true,
},
}, $"(template:payload_userId:%installation%_{_userId})");
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
{
Type = PushType.NotificationStatus,
OrganizationId = _organizationId,
DeviceId = _deviceId,
Payload = new NotificationPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
yield return Typed(new PushSendRequestModel<NotificationPushNotification>
{
Type = PushType.NotificationStatus,
OrganizationId = _organizationId,
DeviceId = _deviceId,
Payload = new NotificationPushNotification
{
Id = Guid.NewGuid(),
UserId = _userId,
},
}, $"(template:payload && organizationId:%installation%_{_organizationId})");
}
[Theory]
[MemberData(nameof(SendData))]
public async Task Send_Works<T>(PushSendRequestModel<T> pushSendRequestModel, string expectedHubTagExpression, bool expectHubCall)
{
var (apiFactory, httpClient, installation, queueClient, notificationHubProxy) = await SetupTest();
// Act
var pushSendResponse = await httpClient.PostAsJsonAsync("push/send", pushSendRequestModel);
// Assert
pushSendResponse.EnsureSuccessStatusCode();
// Relayed notifications, the ones coming to this endpoint should
// not make their way into our Azure Queue and instead should only be sent to Azure Notifications
// hub.
await queueClient
.Received(0)
.SendMessageAsync(Arg.Any<string>());
// Check that this notification was sent through hubs the expected number of times
await notificationHubProxy
.Received(expectHubCall ? 1 : 0)
.SendTemplateNotificationAsync(
Arg.Any<Dictionary<string, string>>(),
Arg.Is(expectedHubTagExpression.Replace("%installation%", installation.Id.ToString()))
);
// TODO: Expect on the dictionary more?
// Notifications being relayed from SH should have the device id
// tracked so that we can later send the notification to that device.
await apiFactory.GetService<IInstallationDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == pushSendRequestModel.DeviceId.ToString()
));
}
[Fact]
public async Task Send_InstallationNotification_NotAuthenticatedInstallation_Fails()
{
var (_, httpClient, _, _, _) = await SetupTest();
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
{
Type = PushType.NotificationStatus,
InstallationId = Guid.NewGuid(),
Payload = new { }
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
Assert.Equal(JsonValueKind.String, message.GetValueKind());
Assert.Equal("InstallationId does not match current context.", message.GetValue<string>());
}
[Fact]
public async Task Send_InstallationNotification_Works()
{
var (apiFactory, httpClient, installation, _, notificationHubProxy) = await SetupTest();
var deviceId = Guid.NewGuid();
var response = await httpClient.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
{
Type = PushType.NotificationStatus,
InstallationId = installation.Id,
Payload = new { },
DeviceId = deviceId,
ClientType = ClientType.Web,
});
response.EnsureSuccessStatusCode();
await notificationHubProxy
.Received(1)
.SendTemplateNotificationAsync(
Arg.Any<Dictionary<string, string>>(),
Arg.Is($"(template:payload && installationId:{installation.Id} && clientType:Web)")
);
await apiFactory.GetService<IInstallationDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<InstallationDeviceEntity>(
ide => ide.PartitionKey == installation.Id.ToString() && ide.RowKey == deviceId.ToString()
));
}
[Fact]
public async Task Send_NoOrganizationNoInstallationNoUser_FailsModelValidation()
{
var (_, client, _, _, _) = await SetupTest();
var response = await client.PostAsJsonAsync("push/send", new PushSendRequestModel<object>
{
Type = PushType.AuthRequest,
Payload = new { },
});
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
var body = await response.Content.ReadFromJsonAsync<JsonNode>();
Assert.Equal(JsonValueKind.Object, body.GetValueKind());
Assert.True(body.AsObject().TryGetPropertyValue("message", out var message));
Assert.Equal(JsonValueKind.String, message.GetValueKind());
Assert.Equal("The model state is invalid.", message.GetValue<string>());
}
private static async Task<(ApiApplicationFactory Factory, HttpClient AuthedClient, Installation Installation, QueueClient MockedQueue, INotificationHubProxy MockedHub)> SetupTest()
{
// Arrange
var apiFactory = new ApiApplicationFactory();
var queueClient = Substitute.For<QueueClient>();
// Substitute the underlying queue messages will go to.
apiFactory.ConfigureServices(services =>
{
var queueClientService = services.FirstOrDefault(
sd => sd.ServiceKey == (object)"notifications"
&& sd.ServiceType == typeof(QueueClient)
) ?? throw new InvalidOperationException("Expected service was not found.");
services.Remove(queueClientService);
services.AddKeyedSingleton("notifications", queueClient);
});
var notificationHubProxy = Substitute.For<INotificationHubProxy>();
apiFactory.SubstituteService<INotificationHubPool>(s =>
{
s.AllClients
.Returns(notificationHubProxy);
});
apiFactory.SubstituteService<IInstallationDeviceRepository>(s => { });
// Setup as cloud with NotificationHub setup and Azure Queue
apiFactory.UpdateConfiguration("GlobalSettings:Notifications:ConnectionString", "any_value");
// Configure hubs
var index = 0;
void AddHub(NotificationHubSettings notificationHubSettings)
{
apiFactory.UpdateConfiguration(
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:ConnectionString",
notificationHubSettings.ConnectionString
);
apiFactory.UpdateConfiguration(
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:HubName",
notificationHubSettings.HubName
);
apiFactory.UpdateConfiguration(
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationStartDate",
notificationHubSettings.RegistrationStartDate?.ToString()
);
apiFactory.UpdateConfiguration(
$"GlobalSettings:NotificationHubPool:NotificationHubs:{index}:RegistrationEndDate",
notificationHubSettings.RegistrationEndDate?.ToString()
);
index++;
}
AddHub(new NotificationHubSettings
{
ConnectionString = "some_value",
RegistrationStartDate = DateTime.UtcNow.AddDays(-2),
});
var httpClient = apiFactory.CreateClient();
// Add installation into database
var installationRepository = apiFactory.GetService<IInstallationRepository>();
var installation = await installationRepository.CreateAsync(new Installation
{
Key = "my_test_key",
Email = "test@example.com",
Enabled = true,
});
var identityClient = apiFactory.Identity.CreateDefaultClient();
var connectTokenResponse = await identityClient.PostAsync("connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "scope", "api.push" },
{ "client_id", $"installation.{installation.Id}" },
{ "client_secret", installation.Key },
}));
connectTokenResponse.EnsureSuccessStatusCode();
var connectTokenResponseModel = await connectTokenResponse.Content.ReadFromJsonAsync<JsonNode>();
// Setup authentication
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
connectTokenResponseModel["token_type"].GetValue<string>(),
connectTokenResponseModel["access_token"].GetValue<string>()
);
return (apiFactory, httpClient, installation, queueClient, notificationHubProxy);
}
}

View File

@ -18,210 +18,6 @@ namespace Bit.Api.Test.Platform.Push.Controllers;
[SutProviderCustomize]
public class PushControllerTests
{
[Theory]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
[BitAutoData(true, true)]
public async Task SendAsync_InstallationIdNotSetOrSelfHosted_BadRequest(bool haveInstallationId, bool selfHosted,
SutProvider<PushController> sutProvider, Guid installationId, Guid userId, Guid organizationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = selfHosted;
if (haveInstallationId)
{
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
}
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
UserId = userId.ToString(),
OrganizationId = organizationId.ToString(),
InstallationId = installationId.ToString(),
Payload = "test-payload"
}));
Assert.Equal("Not correctly configured for push relays.", exception.Message);
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[BitAutoData]
public async Task SendAsync_UserIdAndOrganizationIdAndInstallationIdEmpty_NoPushNotificationSent(
SutProvider<PushController> sutProvider, Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
await sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
UserId = null,
OrganizationId = null,
InstallationId = null,
Payload = "test-payload"
});
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[RepeatingPatternBitAutoData([false, true], [false, true], [false, true])]
public async Task SendAsync_UserIdSet_SendPayloadToUserAsync(bool haveIdentifier, bool haveDeviceId,
bool haveOrganizationId, SutProvider<PushController> sutProvider, Guid installationId, Guid userId,
Guid identifier, Guid deviceId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
var expectedUserId = $"{installationId}_{userId}";
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
await sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
UserId = userId.ToString(),
OrganizationId = haveOrganizationId ? Guid.NewGuid().ToString() : null,
InstallationId = null,
Payload = "test-payload",
DeviceId = haveDeviceId ? deviceId.ToString() : null,
Identifier = haveIdentifier ? identifier.ToString() : null,
ClientType = ClientType.All,
});
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
.SendPayloadToUserAsync(expectedUserId, PushType.Notification, "test-payload", expectedIdentifier,
expectedDeviceId, ClientType.All);
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[RepeatingPatternBitAutoData([false, true], [false, true])]
public async Task SendAsync_OrganizationIdSet_SendPayloadToOrganizationAsync(bool haveIdentifier, bool haveDeviceId,
SutProvider<PushController> sutProvider, Guid installationId, Guid organizationId, Guid identifier,
Guid deviceId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
var expectedOrganizationId = $"{installationId}_{organizationId}";
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
await sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
UserId = null,
OrganizationId = organizationId.ToString(),
InstallationId = null,
Payload = "test-payload",
DeviceId = haveDeviceId ? deviceId.ToString() : null,
Identifier = haveIdentifier ? identifier.ToString() : null,
ClientType = ClientType.All,
});
await sutProvider.GetDependency<IPushNotificationService>().Received(1)
.SendPayloadToOrganizationAsync(expectedOrganizationId, PushType.Notification, "test-payload",
expectedIdentifier, expectedDeviceId, ClientType.All);
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[RepeatingPatternBitAutoData([false, true], [false, true])]
public async Task SendAsync_InstallationIdSet_SendPayloadToInstallationAsync(bool haveIdentifier, bool haveDeviceId,
SutProvider<PushController> sutProvider, Guid installationId, Guid identifier, Guid deviceId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
var expectedIdentifier = haveIdentifier ? $"{installationId}_{identifier}" : null;
var expectedDeviceId = haveDeviceId ? $"{installationId}_{deviceId}" : null;
await sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
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<IPushNotificationService>().Received(1)
.SendPayloadToInstallationAsync(installationId.ToString(), PushType.Notification, "test-payload",
expectedIdentifier, expectedDeviceId, ClientType.All);
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[BitAutoData]
public async Task SendAsync_InstallationIdNotMatching_BadRequest(SutProvider<PushController> sutProvider,
Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
sutProvider.GetDependency<ICurrentContext>().InstallationId.Returns(installationId);
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SendAsync(new PushSendRequestModel
{
Type = PushType.Notification,
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<IPushNotificationService>().Received(0)
.SendPayloadToInstallationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToOrganizationAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(),
Arg.Any<string>(), Arg.Any<string?>(), Arg.Any<ClientType?>());
await sutProvider.GetDependency<IPushNotificationService>().Received(0)
.SendPayloadToUserAsync(Arg.Any<string>(), Arg.Any<PushType>(), Arg.Any<object>(), Arg.Any<string>(),
Arg.Any<string?>(), Arg.Any<ClientType?>());
}
[Theory]
[BitAutoData(false, true)]
[BitAutoData(false, false)]

View File

@ -11,16 +11,14 @@ namespace Bit.Core.Test.Models.Api.Request;
public class PushSendRequestModelTests
{
[Theory]
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "], [null, "", " "])]
public void Validate_UserIdOrganizationIdInstallationIdNullOrEmpty_Invalid(string? userId, string? organizationId,
string? installationId)
[Fact]
public void Validate_UserIdOrganizationIdInstallationIdNull_Invalid()
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = userId,
OrganizationId = organizationId,
InstallationId = installationId,
UserId = null,
OrganizationId = null,
InstallationId = null,
Type = PushType.SyncCiphers,
Payload = "test"
};
@ -32,16 +30,14 @@ public class PushSendRequestModelTests
result => result.ErrorMessage == "UserId or OrganizationId or InstallationId is required.");
}
[Theory]
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
public void Validate_UserIdProvidedOrganizationIdInstallationIdNullOrEmpty_Valid(string? organizationId,
string? installationId)
[Fact]
public void Validate_UserIdProvidedOrganizationIdInstallationIdNull_Valid()
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = Guid.NewGuid().ToString(),
OrganizationId = organizationId,
InstallationId = installationId,
UserId = Guid.NewGuid(),
OrganizationId = null,
InstallationId = null,
Type = PushType.SyncCiphers,
Payload = "test"
};
@ -51,16 +47,14 @@ public class PushSendRequestModelTests
Assert.Empty(results);
}
[Theory]
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
public void Validate_OrganizationIdProvidedUserIdInstallationIdNullOrEmpty_Valid(string? userId,
string? installationId)
[Fact]
public void Validate_OrganizationIdProvidedUserIdInstallationIdNull_Valid()
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = userId,
OrganizationId = Guid.NewGuid().ToString(),
InstallationId = installationId,
UserId = null,
OrganizationId = Guid.NewGuid(),
InstallationId = null,
Type = PushType.SyncCiphers,
Payload = "test"
};
@ -70,16 +64,14 @@ public class PushSendRequestModelTests
Assert.Empty(results);
}
[Theory]
[RepeatingPatternBitAutoData([null, "", " "], [null, "", " "])]
public void Validate_InstallationIdProvidedUserIdOrganizationIdNullOrEmpty_Valid(string? userId,
string? organizationId)
[Fact]
public void Validate_InstallationIdProvidedUserIdOrganizationIdNull_Valid()
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = userId,
OrganizationId = organizationId,
InstallationId = Guid.NewGuid().ToString(),
UserId = null,
OrganizationId = null,
InstallationId = Guid.NewGuid(),
Type = PushType.SyncCiphers,
Payload = "test"
};
@ -94,10 +86,10 @@ public class PushSendRequestModelTests
[BitAutoData("Type")]
public void Validate_RequiredFieldNotProvided_Invalid(string requiredField)
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = Guid.NewGuid().ToString(),
OrganizationId = Guid.NewGuid().ToString(),
UserId = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
Type = PushType.SyncCiphers,
Payload = "test"
};
@ -115,7 +107,7 @@ public class PushSendRequestModelTests
var serialized = JsonSerializer.Serialize(dictionary, JsonHelpers.IgnoreWritingNull);
var jsonException =
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PushSendRequestModel>(serialized));
Assert.Throws<JsonException>(() => JsonSerializer.Deserialize<PushSendRequestModel<string>>(serialized));
Assert.Contains($"missing required properties, including the following: {requiredField}",
jsonException.Message);
}
@ -123,15 +115,15 @@ public class PushSendRequestModelTests
[Fact]
public void Validate_AllFieldsPresent_Valid()
{
var model = new PushSendRequestModel
var model = new PushSendRequestModel<string>
{
UserId = Guid.NewGuid().ToString(),
OrganizationId = Guid.NewGuid().ToString(),
UserId = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
Type = PushType.SyncCiphers,
Payload = "test payload",
Identifier = Guid.NewGuid().ToString(),
ClientType = ClientType.All,
DeviceId = Guid.NewGuid().ToString()
DeviceId = Guid.NewGuid()
};
var results = Validate(model);
@ -139,7 +131,7 @@ public class PushSendRequestModelTests
Assert.Empty(results);
}
private static List<ValidationResult> Validate(PushSendRequestModel model)
private static List<ValidationResult> Validate<T>(PushSendRequestModel<T> model)
{
var results = new List<ValidationResult>();
Validator.TryValidateObject(model, new ValidationContext(model), results, true);

View File

@ -5,12 +5,11 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Data;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.NotificationHub;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
@ -33,483 +32,6 @@ public class NotificationHubPushNotificationServiceTests
private static readonly DateTime _now = DateTime.UtcNow;
private static readonly Guid _installationId = Guid.Parse("da73177b-513f-4444-b582-595c890e1022");
[Theory]
[BitAutoData]
[NotificationCustomize]
public async Task PushNotificationAsync_GlobalInstallationIdDefault_NotSent(
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = default;
await sutProvider.Sut.PushNotificationAsync(notification);
await sutProvider.GetDependency<INotificationHubPool>()
.Received(0)
.AllClients
.Received(0)
.SendTemplateNotificationAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<string>());
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData]
[NotificationCustomize]
public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId(
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification, Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, null, installationId);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload && installationId:{installationId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize]
public async Task PushNotificationAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, null, installationId);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload && installationId:{installationId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(false)]
[BitAutoData(true)]
[NotificationCustomize(false)]
public async Task PushNotificationAsync_UserIdProvidedClientTypeAll_SentToUser(
bool organizationIdNull, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification)
{
if (organizationIdNull)
{
notification.OrganizationId = null;
}
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, null, null);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload_userId:{notification.UserId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task PushNotificationAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification)
{
notification.OrganizationId = null;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, null, null);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task PushNotificationAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification)
{
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, null, null);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData]
[NotificationCustomize(false)]
public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization(
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification)
{
notification.UserId = null;
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, null, null);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload && organizationId:{notification.OrganizationId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task PushNotificationAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification)
{
notification.UserId = null;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, null, null);
await sutProvider.Sut.PushNotificationAsync(notification);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.Notification,
expectedNotification,
$"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData]
[NotificationCustomize]
public async Task PushNotificationStatusAsync_GlobalInstallationIdDefault_NotSent(
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification,
NotificationStatus notificationStatus)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = default;
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await sutProvider.GetDependency<INotificationHubPool>()
.Received(0)
.AllClients
.Received(0)
.SendTemplateNotificationAsync(Arg.Any<IDictionary<string, string>>(), Arg.Any<string>());
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData]
[NotificationCustomize]
public async Task PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeAll_SentToInstallationId(
SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus, Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload && installationId:{installationId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize]
public async Task
PushNotificationStatusAsync_GlobalInstallationIdSetClientTypeNotAll_SentToInstallationIdAndClientType(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus, Guid installationId)
{
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, installationId);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload && installationId:{installationId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(false)]
[BitAutoData(true)]
[NotificationCustomize(false)]
public async Task PushNotificationStatusAsync_UserIdProvidedClientTypeAll_SentToUser(
bool organizationIdNull, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus)
{
if (organizationIdNull)
{
notification.OrganizationId = null;
}
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload_userId:{notification.UserId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdNullClientTypeNotAll_SentToUser(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus)
{
notification.OrganizationId = null;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task PushNotificationStatusAsync_UserIdProvidedOrganizationIdProvidedClientTypeNotAll_SentToUser(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus)
{
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload_userId:{notification.UserId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData]
[NotificationCustomize(false)]
public async Task PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeAll_SentToOrganization(
SutProvider<NotificationHubPushNotificationService> sutProvider, Notification notification,
NotificationStatus notificationStatus)
{
notification.UserId = null;
notification.ClientType = ClientType.All;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload && organizationId:{notification.OrganizationId})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Web)]
[BitAutoData(ClientType.Mobile)]
[NotificationCustomize(false)]
public async Task
PushNotificationStatusAsync_UserIdNullOrganizationIdProvidedClientTypeNotAll_SentToOrganization(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider,
Notification notification, NotificationStatus notificationStatus)
{
notification.UserId = null;
notification.ClientType = clientType;
var expectedNotification = ToNotificationPushNotification(notification, notificationStatus, null);
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await AssertSendTemplateNotificationAsync(sutProvider, PushType.NotificationStatus,
expectedNotification,
$"(template:payload && organizationId:{notification.OrganizationId} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData([null])]
[BitAutoData(ClientType.All)]
public async Task SendPayloadToUserAsync_ClientTypeNullOrAll_SentToUser(ClientType? clientType,
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid userId, PushType pushType, string payload,
string identifier)
{
await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null,
clientType);
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
$"(template:payload_userId:{userId} && !deviceIdentifier:{identifier})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Mobile)]
[BitAutoData(ClientType.Web)]
public async Task SendPayloadToUserAsync_ClientTypeExplicit_SentToUserAndClientType(ClientType clientType,
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid userId, PushType pushType, string payload,
string identifier)
{
await sutProvider.Sut.SendPayloadToUserAsync(userId.ToString(), pushType, payload, identifier, null,
clientType);
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
$"(template:payload_userId:{userId} && !deviceIdentifier:{identifier} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData([null])]
[BitAutoData(ClientType.All)]
public async Task SendPayloadToOrganizationAsync_ClientTypeNullOrAll_SentToOrganization(ClientType? clientType,
SutProvider<NotificationHubPushNotificationService> sutProvider, Guid organizationId, PushType pushType,
string payload, string identifier)
{
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier,
null, clientType);
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
$"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Mobile)]
[BitAutoData(ClientType.Web)]
public async Task SendPayloadToOrganizationAsync_ClientTypeExplicit_SentToOrganizationAndClientType(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> sutProvider, Guid organizationId,
PushType pushType, string payload, string identifier)
{
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId.ToString(), pushType, payload, identifier,
null, clientType);
await AssertSendTemplateNotificationAsync(sutProvider, pushType, payload,
$"(template:payload && organizationId:{organizationId} && !deviceIdentifier:{identifier} && clientType:{clientType})");
await sutProvider.GetDependency<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData([null])]
[BitAutoData(ClientType.All)]
public async Task SendPayloadToInstallationAsync_ClientTypeNullOrAll_SentToInstallation(ClientType? clientType,
SutProvider<NotificationHubPushNotificationService> 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<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Theory]
[BitAutoData(ClientType.Browser)]
[BitAutoData(ClientType.Desktop)]
[BitAutoData(ClientType.Mobile)]
[BitAutoData(ClientType.Web)]
public async Task SendPayloadToInstallationAsync_ClientTypeExplicit_SentToInstallationAndClientType(
ClientType clientType, SutProvider<NotificationHubPushNotificationService> 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<IInstallationDeviceRepository>()
.Received(0)
.UpsertAsync(Arg.Any<InstallationDeviceEntity>());
}
[Fact]
public async Task PushSyncCipherCreateAsync_SendExpectedData()
{
@ -1066,7 +588,7 @@ public class NotificationHubPushNotificationServiceTests
);
}
private async Task VerifyNotificationAsync(Func<NotificationHubPushNotificationService, Task> test,
private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test,
PushType type, JsonNode expectedPayload, string tag)
{
var installationDeviceRepository = Substitute.For<IInstallationDeviceRepository>();
@ -1104,12 +626,11 @@ public class NotificationHubPushNotificationServiceTests
notificationHubPool,
httpContextAccessor,
NullLogger<NotificationHubPushNotificationService>.Instance,
globalSettings,
fakeTimeProvider
globalSettings
);
// Act
await test(sut);
await test(new EngineWrapper(sut, fakeTimeProvider, _installationId));
// Assert
var calls = notificationHubProxy.ReceivedCalls();

View File

@ -9,14 +9,11 @@ using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.NotificationCenter.Enums;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Settings;
using Bit.Core.Test.AutoFixture;
using Bit.Core.Test.AutoFixture.CurrentContextFixtures;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
@ -42,96 +39,6 @@ public class AzureQueuePushNotificationServiceTests
_fakeTimeProvider.SetUtcNow(DateTime.UtcNow);
}
[Theory]
[BitAutoData]
[NotificationCustomize]
[CurrentContextCustomize]
public async Task PushNotificationAsync_NotificationGlobal_Sent(
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
ICurrentContext currentContext, Guid installationId)
{
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
.GetService(Arg.Any<Type>()).Returns(currentContext);
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
await sutProvider.Sut.PushNotificationAsync(notification);
await sutProvider.GetDependency<QueueClient>().Received(1)
.SendMessageAsync(Arg.Is<string>(message =>
MatchMessage(PushType.Notification, message,
new NotificationPushNotificationEquals(notification, null, installationId),
deviceIdentifier.ToString())));
}
[Theory]
[BitAutoData]
[NotificationCustomize(false)]
[CurrentContextCustomize]
public async Task PushNotificationAsync_NotificationNotGlobal_Sent(
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
ICurrentContext currentContext, Guid installationId)
{
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
.GetService(Arg.Any<Type>()).Returns(currentContext);
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
await sutProvider.Sut.PushNotificationAsync(notification);
await sutProvider.GetDependency<QueueClient>().Received(1)
.SendMessageAsync(Arg.Is<string>(message =>
MatchMessage(PushType.Notification, message,
new NotificationPushNotificationEquals(notification, null, null),
deviceIdentifier.ToString())));
}
[Theory]
[BitAutoData]
[NotificationCustomize]
[NotificationStatusCustomize]
[CurrentContextCustomize]
public async Task PushNotificationStatusAsync_NotificationGlobal_Sent(
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId)
{
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
.GetService(Arg.Any<Type>()).Returns(currentContext);
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await sutProvider.GetDependency<QueueClient>().Received(1)
.SendMessageAsync(Arg.Is<string>(message =>
MatchMessage(PushType.NotificationStatus, message,
new NotificationPushNotificationEquals(notification, notificationStatus, installationId),
deviceIdentifier.ToString())));
}
[Theory]
[BitAutoData]
[NotificationCustomize(false)]
[NotificationStatusCustomize]
[CurrentContextCustomize]
public async Task PushNotificationStatusAsync_NotificationNotGlobal_Sent(
SutProvider<AzureQueuePushNotificationService> sutProvider, Notification notification, Guid deviceIdentifier,
ICurrentContext currentContext, NotificationStatus notificationStatus, Guid installationId)
{
currentContext.DeviceIdentifier.Returns(deviceIdentifier.ToString());
sutProvider.GetDependency<IHttpContextAccessor>().HttpContext!.RequestServices
.GetService(Arg.Any<Type>()).Returns(currentContext);
sutProvider.GetDependency<IGlobalSettings>().Installation.Id = installationId;
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await sutProvider.GetDependency<QueueClient>().Received(1)
.SendMessageAsync(Arg.Is<string>(message =>
MatchMessage(PushType.NotificationStatus, message,
new NotificationPushNotificationEquals(notification, notificationStatus, null),
deviceIdentifier.ToString())));
}
[Theory]
[InlineData("6a5bbe1b-cf16-49a6-965f-5c2eac56a531", null)]
[InlineData(null, "b9a3fcb4-2447-45c1-aad2-24de43c88c44")]
@ -844,7 +751,7 @@ public class AzureQueuePushNotificationServiceTests
// );
// }
private async Task VerifyNotificationAsync(Func<AzureQueuePushNotificationService, Task> test, JsonNode expectedMessage)
private async Task VerifyNotificationAsync(Func<IPushNotificationService, Task> test, JsonNode expectedMessage)
{
var queueClient = Substitute.For<QueueClient>();
@ -872,7 +779,7 @@ public class AzureQueuePushNotificationServiceTests
_fakeTimeProvider
);
await test(sut);
await test(new EngineWrapper(sut, _fakeTimeProvider, _globalSettings.Installation.Id));
// Hoist equality checker outside the expression so that we
// can more easily place a breakpoint

View File

@ -1,98 +1,8 @@
#nullable enable
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Test.NotificationCenter.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Push.Services;
[SutProviderCustomize]
public class MultiServicePushNotificationServiceTests
{
[Theory]
[BitAutoData]
[NotificationCustomize]
public async Task PushNotificationAsync_Notification_Sent(
SutProvider<MultiServicePushNotificationService> sutProvider, Notification notification)
{
await sutProvider.Sut.PushNotificationAsync(notification);
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
.First()
.Received(1)
.PushNotificationAsync(notification);
}
[Theory]
[BitAutoData]
[NotificationCustomize]
[NotificationStatusCustomize]
public async Task PushNotificationStatusAsync_Notification_Sent(
SutProvider<MultiServicePushNotificationService> sutProvider, Notification notification,
NotificationStatus notificationStatus)
{
await sutProvider.Sut.PushNotificationStatusAsync(notification, notificationStatus);
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
.First()
.Received(1)
.PushNotificationStatusAsync(notification, notificationStatus);
}
[Theory]
[BitAutoData([null, null])]
[BitAutoData(ClientType.All, null)]
[BitAutoData([null, "test device id"])]
[BitAutoData(ClientType.All, "test device id")]
public async Task SendPayloadToUserAsync_Message_Sent(ClientType? clientType, string? deviceId, string userId,
PushType type, object payload, string identifier, SutProvider<MultiServicePushNotificationService> sutProvider)
{
await sutProvider.Sut.SendPayloadToUserAsync(userId, type, payload, identifier, deviceId, clientType);
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
.First()
.Received(1)
.SendPayloadToUserAsync(userId, 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 SendPayloadToOrganizationAsync_Message_Sent(ClientType? clientType, string? deviceId,
string organizationId, PushType type, object payload, string identifier,
SutProvider<MultiServicePushNotificationService> sutProvider)
{
await sutProvider.Sut.SendPayloadToOrganizationAsync(organizationId, type, payload, identifier, deviceId,
clientType);
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
.First()
.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<MultiServicePushNotificationService> sutProvider)
{
await sutProvider.Sut.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId,
clientType);
await sutProvider.GetDependency<IEnumerable<IPushNotificationService>>()
.First()
.Received(1)
.SendPayloadToInstallationAsync(installationId, type, payload, identifier, deviceId, clientType);
}
// TODO: Can add a couple tests here
}

View File

@ -19,14 +19,13 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
protected override string ExpectedClientUrl() => "https://localhost:7777/send";
protected override IPushNotificationService CreateService()
protected override IPushEngine CreateService()
{
return new NotificationsApiPushNotificationService(
HttpClientFactory,
GlobalSettings,
HttpContextAccessor,
NullLogger<NotificationsApiPushNotificationService>.Instance,
FakeTimeProvider
NullLogger<NotificationsApiPushNotificationService>.Instance
);
}
@ -221,7 +220,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
["UserId"] = send.UserId,
["RevisionDate"] = send.RevisionDate,
},
["ContextId"] = null,
["ContextId"] = DeviceIdentifier,
};
}
@ -236,7 +235,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
["UserId"] = send.UserId,
["RevisionDate"] = send.RevisionDate,
},
["ContextId"] = null,
["ContextId"] = DeviceIdentifier,
};
}
@ -251,7 +250,7 @@ public class NotificationsApiPushNotificationServiceTests : PushTestBase
["UserId"] = send.UserId,
["RevisionDate"] = send.RevisionDate,
},
["ContextId"] = null,
["ContextId"] = DeviceIdentifier,
};
}

View File

@ -15,11 +15,28 @@ using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using RichardSzalay.MockHttp;
using Xunit;
public class EngineWrapper(IPushEngine pushEngine, FakeTimeProvider fakeTimeProvider, Guid installationId) : IPushNotificationService
{
public Guid InstallationId { get; } = installationId;
public TimeProvider TimeProvider { get; } = fakeTimeProvider;
public ILogger Logger => NullLogger<EngineWrapper>.Instance;
public Task PushAsync<T>(PushNotification<T> pushNotification) where T : class
=> pushEngine.PushAsync(pushNotification);
public Task PushCipherAsync(Cipher cipher, PushType pushType, IEnumerable<Guid>? collectionIds)
=> pushEngine.PushCipherAsync(cipher, pushType, collectionIds);
}
public abstract class PushTestBase
{
protected static readonly string DeviceIdentifier = "test_device_identifier";
@ -51,7 +68,7 @@ public abstract class PushTestBase
FakeTimeProvider.SetUtcNow(DateTimeOffset.UtcNow);
}
protected abstract IPushNotificationService CreateService();
protected abstract IPushEngine CreateService();
protected abstract string ExpectedClientUrl();
@ -480,7 +497,7 @@ public abstract class PushTestBase
})
.Respond(HttpStatusCode.OK);
await test(CreateService());
await test(new EngineWrapper(CreateService(), FakeTimeProvider, GlobalSettings.Installation.Id));
Assert.NotNull(actualNode);

View File

@ -4,8 +4,8 @@ using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Auth.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.NotificationCenter.Entities;
using Bit.Core.Platform.Push;
using Bit.Core.Platform.Push.Internal;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@ -14,7 +14,6 @@ using Bit.Core.Vault.Entities;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Platform.Push.Services;
@ -38,47 +37,19 @@ public class RelayPushNotificationServiceTests : PushTestBase
GlobalSettings.Installation.IdentityUri = "https://localhost:8888";
}
protected override RelayPushNotificationService CreateService()
protected override IPushEngine CreateService()
{
return new RelayPushNotificationService(
HttpClientFactory,
_deviceRepository,
GlobalSettings,
HttpContextAccessor,
NullLogger<RelayPushNotificationService>.Instance,
FakeTimeProvider
NullLogger<RelayPushNotificationService>.Instance
);
}
protected override string ExpectedClientUrl() => "https://localhost:7777/push/send";
[Fact]
public async Task SendPayloadToInstallationAsync_ThrowsNotImplementedException()
{
var sut = CreateService();
await Assert.ThrowsAsync<NotImplementedException>(
async () => await sut.SendPayloadToInstallationAsync("installation_id", PushType.AuthRequest, new { }, null)
);
}
[Fact]
public async Task SendPayloadToUserAsync_ThrowsNotImplementedException()
{
var sut = CreateService();
await Assert.ThrowsAsync<NotImplementedException>(
async () => await sut.SendPayloadToUserAsync("user_id", PushType.AuthRequest, new { }, null)
);
}
[Fact]
public async Task SendPayloadToOrganizationAsync_ThrowsNotImplementedException()
{
var sut = CreateService();
await Assert.ThrowsAsync<NotImplementedException>(
async () => await sut.SendPayloadToOrganizationAsync("organization_id", PushType.AuthRequest, new { }, null)
);
}
protected override JsonNode GetPushSyncCipherCreatePayload(Cipher cipher, Guid collectionIds)
{
return new JsonObject