using System.Net; using System.Net.Http.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; 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; namespace Bit.Events.IntegrationTest.Controllers; public class CollectControllerTests : IAsyncLifetime { private EventsApplicationFactory _factory = null!; private HttpClient _client = null!; private string _ownerEmail = null!; private Guid _ownerId; 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(); 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?>("collect", null); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Post_EmptyModel_ReturnsBadRequest() { var response = await _client.PostAsJsonAsync("collect", Array.Empty()); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } [Fact] public async Task Post_UserClientExportedVault_Success() { var response = await _client.PostAsJsonAsync>("collect", [ new EventModel { Type = EventType.User_ClientExportedVault, Date = DateTime.UtcNow, }, ]); response.EnsureSuccessStatusCode(); } [Fact] public async Task Post_CipherClientAutofilled_WithValidCipher_Success() { var cipher = await CreateCipherForUserAsync(_ownerId); var response = await _client.PostAsJsonAsync>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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>("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 CreateCipherForUserAsync(Guid userId) { var cipherRepository = _factory.GetService(); 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 CreateOrganizationAsync(Guid ownerId) { var organizationRepository = _factory.GetService(); var organizationUserRepository = _factory.GetService(); 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; } }