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(); _eventService = Substitute.For(); _cipherRepository = Substitute.For(); _organizationRepository = Substitute.For(); _sut = new CollectController( _currentContext, _eventService, _cipherRepository, _organizationRepository ); } [Fact] public async Task Post_NullModel_ReturnsBadRequest() { var result = await _sut.Post(null); Assert.IsType(result); } [Fact] public async Task Post_EmptyModel_ReturnsBadRequest() { var result = await _sut.Post(new List()); Assert.IsType(result); } [Theory] [AutoData] public async Task Post_UserClientExportedVault_LogsUserEvent(Guid userId) { _currentContext.UserId.Returns(userId); var eventDate = DateTime.UtcNow; var events = new List { new EventModel { Type = EventType.User_ClientExportedVault, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientCopiedPassword, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientCopiedHiddenField, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientCopiedCardCode, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientToggledCardNumberVisible, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientToggledCardCodeVisible, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientToggledHiddenFieldVisible, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientToggledPasswordVisible, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientViewed, CipherId = cipherId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = null, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, OrganizationId = null, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, OrganizationId = orgId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(result); await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId); await _cipherRepository.Received(1).GetByIdAsync(cipherId); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>( 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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, OrganizationId = orgId, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, OrganizationId = orgId, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Cipher_ClientAutofilled, CipherId = cipherId, OrganizationId = orgId, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { 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(result); await _cipherRepository.Received(1).GetByIdAsync(cipherId, userId); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>(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 { new EventModel { Type = EventType.Organization_ClientExportedVault, OrganizationId = orgId, Date = eventDate } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Organization_ClientExportedVault, OrganizationId = null, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.Organization_ClientExportedVault, OrganizationId = orgId, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { new EventModel { Type = EventType.User_LoggedIn, Date = DateTime.UtcNow } }; var result = await _sut.Post(events); Assert.IsType(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 { 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(result); await _eventService.Received(1).LogUserEventAsync(userId, EventType.User_ClientExportedVault, Arg.Any()); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>(tuples => tuples.Count() == 1) ); await _eventService.Received(1).LogOrganizationEventAsync(organization, EventType.Organization_ClientExportedVault, Arg.Any()); } [Theory] [AutoData] public async Task Post_MoreThan50CipherEvents_LogsInBatches(Guid userId, List ciphers) { _currentContext.UserId.Returns(userId); var events = new List(); 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(result); await _eventService.Received(2).LogCipherEventsAsync( Arg.Is>>(tuples => tuples.Count() == 50) ); } [Theory] [AutoData] public async Task Post_Exactly50CipherEvents_LogsInSingleBatch(Guid userId, List ciphers) { _currentContext.UserId.Returns(userId); var events = new List(); 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(result); await _eventService.Received(1).LogCipherEventsAsync( Arg.Is>>(tuples => tuples.Count() == 50) ); } }