mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
* Handle null cipher * Check for an org being null too * Add unit and integration tests * Clean up unused members
493 lines
14 KiB
C#
493 lines
14 KiB
C#
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<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
|
|
{
|
|
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<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;
|
|
}
|
|
}
|