1
0
mirror of https://github.com/bitwarden/server synced 2025-12-11 13:53:40 +00:00
Files
server/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationsControllerPerformanceTests.cs
Rui Tomé 80ee31b4fe [PM-25015] Add performance tests for Admin Console endpoints (#6235)
* Add GroupsRecipe to manage group creation and user relationships in organizations

* Add CollectionsRecipe to manage collection creation and user relationships in organizations

* Refactor OrganizationUsersControllerPerformanceTests to enhance performance testing and add new test cases

* Add OrganizationDomainRecipe to add verified domains for organizations

* Add more tests to OrganizationUsersControllerPerformanceTests and enhance seeding logic for organizations

- Updated performance tests to use dynamic domain generation for organization users.
- Refactored seeding methods in OrganizationWithUsersRecipe to accept user status and type.
- Modified AddToOrganization methods in CollectionsRecipe and GroupsRecipe to return created IDs.
- Adjusted DbSeederUtility to align with new seeding method signatures.

* Enhance OrganizationSeeder with additional configuration options and update seat calculation in OrganizationWithUsersRecipe to ensure a minimum of 1000 seats.

* Add performance tests for Groups, Organizations, Organization Users, and Provider Organizations controllers

- Introduced `GroupsControllerPerformanceTests` to validate the performance of the PutGroupAsync method.
- Added `OrganizationsControllerPerformanceTests` with multiple tests including DeleteOrganizationAsync, DeleteOrganizationWithTokenAsync, PostStorageAsync, and CreateWithoutPaymentAsync.
- Enhanced `OrganizationUsersControllerPerformanceTests` with DeleteSingleUserAccountAsync and InviteUsersAsync methods to test user account deletion and bulk invitations.
- Created `ProviderOrganizationsControllerPerformanceTests` to assess the performance of deleting provider organizations.

These tests ensure the reliability and efficiency of the respective controller actions under various scenarios.

* Refactor GroupsControllerPerformanceTests to use parameterized tests

- Renamed `GroupsControllerPerformanceTest` to `GroupsControllerPerformanceTests` for consistency.
- Updated `PutGroupAsync` method to use `[Theory]` with `InlineData` for dynamic user and collection counts.
- Adjusted organization user and collection seeding logic to utilize the new parameters.
- Enhanced logging to provide clearer performance metrics during tests.

* Update domain generation in GroupsControllerPerformanceTests for improved test consistency

* Remove ProviderOrganizationsControllerPerformanceTests

* Refactor performance tests for Groups, Organizations, and Organization Users controllers

- Updated method names for clarity and consistency, e.g., `PutGroupAsync` to `UpdateGroup_WithUsersAndCollections`.
- Enhanced test documentation with XML comments to describe the purpose of each test.
- Improved domain generation logic for consistency across tests.
- Adjusted logging to provide detailed performance metrics during test execution.
- Renamed several test methods to better reflect their functionality.

* Refactor performance tests in Organizations and Organization Users controllers

- Updated tests to use parameterized `[Theory]` attributes with `InlineData` for dynamic user, collection, and group counts.
- Enhanced logging to include detailed metrics such as user and collection counts during test execution.
- Marked several tests as skipped for performance considerations.
- Removed unused code and improved organization of test methods for clarity.

* Add bulk reinvite users performance test to OrganizationUsersControllerPerformanceTests

- Implemented a new performance test for the POST /organizations/{orgId}/users/reinvite endpoint.
- Utilized parameterized testing with `[Theory]` and `InlineData` to evaluate performance with varying user counts.
- Enhanced logging to capture request duration and response status for better performance insights.
- Updated OrganizationSeeder to conditionally set email based on user status during seeding.

* Refactor domain generation in performance tests to use OrganizationTestHelpers

- Updated domain generation logic in GroupsControllerPerformanceTests, OrganizationsControllerPerformanceTests, and OrganizationUsersControllerPerformanceTests to utilize the new GenerateRandomDomain method from OrganizationTestHelpers.
- This change enhances consistency and readability across the tests by centralizing domain generation logic.

* Update CollectionsRecipe to have better readability

* Update GroupsRecipe to have better readability

* Refactor authentication in performance tests to use centralized helper method. This change reduces code duplication across Groups, Organizations, and OrganizationUsers controller tests by implementing the `AuthenticateClientAsync` method in a new `PerformanceTestHelpers` class.

* Refactor OrganizationUsersControllerPerformanceTests to filter organization users by OrganizationId.

* Refactor CreateOrganizationUser method to improve handling of user status and key assignment based on invitation and confirmation states.

* Add XML documentation for CreateOrganizationUser method to clarify user status handling
2025-12-05 14:22:00 +00:00

164 lines
7.1 KiB
C#

using System.Net;
using System.Text;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.Billing.Enums;
using Bit.Core.Tokens;
using Bit.Seeder.Recipes;
using Xunit;
using Xunit.Abstractions;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationsControllerPerformanceTests(ITestOutputHelper testOutputHelper)
{
/// <summary>
/// Tests DELETE /organizations/{id} with password verification
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10, 5, 3)]
//[InlineData(100, 20, 10)]
//[InlineData(1000, 50, 25)]
public async Task DeleteOrganization_WithPasswordVerification(int userCount, int collectionCount, int groupCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var deleteRequest = new SecretVerificationRequestModel
{
MasterPasswordHash = "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="
};
var request = new HttpRequestMessage(HttpMethod.Delete, $"/organizations/{orgId}")
{
Content = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, "application/json")
};
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.SendAsync(request);
stopwatch.Stop();
testOutputHelper.WriteLine($"DELETE /organizations/{{id}} - Users: {userCount}; Collections: {collectionCount}; Groups: {groupCount}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests POST /organizations/{id}/delete-recover-token with token verification
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10, 5, 3)]
//[InlineData(100, 20, 10)]
//[InlineData(1000, 50, 25)]
public async Task DeleteOrganization_WithTokenVerification(int userCount, int collectionCount, int groupCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
groupsSeeder.AddToOrganization(orgId, groupCount, orgUserIds, 0);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var organization = db.Organizations.FirstOrDefault(o => o.Id == orgId);
Assert.NotNull(organization);
var tokenFactory = factory.GetService<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
var tokenable = new OrgDeleteTokenable(organization, 24);
var token = tokenFactory.Protect(tokenable);
var deleteRequest = new OrganizationVerifyDeleteRecoverRequestModel
{
Token = token
};
var requestContent = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync($"/organizations/{orgId}/delete-recover-token", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /organizations/{{id}}/delete-recover-token - Users: {userCount}; Collections: {collectionCount}; Groups: {groupCount}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests POST /organizations/create-without-payment
/// </summary>
[Fact(Skip = "Performance test")]
public async Task CreateOrganization_WithoutPayment()
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var email = $"user@{OrganizationTestHelpers.GenerateRandomDomain()}";
var masterPasswordHash = "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=";
await factory.LoginWithNewAccount(email, masterPasswordHash);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, email, masterPasswordHash);
var createRequest = new OrganizationNoPaymentCreateRequest
{
Name = "Test Organization",
BusinessName = "Test Business Name",
BillingEmail = email,
PlanType = PlanType.EnterpriseAnnually,
Key = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk=",
AdditionalSeats = 1,
AdditionalStorageGb = 1,
UseSecretsManager = true,
AdditionalSmSeats = 1,
AdditionalServiceAccounts = 2,
MaxAutoscaleSeats = 100,
PremiumAccessAddon = false,
CollectionName = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="
};
var requestContent = new StringContent(JsonSerializer.Serialize(createRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync("/organizations/create-without-payment", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /organizations/create-without-payment - AdditionalSeats: {createRequest.AdditionalSeats}; AdditionalStorageGb: {createRequest.AdditionalStorageGb}; AdditionalSmSeats: {createRequest.AdditionalSmSeats}; AdditionalServiceAccounts: {createRequest.AdditionalServiceAccounts}; MaxAutoscaleSeats: {createRequest.MaxAutoscaleSeats}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}