mirror of
https://github.com/bitwarden/server
synced 2025-12-24 04:03:25 +00:00
Merge branch 'main' into SM-1571-DisableSMAdsForUsers
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using Azure.Messaging.EventGrid;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
@@ -1366,7 +1367,7 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
var (attachmentId, uploadUrl) = await _cipherService.CreateAttachmentForDelayedUploadAsync(cipher,
|
||||
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id);
|
||||
request.Key, request.FileName, request.FileSize, request.AdminRequest, user.Id, request.LastKnownRevisionDate);
|
||||
return new AttachmentUploadDataResponseModel
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
@@ -1419,9 +1420,11 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
await Request.GetFileAsync(async (stream) =>
|
||||
{
|
||||
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData);
|
||||
await _cipherService.UploadFileForExistingAttachmentAsync(stream, cipher, attachmentData, lastKnownRevisionDate);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1440,10 +1443,12 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||
{
|
||||
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
|
||||
Request.ContentLength.GetValueOrDefault(0), user.Id);
|
||||
Request.ContentLength.GetValueOrDefault(0), user.Id, false, lastKnownRevisionDate);
|
||||
});
|
||||
|
||||
return new CipherResponseModel(
|
||||
@@ -1469,10 +1474,13 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
|
||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||
{
|
||||
await _cipherService.CreateAttachmentAsync(cipher, stream, fileName, key,
|
||||
Request.ContentLength.GetValueOrDefault(0), userId, true);
|
||||
Request.ContentLength.GetValueOrDefault(0), userId, true, lastKnownRevisionDate);
|
||||
});
|
||||
|
||||
return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp);
|
||||
@@ -1515,10 +1523,13 @@ public class CiphersController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
// Extract lastKnownRevisionDate from form data if present
|
||||
DateTime? lastKnownRevisionDate = GetLastKnownRevisionDateFromForm();
|
||||
|
||||
await Request.GetFileAsync(async (stream, fileName, key) =>
|
||||
{
|
||||
await _cipherService.CreateAttachmentShareAsync(cipher, stream, fileName, key,
|
||||
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId);
|
||||
Request.ContentLength.GetValueOrDefault(0), attachmentId, organizationId, lastKnownRevisionDate);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1630,4 +1641,19 @@ public class CiphersController : Controller
|
||||
{
|
||||
return await _cipherRepository.GetByIdAsync(cipherId, userId);
|
||||
}
|
||||
|
||||
private DateTime? GetLastKnownRevisionDateFromForm()
|
||||
{
|
||||
DateTime? lastKnownRevisionDate = null;
|
||||
if (Request.Form.TryGetValue("lastKnownRevisionDate", out var dateValue))
|
||||
{
|
||||
if (!DateTime.TryParse(dateValue, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsedDate))
|
||||
{
|
||||
throw new BadRequestException("Invalid lastKnownRevisionDate format.");
|
||||
}
|
||||
lastKnownRevisionDate = parsedDate;
|
||||
}
|
||||
|
||||
return lastKnownRevisionDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,4 +9,9 @@ public class AttachmentRequestModel
|
||||
public string FileName { get; set; }
|
||||
public long FileSize { get; set; }
|
||||
public bool AdminRequest { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// The last known revision date of the Cipher that this attachment belongs to.
|
||||
/// </summary>
|
||||
public DateTime? LastKnownRevisionDate { get; set; }
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface IIntegrationMessage
|
||||
{
|
||||
IntegrationType IntegrationType { get; }
|
||||
string MessageId { get; set; }
|
||||
string? OrganizationId { get; set; }
|
||||
int RetryCount { get; }
|
||||
DateTime? DelayUntilDate { get; }
|
||||
void ApplyRetry(DateTime? handlerDelayUntilDate);
|
||||
|
||||
@@ -7,6 +7,7 @@ public class IntegrationMessage : IIntegrationMessage
|
||||
{
|
||||
public IntegrationType IntegrationType { get; set; }
|
||||
public required string MessageId { get; set; }
|
||||
public string? OrganizationId { get; set; }
|
||||
public required string RenderedTemplate { get; set; }
|
||||
public int RetryCount { get; set; } = 0;
|
||||
public DateTime? DelayUntilDate { get; set; }
|
||||
|
||||
@@ -5,5 +5,5 @@ namespace Bit.Core.Services;
|
||||
public interface IEventIntegrationPublisher : IAsyncDisposable
|
||||
{
|
||||
Task PublishAsync(IIntegrationMessage message);
|
||||
Task PublishEventAsync(string body);
|
||||
Task PublishEventAsync(string body, string? organizationId);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,8 @@ public class AzureServiceBusService : IAzureServiceBusService
|
||||
var serviceBusMessage = new ServiceBusMessage(json)
|
||||
{
|
||||
Subject = message.IntegrationType.ToRoutingKey(),
|
||||
MessageId = message.MessageId
|
||||
MessageId = message.MessageId,
|
||||
PartitionKey = message.OrganizationId
|
||||
};
|
||||
|
||||
await _integrationSender.SendMessageAsync(serviceBusMessage);
|
||||
@@ -44,18 +45,20 @@ public class AzureServiceBusService : IAzureServiceBusService
|
||||
{
|
||||
Subject = message.IntegrationType.ToRoutingKey(),
|
||||
ScheduledEnqueueTime = message.DelayUntilDate ?? DateTime.UtcNow,
|
||||
MessageId = message.MessageId
|
||||
MessageId = message.MessageId,
|
||||
PartitionKey = message.OrganizationId
|
||||
};
|
||||
|
||||
await _integrationSender.SendMessageAsync(serviceBusMessage);
|
||||
}
|
||||
|
||||
public async Task PublishEventAsync(string body)
|
||||
public async Task PublishEventAsync(string body, string? organizationId)
|
||||
{
|
||||
var message = new ServiceBusMessage(body)
|
||||
{
|
||||
ContentType = "application/json",
|
||||
MessageId = Guid.NewGuid().ToString()
|
||||
MessageId = Guid.NewGuid().ToString(),
|
||||
PartitionKey = organizationId
|
||||
};
|
||||
|
||||
await _eventSender.SendMessageAsync(message);
|
||||
|
||||
@@ -14,15 +14,21 @@ public class EventIntegrationEventWriteService : IEventWriteService, IAsyncDispo
|
||||
public async Task CreateAsync(IEvent e)
|
||||
{
|
||||
var body = JsonSerializer.Serialize(e);
|
||||
await _eventIntegrationPublisher.PublishEventAsync(body: body);
|
||||
await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: e.OrganizationId?.ToString());
|
||||
}
|
||||
|
||||
public async Task CreateManyAsync(IEnumerable<IEvent> events)
|
||||
{
|
||||
var body = JsonSerializer.Serialize(events);
|
||||
await _eventIntegrationPublisher.PublishEventAsync(body: body);
|
||||
}
|
||||
var eventList = events as IList<IEvent> ?? events.ToList();
|
||||
if (eventList.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var organizationId = eventList[0].OrganizationId?.ToString();
|
||||
var body = JsonSerializer.Serialize(eventList);
|
||||
await _eventIntegrationPublisher.PublishEventAsync(body: body, organizationId: organizationId);
|
||||
}
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _eventIntegrationPublisher.DisposeAsync();
|
||||
|
||||
@@ -57,6 +57,7 @@ public class EventIntegrationHandler<T>(
|
||||
{
|
||||
IntegrationType = integrationType,
|
||||
MessageId = messageId.ToString(),
|
||||
OrganizationId = organizationId.ToString(),
|
||||
Configuration = config,
|
||||
RenderedTemplate = renderedTemplate,
|
||||
RetryCount = 0,
|
||||
|
||||
@@ -122,7 +122,7 @@ public class RabbitMqService : IRabbitMqService
|
||||
body: body);
|
||||
}
|
||||
|
||||
public async Task PublishEventAsync(string body)
|
||||
public async Task PublishEventAsync(string body, string? organizationId)
|
||||
{
|
||||
await using var channel = await CreateChannelAsync();
|
||||
var properties = new BasicProperties
|
||||
|
||||
@@ -13,11 +13,11 @@ public interface ICipherService
|
||||
Task SaveDetailsAsync(CipherDetails cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||
IEnumerable<Guid> collectionIds = null, bool skipPermissionCheck = false);
|
||||
Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,
|
||||
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId);
|
||||
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null);
|
||||
Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false);
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null);
|
||||
Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key, long requestLength,
|
||||
string attachmentId, Guid organizationShareId);
|
||||
string attachmentId, Guid organizationShareId, DateTime? lastKnownRevisionDate = null);
|
||||
Task DeleteAsync(CipherDetails cipherDetails, Guid deletingUserId, bool orgAdmin = false);
|
||||
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
|
||||
@@ -34,7 +34,7 @@ public interface ICipherService
|
||||
Task SoftDeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task RestoreAsync(CipherDetails cipherDetails, Guid restoringUserId, bool orgAdmin = false);
|
||||
Task<ICollection<CipherOrganizationDetails>> RestoreManyAsync(IEnumerable<Guid> cipherIds, Guid restoringUserId, Guid? organizationId = null, bool orgAdmin = false);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId);
|
||||
Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachmentId, DateTime? lastKnownRevisionDate = null);
|
||||
Task<AttachmentResponseData> GetAttachmentDownloadDataAsync(Cipher cipher, string attachmentId);
|
||||
Task<bool> ValidateCipherAttachmentFile(Cipher cipher, CipherAttachment.MetaData attachmentData);
|
||||
Task ValidateBulkCollectionAssignmentAsync(IEnumerable<Guid> collectionIds, IEnumerable<Guid> cipherIds, Guid userId);
|
||||
|
||||
@@ -113,7 +113,7 @@ public class CipherService : ICipherService
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
cipher.RevisionDate = DateTime.UtcNow;
|
||||
await _cipherRepository.ReplaceAsync(cipher);
|
||||
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_Updated);
|
||||
@@ -168,7 +168,7 @@ public class CipherService : ICipherService
|
||||
}
|
||||
else
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
cipher.RevisionDate = DateTime.UtcNow;
|
||||
await ValidateChangeInCollectionsAsync(cipher, collectionIds, savingUserId);
|
||||
await ValidateViewPasswordUserAsync(cipher);
|
||||
@@ -180,8 +180,9 @@ public class CipherService : ICipherService
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment)
|
||||
public async Task UploadFileForExistingAttachmentAsync(Stream stream, Cipher cipher, CipherAttachment.MetaData attachment, DateTime? lastKnownRevisionDate = null)
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
if (attachment == null)
|
||||
{
|
||||
throw new BadRequestException("Cipher attachment does not exist");
|
||||
@@ -196,8 +197,9 @@ public class CipherService : ICipherService
|
||||
}
|
||||
|
||||
public async Task<(string attachmentId, string uploadUrl)> CreateAttachmentForDelayedUploadAsync(Cipher cipher,
|
||||
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId)
|
||||
string key, string fileName, long fileSize, bool adminRequest, Guid savingUserId, DateTime? lastKnownRevisionDate = null)
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, adminRequest, fileSize);
|
||||
|
||||
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
||||
@@ -232,8 +234,9 @@ public class CipherService : ICipherService
|
||||
}
|
||||
|
||||
public async Task CreateAttachmentAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false)
|
||||
long requestLength, Guid savingUserId, bool orgAdmin = false, DateTime? lastKnownRevisionDate = null)
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
await ValidateCipherEditForAttachmentAsync(cipher, savingUserId, orgAdmin, requestLength);
|
||||
|
||||
var attachmentId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false);
|
||||
@@ -284,10 +287,11 @@ public class CipherService : ICipherService
|
||||
}
|
||||
|
||||
public async Task CreateAttachmentShareAsync(Cipher cipher, Stream stream, string fileName, string key,
|
||||
long requestLength, string attachmentId, Guid organizationId)
|
||||
long requestLength, string attachmentId, Guid organizationId, DateTime? lastKnownRevisionDate = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
if (requestLength < 1)
|
||||
{
|
||||
throw new BadRequestException("No data to attach.");
|
||||
@@ -859,7 +863,7 @@ public class CipherService : ICipherService
|
||||
return NormalCipherPermissions.CanRestore(user, cipher, organizationAbility);
|
||||
}
|
||||
|
||||
private void ValidateCipherLastKnownRevisionDateAsync(Cipher cipher, DateTime? lastKnownRevisionDate)
|
||||
private void ValidateCipherLastKnownRevisionDate(Cipher cipher, DateTime? lastKnownRevisionDate)
|
||||
{
|
||||
if (cipher.Id == default || !lastKnownRevisionDate.HasValue)
|
||||
{
|
||||
@@ -1007,7 +1011,7 @@ public class CipherService : ICipherService
|
||||
throw new BadRequestException("Not enough storage available for this organization.");
|
||||
}
|
||||
|
||||
ValidateCipherLastKnownRevisionDateAsync(cipher, lastKnownRevisionDate);
|
||||
ValidateCipherLastKnownRevisionDate(cipher, lastKnownRevisionDate);
|
||||
}
|
||||
|
||||
private async Task ValidateViewPasswordUserAsync(Cipher cipher)
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Azure.Storage.Queues;
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
|
||||
public class AzureQueueHostedService : IHostedService, IDisposable
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHubContext<NotificationsHub> _hubContext;
|
||||
private readonly IHubContext<AnonymousNotificationsHub> _anonymousHubContext;
|
||||
private readonly HubHelpers _hubHelpers;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
private Task _executingTask;
|
||||
private CancellationTokenSource _cts;
|
||||
private QueueClient _queueClient;
|
||||
private Task? _executingTask;
|
||||
private CancellationTokenSource? _cts;
|
||||
|
||||
public AzureQueueHostedService(
|
||||
ILogger<AzureQueueHostedService> logger,
|
||||
IHubContext<NotificationsHub> hubContext,
|
||||
IHubContext<AnonymousNotificationsHub> anonymousHubContext,
|
||||
HubHelpers hubHelpers,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_logger = logger;
|
||||
_hubContext = hubContext;
|
||||
_hubHelpers = hubHelpers;
|
||||
_globalSettings = globalSettings;
|
||||
_anonymousHubContext = anonymousHubContext;
|
||||
}
|
||||
|
||||
public Task StartAsync(CancellationToken cancellationToken)
|
||||
@@ -44,32 +36,39 @@ public class AzureQueueHostedService : IHostedService, IDisposable
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogWarning("Stopping service.");
|
||||
_cts.Cancel();
|
||||
_cts?.Cancel();
|
||||
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{ }
|
||||
{
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
_queueClient = new QueueClient(_globalSettings.Notifications.ConnectionString, "notifications");
|
||||
var queueClient = new QueueClient(_globalSettings.Notifications.ConnectionString, "notifications");
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
var messages = await _queueClient.ReceiveMessagesAsync(32);
|
||||
var messages = await queueClient.ReceiveMessagesAsync(32, cancellationToken: cancellationToken);
|
||||
if (messages.Value?.Any() ?? false)
|
||||
{
|
||||
foreach (var message in messages.Value)
|
||||
{
|
||||
try
|
||||
{
|
||||
await HubHelpers.SendNotificationToHubAsync(
|
||||
message.DecodeMessageText(), _hubContext, _anonymousHubContext, _logger, cancellationToken);
|
||||
await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
||||
var decodedMessage = message.DecodeMessageText();
|
||||
if (!string.IsNullOrWhiteSpace(decodedMessage))
|
||||
{
|
||||
await _hubHelpers.SendNotificationToHubAsync(decodedMessage, cancellationToken);
|
||||
}
|
||||
|
||||
await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt,
|
||||
cancellationToken);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -77,7 +76,8 @@ public class AzureQueueHostedService : IHostedService, IDisposable
|
||||
message.MessageId, message.DequeueCount);
|
||||
if (message.DequeueCount > 2)
|
||||
{
|
||||
await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
||||
await queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,36 +1,30 @@
|
||||
using System.Text;
|
||||
#nullable enable
|
||||
using System.Text;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
namespace Bit.Notifications.Controllers;
|
||||
|
||||
[Authorize("Internal")]
|
||||
public class SendController : Controller
|
||||
{
|
||||
private readonly IHubContext<NotificationsHub> _hubContext;
|
||||
private readonly IHubContext<AnonymousNotificationsHub> _anonymousHubContext;
|
||||
private readonly ILogger<SendController> _logger;
|
||||
private readonly HubHelpers _hubHelpers;
|
||||
|
||||
public SendController(IHubContext<NotificationsHub> hubContext, IHubContext<AnonymousNotificationsHub> anonymousHubContext, ILogger<SendController> logger)
|
||||
public SendController(HubHelpers hubHelpers)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_anonymousHubContext = anonymousHubContext;
|
||||
_logger = logger;
|
||||
_hubHelpers = hubHelpers;
|
||||
}
|
||||
|
||||
[HttpPost("~/send")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task PostSend()
|
||||
public async Task PostSendAsync()
|
||||
{
|
||||
using (var reader = new StreamReader(Request.Body, Encoding.UTF8))
|
||||
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
|
||||
var notificationJson = await reader.ReadToEndAsync();
|
||||
if (!string.IsNullOrWhiteSpace(notificationJson))
|
||||
{
|
||||
var notificationJson = await reader.ReadToEndAsync();
|
||||
if (!string.IsNullOrWhiteSpace(notificationJson))
|
||||
{
|
||||
await HubHelpers.SendNotificationToHubAsync(notificationJson, _hubContext, _anonymousHubContext, _logger);
|
||||
}
|
||||
await _hubHelpers.SendNotificationToHubAsync(notificationJson);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
|
||||
public static class HubHelpers
|
||||
public class HubHelpers
|
||||
{
|
||||
private static JsonSerializerOptions _deserializerOptions =
|
||||
new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||
private static readonly JsonSerializerOptions _deserializerOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
|
||||
private static readonly string _receiveMessageMethod = "ReceiveMessage";
|
||||
|
||||
public static async Task SendNotificationToHubAsync(
|
||||
string notificationJson,
|
||||
IHubContext<NotificationsHub> hubContext,
|
||||
private readonly IHubContext<NotificationsHub> _hubContext;
|
||||
private readonly IHubContext<AnonymousNotificationsHub> _anonymousHubContext;
|
||||
private readonly ILogger<HubHelpers> _logger;
|
||||
|
||||
public HubHelpers(IHubContext<NotificationsHub> hubContext,
|
||||
IHubContext<AnonymousNotificationsHub> anonymousHubContext,
|
||||
ILogger logger,
|
||||
CancellationToken cancellationToken = default(CancellationToken)
|
||||
)
|
||||
ILogger<HubHelpers> logger)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_anonymousHubContext = anonymousHubContext;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SendNotificationToHubAsync(string notificationJson, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var notification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<object>>(notificationJson, _deserializerOptions);
|
||||
logger.LogInformation("Sending notification: {NotificationType}", notification.Type);
|
||||
if (notification is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Sending notification: {NotificationType}", notification.Type);
|
||||
switch (notification.Type)
|
||||
{
|
||||
case PushType.SyncCipherUpdate:
|
||||
@@ -35,14 +43,19 @@ public static class HubHelpers
|
||||
var cipherNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<SyncCipherPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
if (cipherNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (cipherNotification.Payload.UserId.HasValue)
|
||||
{
|
||||
await hubContext.Clients.User(cipherNotification.Payload.UserId.ToString())
|
||||
await _hubContext.Clients.User(cipherNotification.Payload.UserId.Value.ToString())
|
||||
.SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);
|
||||
}
|
||||
else if (cipherNotification.Payload.OrganizationId.HasValue)
|
||||
{
|
||||
await hubContext.Clients
|
||||
await _hubContext.Clients
|
||||
.Group(NotificationsHub.GetOrganizationGroup(cipherNotification.Payload.OrganizationId.Value))
|
||||
.SendAsync(_receiveMessageMethod, cipherNotification, cancellationToken);
|
||||
}
|
||||
@@ -54,7 +67,12 @@ public static class HubHelpers
|
||||
var folderNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<SyncFolderPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(folderNotification.Payload.UserId.ToString())
|
||||
if (folderNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients.User(folderNotification.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, folderNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.SyncCiphers:
|
||||
@@ -66,7 +84,12 @@ public static class HubHelpers
|
||||
var userNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<LogOutPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(userNotification.Payload.UserId.ToString())
|
||||
if (userNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients.User(userNotification.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, userNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.SyncSendCreate:
|
||||
@@ -75,36 +98,65 @@ public static class HubHelpers
|
||||
var sendNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<SyncSendPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(sendNotification.Payload.UserId.ToString())
|
||||
if (sendNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients.User(sendNotification.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, sendNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.AuthRequestResponse:
|
||||
var authRequestResponseNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString())
|
||||
if (authRequestResponseNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _anonymousHubContext.Clients.Group(authRequestResponseNotification.Payload.Id.ToString())
|
||||
.SendAsync("AuthRequestResponseRecieved", authRequestResponseNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.AuthRequest:
|
||||
var authRequestNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<AuthRequestPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString())
|
||||
if (authRequestNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients.User(authRequestNotification.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, authRequestNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.SyncOrganizationStatusChanged:
|
||||
var orgStatusNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<OrganizationStatusPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId))
|
||||
if (orgStatusNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients
|
||||
.Group(NotificationsHub.GetOrganizationGroup(orgStatusNotification.Payload.OrganizationId))
|
||||
.SendAsync(_receiveMessageMethod, orgStatusNotification, cancellationToken);
|
||||
break;
|
||||
case PushType.SyncOrganizationCollectionSettingChanged:
|
||||
var organizationCollectionSettingsChangedNotification =
|
||||
JsonSerializer.Deserialize<PushNotificationData<OrganizationStatusPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification.Payload.OrganizationId))
|
||||
.SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification, cancellationToken);
|
||||
if (organizationCollectionSettingsChangedNotification is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients
|
||||
.Group(NotificationsHub.GetOrganizationGroup(organizationCollectionSettingsChangedNotification
|
||||
.Payload.OrganizationId))
|
||||
.SendAsync(_receiveMessageMethod, organizationCollectionSettingsChangedNotification,
|
||||
cancellationToken);
|
||||
break;
|
||||
case PushType.OrganizationBankAccountVerified:
|
||||
var organizationBankAccountVerifiedNotification =
|
||||
@@ -124,9 +176,14 @@ public static class HubHelpers
|
||||
case PushType.NotificationStatus:
|
||||
var notificationData = JsonSerializer.Deserialize<PushNotificationData<NotificationPushNotification>>(
|
||||
notificationJson, _deserializerOptions);
|
||||
if (notificationData is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (notificationData.Payload.InstallationId.HasValue)
|
||||
{
|
||||
await hubContext.Clients.Group(NotificationsHub.GetInstallationGroup(
|
||||
await _hubContext.Clients.Group(NotificationsHub.GetInstallationGroup(
|
||||
notificationData.Payload.InstallationId.Value, notificationData.Payload.ClientType))
|
||||
.SendAsync(_receiveMessageMethod, notificationData, cancellationToken);
|
||||
}
|
||||
@@ -134,27 +191,34 @@ public static class HubHelpers
|
||||
{
|
||||
if (notificationData.Payload.ClientType == ClientType.All)
|
||||
{
|
||||
await hubContext.Clients.User(notificationData.Payload.UserId.ToString())
|
||||
await _hubContext.Clients.User(notificationData.Payload.UserId.Value.ToString())
|
||||
.SendAsync(_receiveMessageMethod, notificationData, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
await hubContext.Clients.Group(NotificationsHub.GetUserGroup(
|
||||
await _hubContext.Clients.Group(NotificationsHub.GetUserGroup(
|
||||
notificationData.Payload.UserId.Value, notificationData.Payload.ClientType))
|
||||
.SendAsync(_receiveMessageMethod, notificationData, cancellationToken);
|
||||
}
|
||||
}
|
||||
else if (notificationData.Payload.OrganizationId.HasValue)
|
||||
{
|
||||
await hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(
|
||||
await _hubContext.Clients.Group(NotificationsHub.GetOrganizationGroup(
|
||||
notificationData.Payload.OrganizationId.Value, notificationData.Payload.ClientType))
|
||||
.SendAsync(_receiveMessageMethod, notificationData, cancellationToken);
|
||||
}
|
||||
|
||||
break;
|
||||
case PushType.RefreshSecurityTasks:
|
||||
var pendingTasksData = JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(notificationJson, _deserializerOptions);
|
||||
await hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())
|
||||
var pendingTasksData =
|
||||
JsonSerializer.Deserialize<PushNotificationData<UserPushNotification>>(notificationJson,
|
||||
_deserializerOptions);
|
||||
if (pendingTasksData is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
await _hubContext.Clients.User(pendingTasksData.Payload.UserId.ToString())
|
||||
.SendAsync(_receiveMessageMethod, pendingTasksData, cancellationToken);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -61,6 +61,7 @@ public class Startup
|
||||
}
|
||||
services.AddSingleton<IUserIdProvider, SubjectUserIdProvider>();
|
||||
services.AddSingleton<ConnectionCounter>();
|
||||
services.AddSingleton<HubHelpers>();
|
||||
|
||||
// Mvc
|
||||
services.AddMvc();
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace Bit.Core.Test.Models.Data.EventIntegrations;
|
||||
public class IntegrationMessageTests
|
||||
{
|
||||
private const string _messageId = "TestMessageId";
|
||||
private const string _organizationId = "TestOrganizationId";
|
||||
|
||||
[Fact]
|
||||
public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate()
|
||||
@@ -16,6 +17,7 @@ public class IntegrationMessageTests
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = _messageId,
|
||||
OrganizationId = _organizationId,
|
||||
RetryCount = 2,
|
||||
RenderedTemplate = string.Empty,
|
||||
DelayUntilDate = null
|
||||
@@ -36,6 +38,7 @@ public class IntegrationMessageTests
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = _messageId,
|
||||
OrganizationId = _organizationId,
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
@@ -48,6 +51,7 @@ public class IntegrationMessageTests
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(message.Configuration, result.Configuration);
|
||||
Assert.Equal(message.MessageId, result.MessageId);
|
||||
Assert.Equal(message.OrganizationId, result.OrganizationId);
|
||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
||||
Assert.Equal(message.IntegrationType, result.IntegrationType);
|
||||
Assert.Equal(message.RetryCount, result.RetryCount);
|
||||
@@ -67,6 +71,7 @@ public class IntegrationMessageTests
|
||||
var message = new IntegrationMessage
|
||||
{
|
||||
MessageId = _messageId,
|
||||
OrganizationId = _organizationId,
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
@@ -77,6 +82,7 @@ public class IntegrationMessageTests
|
||||
var result = JsonSerializer.Deserialize<IntegrationMessage>(json);
|
||||
|
||||
Assert.Equal(message.MessageId, result.MessageId);
|
||||
Assert.Equal(message.OrganizationId, result.OrganizationId);
|
||||
Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
|
||||
Assert.Equal(message.IntegrationType, result.IntegrationType);
|
||||
Assert.Equal(message.RetryCount, result.RetryCount);
|
||||
|
||||
@@ -22,18 +22,20 @@ public class EventIntegrationEventWriteServiceTests
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_EventPublishedToEventQueue(EventMessage eventMessage)
|
||||
{
|
||||
var expected = JsonSerializer.Serialize(eventMessage);
|
||||
await Subject.CreateAsync(eventMessage);
|
||||
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
|
||||
Arg.Is<string>(body => AssertJsonStringsMatch(eventMessage, body)));
|
||||
body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessage, body)),
|
||||
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateManyAsync_EventsPublishedToEventQueue(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
var eventMessage = eventMessages.First();
|
||||
await Subject.CreateManyAsync(eventMessages);
|
||||
await _eventIntegrationPublisher.Received(1).PublishEventAsync(
|
||||
Arg.Is<string>(body => AssertJsonStringsMatch(eventMessages, body)));
|
||||
body: Arg.Is<string>(body => AssertJsonStringsMatch(eventMessages, body)),
|
||||
organizationId: Arg.Is<string>(orgId => eventMessage.OrganizationId.ToString().Equals(orgId)));
|
||||
}
|
||||
|
||||
private static bool AssertJsonStringsMatch(EventMessage expected, string body)
|
||||
|
||||
@@ -23,6 +23,7 @@ public class EventIntegrationHandlerTests
|
||||
private const string _templateWithOrganization = "Org: #OrganizationName#";
|
||||
private const string _templateWithUser = "#UserName#, #UserEmail#";
|
||||
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
|
||||
private static readonly Guid _organizationId = Guid.NewGuid();
|
||||
private static readonly Uri _uri = new Uri("https://localhost");
|
||||
private static readonly Uri _uri2 = new Uri("https://example.com");
|
||||
private readonly IEventIntegrationPublisher _eventIntegrationPublisher = Substitute.For<IEventIntegrationPublisher>();
|
||||
@@ -50,6 +51,7 @@ public class EventIntegrationHandlerTests
|
||||
{
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
MessageId = "TestMessageId",
|
||||
OrganizationId = _organizationId.ToString(),
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(_uri),
|
||||
RenderedTemplate = template,
|
||||
RetryCount = 0,
|
||||
@@ -122,6 +124,7 @@ public class EventIntegrationHandlerTests
|
||||
public async Task HandleEventAsync_BaseTemplateOneConfiguration_PublishesIntegrationMessage(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
@@ -140,6 +143,7 @@ public class EventIntegrationHandlerTests
|
||||
public async Task HandleEventAsync_BaseTemplateTwoConfigurations_PublishesIntegrationMessages(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
@@ -164,6 +168,7 @@ public class EventIntegrationHandlerTests
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
@@ -183,6 +188,7 @@ public class EventIntegrationHandlerTests
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
|
||||
var organization = Substitute.For<Organization>();
|
||||
organization.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(organization);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
@@ -205,6 +211,7 @@ public class EventIntegrationHandlerTests
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
@@ -235,6 +242,7 @@ public class EventIntegrationHandlerTests
|
||||
var sutProvider = GetSutProvider(ValidFilterConfiguration());
|
||||
sutProvider.GetDependency<IIntegrationFilterService>().EvaluateFilterGroup(
|
||||
Arg.Any<IntegrationFilterGroup>(), Arg.Any<EventMessage>()).Returns(true);
|
||||
eventMessage.OrganizationId = _organizationId;
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
|
||||
@@ -284,7 +292,7 @@ public class EventIntegrationHandlerTests
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,12 +309,12 @@ public class EventIntegrationHandlerTests
|
||||
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
|
||||
);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
||||
|
||||
expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_uri2);
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
|
||||
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
|
||||
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
expectedMessage, new[] { "MessageId", "OrganizationId" })));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public class IntegrationHandlerTests
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(new Uri("https://localhost"), "Bearer", "AUTH-TOKEN"),
|
||||
MessageId = "TestMessageId",
|
||||
OrganizationId = "TestOrganizationId",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RenderedTemplate = "Template",
|
||||
DelayUntilDate = null,
|
||||
@@ -25,6 +26,8 @@ public class IntegrationHandlerTests
|
||||
var result = await sut.HandleAsync(expected.ToJson());
|
||||
var typedResult = Assert.IsType<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
|
||||
|
||||
Assert.Equal(expected.MessageId, typedResult.MessageId);
|
||||
Assert.Equal(expected.OrganizationId, typedResult.OrganizationId);
|
||||
Assert.Equal(expected.Configuration, typedResult.Configuration);
|
||||
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
|
||||
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
|
||||
|
||||
@@ -113,6 +113,242 @@ public class CipherServiceTests
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider, Cipher cipher, Guid savingUserId)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var stream = new MemoryStream();
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task CreateAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
|
||||
// Setup cipher with user ownership
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
// Mock user storage and premium access
|
||||
var user = new User { Id = savingUserId, MaxStorageGb = 1 };
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(savingUserId)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.ValidateFileAsync(cipher, Arg.Any<CipherAttachment.MetaData>(), Arg.Any<long>())
|
||||
.Returns((true, 100L));
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, lastKnownRevisionDate);
|
||||
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentForDelayedUploadAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider, Cipher cipher, Guid savingUserId)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var key = "test-key";
|
||||
var fileName = "test.txt";
|
||||
var fileSize = 100L;
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAttachmentForDelayedUploadAsync(cipher, key, fileName, fileSize, false, savingUserId, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task CreateAttachmentForDelayedUploadAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var key = "test-key";
|
||||
var fileName = "test.txt";
|
||||
var fileSize = 100L;
|
||||
|
||||
// Setup cipher with user ownership
|
||||
cipher.UserId = savingUserId;
|
||||
cipher.OrganizationId = null;
|
||||
|
||||
// Mock user storage and premium access
|
||||
var user = new User { Id = savingUserId, MaxStorageGb = 1 };
|
||||
sutProvider.GetDependency<IUserRepository>()
|
||||
.GetByIdAsync(savingUserId)
|
||||
.Returns(user);
|
||||
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.CanAccessPremium(user)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.GetAttachmentUploadUrlAsync(cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns("https://example.com/upload");
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
var result = await sutProvider.Sut.CreateAttachmentForDelayedUploadAsync(cipher, key, fileName, fileSize, false, savingUserId, lastKnownRevisionDate);
|
||||
|
||||
Assert.NotNull(result.attachmentId);
|
||||
Assert.NotNull(result.uploadUrl);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UploadFileForExistingAttachmentAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var stream = new MemoryStream();
|
||||
var attachment = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = "test-attachment-id",
|
||||
Size = 100,
|
||||
FileName = "test.txt",
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task UploadFileForExistingAttachmentAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var attachmentId = "test-attachment-id";
|
||||
var attachment = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
Size = 100,
|
||||
FileName = "test.txt",
|
||||
Key = "test-key"
|
||||
};
|
||||
|
||||
// Set the attachment on the cipher so ValidateCipherAttachmentFile can find it
|
||||
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||
{
|
||||
[attachmentId] = attachment
|
||||
});
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadNewAttachmentAsync(stream, cipher, attachment)
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.ValidateFileAsync(cipher, attachment, Arg.Any<long>())
|
||||
.Returns((true, 100L));
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sutProvider.Sut.UploadFileForExistingAttachmentAsync(stream, cipher, attachment, lastKnownRevisionDate);
|
||||
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadNewAttachmentAsync(stream, cipher, attachment);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAttachmentShareAsync_WrongRevisionDate_Throws(SutProvider<CipherService> sutProvider,
|
||||
Cipher cipher, Guid organizationId)
|
||||
{
|
||||
var lastKnownRevisionDate = cipher.RevisionDate.AddDays(-1);
|
||||
var stream = new MemoryStream();
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
var attachmentId = "attachment-id";
|
||||
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate));
|
||||
Assert.Contains("out of date", exception.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("")]
|
||||
[BitAutoData("Correct Time")]
|
||||
public async Task CreateAttachmentShareAsync_CorrectRevisionDate_DoesNotThrow(string revisionDateString,
|
||||
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid organizationId)
|
||||
{
|
||||
var lastKnownRevisionDate = string.IsNullOrEmpty(revisionDateString) ? (DateTime?)null : cipher.RevisionDate;
|
||||
var stream = new MemoryStream(new byte[100]);
|
||||
var fileName = "test.txt";
|
||||
var key = "test-key";
|
||||
var attachmentId = "attachment-id";
|
||||
|
||||
// Setup cipher with existing attachment (no TempMetadata)
|
||||
cipher.OrganizationId = null;
|
||||
cipher.SetAttachments(new Dictionary<string, CipherAttachment.MetaData>
|
||||
{
|
||||
[attachmentId] = new CipherAttachment.MetaData
|
||||
{
|
||||
AttachmentId = attachmentId,
|
||||
Size = 100,
|
||||
FileName = "existing.txt",
|
||||
Key = "existing-key"
|
||||
}
|
||||
});
|
||||
|
||||
// Mock organization
|
||||
var organization = new Organization
|
||||
{
|
||||
Id = organizationId,
|
||||
MaxStorageGb = 1
|
||||
};
|
||||
sutProvider.GetDependency<IOrganizationRepository>()
|
||||
.GetByIdAsync(organizationId)
|
||||
.Returns(organization);
|
||||
|
||||
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||
.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any<CipherAttachment.MetaData>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
sutProvider.GetDependency<ICipherRepository>()
|
||||
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||
.Returns(Task.CompletedTask);
|
||||
|
||||
await sutProvider.Sut.CreateAttachmentShareAsync(cipher, stream, fileName, key, 100, attachmentId, organizationId, lastKnownRevisionDate);
|
||||
|
||||
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||
.UploadShareAttachmentAsync(stream, cipher.Id, organizationId, Arg.Any<CipherAttachment.MetaData>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SaveDetailsAsync_PersonalVault_WithOrganizationDataOwnershipPolicyEnabled_Throws(
|
||||
|
||||
250
test/Notifications.Test/HubHelpersTest.cs
Normal file
250
test/Notifications.Test/HubHelpersTest.cs
Normal file
@@ -0,0 +1,250 @@
|
||||
#nullable enable
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Test.NotificationCenter.AutoFixture;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Notifications;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using NSubstitute;
|
||||
|
||||
namespace Notifications.Test;
|
||||
|
||||
[SutProviderCustomize]
|
||||
[NotificationCustomize(false)]
|
||||
public class HubHelpersTest
|
||||
{
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task SendNotificationToHubAsync_NotificationPushNotificationGlobal_NothingSent(
|
||||
SutProvider<HubHelpers> sutProvider,
|
||||
NotificationPushNotification notification,
|
||||
string contextId, CancellationToken cancellationToke)
|
||||
{
|
||||
notification.Global = true;
|
||||
notification.InstallationId = null;
|
||||
notification.UserId = null;
|
||||
notification.OrganizationId = null;
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToke);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).Group(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
SendNotificationToHubAsync_NotificationPushNotificationInstallationIdProvidedClientTypeAll_SentToGroupInstallation(
|
||||
SutProvider<HubHelpers> sutProvider,
|
||||
NotificationPushNotification notification,
|
||||
string contextId, CancellationToken cancellationToken)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.OrganizationId = null;
|
||||
notification.ClientType = ClientType.All;
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"Installation_{notification.InstallationId!.Value.ToString()}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
public async Task
|
||||
SendNotificationToHubAsync_NotificationPushNotificationInstallationIdProvidedClientTypeNotAll_SentToGroupInstallationClientType(
|
||||
ClientType clientType, SutProvider<HubHelpers> sutProvider,
|
||||
NotificationPushNotification notification,
|
||||
string contextId, CancellationToken cancellationToken)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.OrganizationId = null;
|
||||
notification.ClientType = clientType;
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"Installation_ClientType_{notification.InstallationId!.Value}_{clientType}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false)]
|
||||
[BitAutoData(true)]
|
||||
public async Task SendNotificationToHubAsync_NotificationPushNotificationUserIdProvidedClientTypeAll_SentToUser(
|
||||
bool organizationIdProvided, SutProvider<HubHelpers> sutProvider,
|
||||
NotificationPushNotification notification,
|
||||
string contextId, CancellationToken cancellationToken)
|
||||
{
|
||||
notification.InstallationId = null;
|
||||
notification.ClientType = ClientType.All;
|
||||
if (!organizationIdProvided)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
}
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.User(notification.UserId!.Value.ToString())
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).Group(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(false, ClientType.Browser)]
|
||||
[BitAutoData(false, ClientType.Desktop)]
|
||||
[BitAutoData(false, ClientType.Mobile)]
|
||||
[BitAutoData(false, ClientType.Web)]
|
||||
[BitAutoData(true, ClientType.Browser)]
|
||||
[BitAutoData(true, ClientType.Desktop)]
|
||||
[BitAutoData(true, ClientType.Mobile)]
|
||||
[BitAutoData(true, ClientType.Web)]
|
||||
public async Task
|
||||
SendNotificationToHubAsync_NotificationPushNotificationUserIdProvidedClientTypeNotAll_SentToGroupUserClientType(
|
||||
bool organizationIdProvided, ClientType clientType, SutProvider<HubHelpers> sutProvider,
|
||||
NotificationPushNotification notification,
|
||||
string contextId, CancellationToken cancellationToken)
|
||||
{
|
||||
notification.InstallationId = null;
|
||||
notification.ClientType = clientType;
|
||||
if (!organizationIdProvided)
|
||||
{
|
||||
notification.OrganizationId = null;
|
||||
}
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"UserClientType_{notification.UserId!.Value}_{clientType}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task
|
||||
SendNotificationToHubAsync_NotificationPushNotificationOrganizationIdProvidedClientTypeAll_SentToGroupOrganization(
|
||||
SutProvider<HubHelpers> sutProvider, string contextId,
|
||||
NotificationPushNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.InstallationId = null;
|
||||
notification.ClientType = ClientType.All;
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"Organization_{notification.OrganizationId!.Value}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(ClientType.Browser)]
|
||||
[BitAutoData(ClientType.Desktop)]
|
||||
[BitAutoData(ClientType.Mobile)]
|
||||
[BitAutoData(ClientType.Web)]
|
||||
public async Task
|
||||
SendNotificationToHubAsync_NotificationPushNotificationOrganizationIdProvidedClientTypeNotAll_SentToGroupOrganizationClientType(
|
||||
ClientType clientType, SutProvider<HubHelpers> sutProvider, string contextId,
|
||||
NotificationPushNotification notification,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
notification.UserId = null;
|
||||
notification.InstallationId = null;
|
||||
notification.ClientType = clientType;
|
||||
|
||||
var json = ToNotificationJson(notification, PushType.Notification, contextId);
|
||||
await sutProvider.Sut.SendNotificationToHubAsync(json, cancellationToken);
|
||||
|
||||
sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
await sutProvider.GetDependency<IHubContext<NotificationsHub>>().Clients.Received(1)
|
||||
.Group($"OrganizationClientType_{notification.OrganizationId!.Value}_{clientType}")
|
||||
.Received(1)
|
||||
.SendCoreAsync("ReceiveMessage", Arg.Is<object?[]>(objects =>
|
||||
objects.Length == 1 && IsNotificationPushNotificationEqual(notification, objects[0],
|
||||
PushType.Notification, contextId)),
|
||||
cancellationToken);
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0).User(Arg.Any<string>());
|
||||
sutProvider.GetDependency<IHubContext<AnonymousNotificationsHub>>().Clients.Received(0)
|
||||
.Group(Arg.Any<string>());
|
||||
}
|
||||
|
||||
private static string ToNotificationJson(object payload, PushType type, string contextId)
|
||||
{
|
||||
var notification = new PushNotificationData<object>(type, payload, contextId);
|
||||
return JsonSerializer.Serialize(notification, JsonHelpers.IgnoreWritingNull);
|
||||
}
|
||||
|
||||
private static bool IsNotificationPushNotificationEqual(NotificationPushNotification expected, object? actual,
|
||||
PushType type, string contextId)
|
||||
{
|
||||
if (actual is not PushNotificationData<NotificationPushNotification> pushNotificationData)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return pushNotificationData.Type == type &&
|
||||
pushNotificationData.ContextId == contextId &&
|
||||
expected.Id == pushNotificationData.Payload.Id &&
|
||||
expected.UserId == pushNotificationData.Payload.UserId &&
|
||||
expected.OrganizationId == pushNotificationData.Payload.OrganizationId &&
|
||||
expected.ClientType == pushNotificationData.Payload.ClientType &&
|
||||
expected.RevisionDate == pushNotificationData.Payload.RevisionDate;
|
||||
}
|
||||
}
|
||||
@@ -18,5 +18,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Notifications\Notifications.csproj" />
|
||||
<ProjectReference Include="..\Common\Common.csproj" />
|
||||
<ProjectReference Include="..\Core.Test\Core.Test.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user