mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
Handle null cipher or organization with event submission (#6509)
* Handle null cipher * Check for an org being null too * Add unit and integration tests * Clean up unused members
This commit is contained in:
@@ -21,23 +21,17 @@ public class CollectController : Controller
|
|||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly ICipherRepository _cipherRepository;
|
private readonly ICipherRepository _cipherRepository;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
|
||||||
|
|
||||||
public CollectController(
|
public CollectController(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository)
|
||||||
IFeatureService featureService,
|
|
||||||
IApplicationCacheService applicationCacheService)
|
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_featureService = featureService;
|
|
||||||
_applicationCacheService = applicationCacheService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
@@ -47,8 +41,10 @@ public class CollectController : Controller
|
|||||||
{
|
{
|
||||||
return new BadRequestResult();
|
return new BadRequestResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
var cipherEvents = new List<Tuple<Cipher, EventType, DateTime?>>();
|
var cipherEvents = new List<Tuple<Cipher, EventType, DateTime?>>();
|
||||||
var ciphersCache = new Dictionary<Guid, Cipher>();
|
var ciphersCache = new Dictionary<Guid, Cipher>();
|
||||||
|
|
||||||
foreach (var eventModel in model)
|
foreach (var eventModel in model)
|
||||||
{
|
{
|
||||||
switch (eventModel.Type)
|
switch (eventModel.Type)
|
||||||
@@ -57,6 +53,7 @@ public class CollectController : Controller
|
|||||||
case EventType.User_ClientExportedVault:
|
case EventType.User_ClientExportedVault:
|
||||||
await _eventService.LogUserEventAsync(_currentContext.UserId.Value, eventModel.Type, eventModel.Date);
|
await _eventService.LogUserEventAsync(_currentContext.UserId.Value, eventModel.Type, eventModel.Date);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
// Cipher events
|
// Cipher events
|
||||||
case EventType.Cipher_ClientAutofilled:
|
case EventType.Cipher_ClientAutofilled:
|
||||||
case EventType.Cipher_ClientCopiedHiddenField:
|
case EventType.Cipher_ClientCopiedHiddenField:
|
||||||
@@ -71,7 +68,8 @@ public class CollectController : Controller
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Cipher cipher = null;
|
|
||||||
|
Cipher cipher;
|
||||||
if (ciphersCache.TryGetValue(eventModel.CipherId.Value, out var cachedCipher))
|
if (ciphersCache.TryGetValue(eventModel.CipherId.Value, out var cachedCipher))
|
||||||
{
|
{
|
||||||
cipher = cachedCipher;
|
cipher = cachedCipher;
|
||||||
@@ -81,6 +79,7 @@ public class CollectController : Controller
|
|||||||
cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value,
|
cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value,
|
||||||
_currentContext.UserId.Value);
|
_currentContext.UserId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipher == null)
|
if (cipher == null)
|
||||||
{
|
{
|
||||||
// When the user cannot access the cipher directly, check if the organization allows for
|
// When the user cannot access the cipher directly, check if the organization allows for
|
||||||
@@ -91,29 +90,44 @@ public class CollectController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value);
|
cipher = await _cipherRepository.GetByIdAsync(eventModel.CipherId.Value);
|
||||||
|
if (cipher == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var cipherBelongsToOrg = cipher.OrganizationId == eventModel.OrganizationId;
|
var cipherBelongsToOrg = cipher.OrganizationId == eventModel.OrganizationId;
|
||||||
var org = _currentContext.GetOrganization(eventModel.OrganizationId.Value);
|
var org = _currentContext.GetOrganization(eventModel.OrganizationId.Value);
|
||||||
|
|
||||||
if (!cipherBelongsToOrg || org == null || cipher == null)
|
if (!cipherBelongsToOrg || org == null)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ciphersCache.TryAdd(eventModel.CipherId.Value, cipher);
|
ciphersCache.TryAdd(eventModel.CipherId.Value, cipher);
|
||||||
cipherEvents.Add(new Tuple<Cipher, EventType, DateTime?>(cipher, eventModel.Type, eventModel.Date));
|
cipherEvents.Add(new Tuple<Cipher, EventType, DateTime?>(cipher, eventModel.Type, eventModel.Date));
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case EventType.Organization_ClientExportedVault:
|
case EventType.Organization_ClientExportedVault:
|
||||||
if (!eventModel.OrganizationId.HasValue)
|
if (!eventModel.OrganizationId.HasValue)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(eventModel.OrganizationId.Value);
|
var organization = await _organizationRepository.GetByIdAsync(eventModel.OrganizationId.Value);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
await _eventService.LogOrganizationEventAsync(organization, eventModel.Type, eventModel.Date);
|
await _eventService.LogOrganizationEventAsync(organization, eventModel.Type, eventModel.Date);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cipherEvents.Any())
|
if (cipherEvents.Any())
|
||||||
{
|
{
|
||||||
foreach (var eventsBatch in cipherEvents.Chunk(50))
|
foreach (var eventsBatch in cipherEvents.Chunk(50))
|
||||||
@@ -121,6 +135,7 @@ public class CollectController : Controller
|
|||||||
await _eventService.LogCipherEventsAsync(eventsBatch);
|
await _eventService.LogCipherEventsAsync(eventsBatch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new OkResult();
|
return new OkResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,63 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Enums;
|
||||||
|
using Bit.Core.Vault.Repositories;
|
||||||
using Bit.Events.Models;
|
using Bit.Events.Models;
|
||||||
|
|
||||||
namespace Bit.Events.IntegrationTest.Controllers;
|
namespace Bit.Events.IntegrationTest.Controllers;
|
||||||
|
|
||||||
public class CollectControllerTests
|
public class CollectControllerTests : IAsyncLifetime
|
||||||
{
|
{
|
||||||
// This is a very simple test, and should be updated to assert more things, but for now
|
private EventsApplicationFactory _factory = null!;
|
||||||
// it ensures that the events startup doesn't throw any errors with fairly basic configuration.
|
private HttpClient _client = null!;
|
||||||
[Fact]
|
private string _ownerEmail = null!;
|
||||||
public async Task Post_Works()
|
private Guid _ownerId;
|
||||||
{
|
|
||||||
var eventsApplicationFactory = new EventsApplicationFactory();
|
|
||||||
var (accessToken, _) = await eventsApplicationFactory.LoginWithNewAccount();
|
|
||||||
var client = eventsApplicationFactory.CreateAuthedClient(accessToken);
|
|
||||||
|
|
||||||
var response = await client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_factory = new EventsApplicationFactory();
|
||||||
|
_ownerEmail = $"integration-test+{Guid.NewGuid()}@bitwarden.com";
|
||||||
|
var (accessToken, _) = await _factory.LoginWithNewAccount(_ownerEmail);
|
||||||
|
_client = _factory.CreateAuthedClient(accessToken);
|
||||||
|
|
||||||
|
// Get the user ID
|
||||||
|
var userRepository = _factory.GetService<IUserRepository>();
|
||||||
|
var user = await userRepository.GetByEmailAsync(_ownerEmail);
|
||||||
|
_ownerId = user!.Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task DisposeAsync()
|
||||||
|
{
|
||||||
|
_client?.Dispose();
|
||||||
|
_factory?.Dispose();
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_NullModel_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>?>("collect", null);
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_EmptyModel_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync("collect", Array.Empty<EventModel>());
|
||||||
|
|
||||||
|
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_UserClientExportedVault_Success()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
[
|
[
|
||||||
new EventModel
|
new EventModel
|
||||||
{
|
{
|
||||||
@@ -26,4 +68,425 @@ public class CollectControllerTests
|
|||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientAutofilled_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientCopiedPassword_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedPassword,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientCopiedHiddenField_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedHiddenField,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientCopiedCardCode_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedCardCode,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientToggledCardNumberVisible_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledCardNumberVisible,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientToggledCardCodeVisible_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledCardCodeVisible,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientToggledHiddenFieldVisible_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientToggledPasswordVisible_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledPasswordVisible,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherClientViewed_WithValidCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherEvent_WithoutCipherId_Success()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherEvent_WithInvalidCipherId_Success()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = Guid.NewGuid(),
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithValidOrganization_Success()
|
||||||
|
{
|
||||||
|
var organization = await CreateOrganizationAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithoutOrganizationId_Success()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithInvalidOrganizationId_Success()
|
||||||
|
{
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = Guid.NewGuid(),
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_MultipleEvents_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
var organization = await CreateOrganizationAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_ClientExportedVault,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherEventsBatch_MoreThan50Items_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
// Create 60 cipher events to test batching logic (should be processed in 2 batches of 50)
|
||||||
|
var events = Enumerable.Range(0, 60)
|
||||||
|
.Select(_ => new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect", events);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_UnsupportedEventType_Success()
|
||||||
|
{
|
||||||
|
// Testing with an event type not explicitly handled in the switch statement
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_LoggedIn,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_MixedValidAndInvalidEvents_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_ClientExportedVault,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = Guid.NewGuid(), // Invalid cipher ID
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipher.Id, // Valid cipher ID
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_CipherCaching_MultipleEventsForSameCipher_Success()
|
||||||
|
{
|
||||||
|
var cipher = await CreateCipherForUserAsync(_ownerId);
|
||||||
|
|
||||||
|
// Multiple events for the same cipher should use caching
|
||||||
|
var response = await _client.PostAsJsonAsync<IEnumerable<EventModel>>("collect",
|
||||||
|
[
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedPassword,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Cipher> CreateCipherForUserAsync(Guid userId)
|
||||||
|
{
|
||||||
|
var cipherRepository = _factory.GetService<ICipherRepository>();
|
||||||
|
|
||||||
|
var cipher = new Cipher
|
||||||
|
{
|
||||||
|
Type = CipherType.Login,
|
||||||
|
UserId = userId,
|
||||||
|
Data = "{\"name\":\"Test Cipher\"}",
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await cipherRepository.CreateAsync(cipher);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Organization> CreateOrganizationAsync(Guid ownerId)
|
||||||
|
{
|
||||||
|
var organizationRepository = _factory.GetService<IOrganizationRepository>();
|
||||||
|
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
|
||||||
|
|
||||||
|
var organization = new Organization
|
||||||
|
{
|
||||||
|
Name = "Test Organization",
|
||||||
|
BillingEmail = _ownerEmail,
|
||||||
|
Plan = "Free",
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
RevisionDate = DateTime.UtcNow,
|
||||||
|
};
|
||||||
|
|
||||||
|
await organizationRepository.CreateAsync(organization);
|
||||||
|
|
||||||
|
// Add the user as an owner of the organization
|
||||||
|
var organizationUser = new OrganizationUser
|
||||||
|
{
|
||||||
|
OrganizationId = organization.Id,
|
||||||
|
UserId = ownerId,
|
||||||
|
Status = OrganizationUserStatusType.Confirmed,
|
||||||
|
Type = OrganizationUserType.Owner,
|
||||||
|
};
|
||||||
|
|
||||||
|
await organizationUserRepository.CreateAsync(organizationUser);
|
||||||
|
|
||||||
|
return organization;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
715
test/Events.Test/Controllers/CollectControllerTests.cs
Normal file
715
test/Events.Test/Controllers/CollectControllerTests.cs
Normal file
@@ -0,0 +1,715 @@
|
|||||||
|
using AutoFixture.Xunit2;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
|
using Bit.Core.Vault.Models.Data;
|
||||||
|
using Bit.Core.Vault.Repositories;
|
||||||
|
using Bit.Events.Controllers;
|
||||||
|
using Bit.Events.Models;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NSubstitute;
|
||||||
|
|
||||||
|
namespace Events.Test.Controllers;
|
||||||
|
|
||||||
|
public class CollectControllerTests
|
||||||
|
{
|
||||||
|
private readonly CollectController _sut;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IEventService _eventService;
|
||||||
|
private readonly ICipherRepository _cipherRepository;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
|
||||||
|
public CollectControllerTests()
|
||||||
|
{
|
||||||
|
_currentContext = Substitute.For<ICurrentContext>();
|
||||||
|
_eventService = Substitute.For<IEventService>();
|
||||||
|
_cipherRepository = Substitute.For<ICipherRepository>();
|
||||||
|
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||||
|
|
||||||
|
_sut = new CollectController(
|
||||||
|
_currentContext,
|
||||||
|
_eventService,
|
||||||
|
_cipherRepository,
|
||||||
|
_organizationRepository
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_NullModel_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var result = await _sut.Post(null);
|
||||||
|
|
||||||
|
Assert.IsType<BadRequestResult>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Post_EmptyModel_ReturnsBadRequest()
|
||||||
|
{
|
||||||
|
var result = await _sut.Post(new List<EventModel>());
|
||||||
|
|
||||||
|
Assert.IsType<BadRequestResult>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_UserClientExportedVault_LogsUserEvent(Guid userId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_ClientExportedVault,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, eventDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherAutofilled_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.Count() == 1 &&
|
||||||
|
tuples.First().Item1 == cipherDetails &&
|
||||||
|
tuples.First().Item2 == EventType.Cipher_ClientAutofilled &&
|
||||||
|
tuples.First().Item3 == eventDate
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientCopiedPassword_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedPassword,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientCopiedPassword
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientCopiedHiddenField_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedHiddenField,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientCopiedHiddenField
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientCopiedCardCode_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientCopiedCardCode,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientCopiedCardCode
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientToggledCardNumberVisible_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledCardNumberVisible,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientToggledCardNumberVisible
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientToggledCardCodeVisible_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledCardCodeVisible,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientToggledCardCodeVisible
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientToggledHiddenFieldVisible_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledHiddenFieldVisible,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientToggledHiddenFieldVisible
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientToggledPasswordVisible_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientToggledPasswordVisible,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientToggledPasswordVisible
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherClientViewed_WithValidCipher_LogsCipherEvent(Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item2 == EventType.Cipher_ClientViewed
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_WithoutCipherId_SkipsEvent(Guid userId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = null,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default, default);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_WithNullCipher_WithoutOrgId_SkipsEvent(Guid userId, Guid cipherId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
OrganizationId = null,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId);
|
||||||
|
await _cipherRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(cipherId);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_WithNullCipher_WithOrgId_ChecksOrgCipher(
|
||||||
|
Guid userId, Guid cipherId, Guid orgId, Cipher cipher, CurrentContextOrganization org)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipher.Id = cipherId;
|
||||||
|
cipher.OrganizationId = orgId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId).Returns(cipher);
|
||||||
|
_currentContext.GetOrganization(orgId).Returns(org);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(
|
||||||
|
tuples => tuples.First().Item1 == cipher
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_WithNullCipher_OrgCipherNotFound_SkipsEvent(
|
||||||
|
Guid userId, Guid cipherId, Guid orgId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId).Returns((CipherDetails?)null);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_CipherDoesNotBelongToOrg_SkipsEvent(
|
||||||
|
Guid userId, Guid cipherId, Guid orgId, Guid differentOrgId, Cipher cipher)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipher.Id = cipherId;
|
||||||
|
cipher.OrganizationId = differentOrgId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId).Returns(cipher);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_CipherEvent_OrgNotFound_SkipsEvent(
|
||||||
|
Guid userId, Guid cipherId, Guid orgId, Cipher cipher)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipher.Id = cipherId;
|
||||||
|
cipher.OrganizationId = orgId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns((CipherDetails?)null);
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId).Returns(cipher);
|
||||||
|
_currentContext.GetOrganization(orgId).Returns((CurrentContextOrganization)null);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogCipherEventsAsync(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_MultipleCipherEvents_WithSameCipherId_UsesCachedCipher(
|
||||||
|
Guid userId, Guid cipherId, CipherDetails cipherDetails)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientViewed,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(tuples => tuples.Count() == 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithValidOrg_LogsOrgEvent(
|
||||||
|
Guid userId, Guid orgId, Organization organization)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
organization.Id = orgId;
|
||||||
|
_organizationRepository.GetByIdAsync(orgId).Returns(organization);
|
||||||
|
var eventDate = DateTime.UtcNow;
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = eventDate
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(orgId);
|
||||||
|
await _eventService.Received(1).LogOrganizationEventAsync(organization, EventType.Organization_ClientExportedVault, eventDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithoutOrgId_SkipsEvent(Guid userId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = null,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogOrganizationEventAsync(default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_OrganizationClientExportedVault_WithNullOrg_SkipsEvent(Guid userId, Guid orgId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
_organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _organizationRepository.Received(1).GetByIdAsync(orgId);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogOrganizationEventAsync(default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_UnsupportedEventType_SkipsEvent(Guid userId)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_LoggedIn,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.DidNotReceiveWithAnyArgs().LogUserEventAsync(default, default, default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_MixedEventTypes_ProcessesAllEvents(
|
||||||
|
Guid userId, Guid cipherId, Guid orgId, CipherDetails cipherDetails, Organization organization)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
cipherDetails.Id = cipherId;
|
||||||
|
organization.Id = orgId;
|
||||||
|
_cipherRepository.GetByIdAsync(cipherId, userId).Returns(cipherDetails);
|
||||||
|
_organizationRepository.GetByIdAsync(orgId).Returns(organization);
|
||||||
|
var events = new List<EventModel>
|
||||||
|
{
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.User_ClientExportedVault,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipherId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
},
|
||||||
|
new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Organization_ClientExportedVault,
|
||||||
|
OrganizationId = orgId,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, Arg.Any<DateTime?>());
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(tuples => tuples.Count() == 1)
|
||||||
|
);
|
||||||
|
await _eventService.Received(1).LogOrganizationEventAsync(organization, EventType.Organization_ClientExportedVault, Arg.Any<DateTime?>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_MoreThan50CipherEvents_LogsInBatches(Guid userId, List<CipherDetails> ciphers)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var events = new List<EventModel>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
var cipher = ciphers[i % ciphers.Count];
|
||||||
|
_cipherRepository.GetByIdAsync(cipher.Id, userId).Returns(cipher);
|
||||||
|
events.Add(new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(2).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(tuples => tuples.Count() == 50)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[AutoData]
|
||||||
|
public async Task Post_Exactly50CipherEvents_LogsInSingleBatch(Guid userId, List<CipherDetails> ciphers)
|
||||||
|
{
|
||||||
|
_currentContext.UserId.Returns(userId);
|
||||||
|
var events = new List<EventModel>();
|
||||||
|
|
||||||
|
for (int i = 0; i < 50; i++)
|
||||||
|
{
|
||||||
|
var cipher = ciphers[i % ciphers.Count];
|
||||||
|
_cipherRepository.GetByIdAsync(cipher.Id, userId).Returns(cipher);
|
||||||
|
events.Add(new EventModel
|
||||||
|
{
|
||||||
|
Type = EventType.Cipher_ClientAutofilled,
|
||||||
|
CipherId = cipher.Id,
|
||||||
|
Date = DateTime.UtcNow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = await _sut.Post(events);
|
||||||
|
|
||||||
|
Assert.IsType<OkResult>(result);
|
||||||
|
await _eventService.Received(1).LogCipherEventsAsync(
|
||||||
|
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(tuples => tuples.Count() == 50)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,14 +9,18 @@
|
|||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||||
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio"
|
<PackageReference Include="xunit.runner.visualstudio"
|
||||||
Version="$(XUnitRunnerVisualStudioVersion)">
|
Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
<PackageReference Include="AutoFixture.Xunit2" Version="$(AutoFixtureXUnit2Version)" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
<ProjectReference Include="..\..\src\Events\Events.csproj" />
|
||||||
|
<ProjectReference Include="..\..\src\Core\Core.csproj" />
|
||||||
|
<ProjectReference Include="..\Common\Common.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace Events.Test;
|
|
||||||
|
|
||||||
// Delete this file once you have real tests
|
|
||||||
public class PlaceholderUnitTest
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user