1
0
mirror of https://github.com/bitwarden/server synced 2025-12-26 05:03:18 +00:00

Merge remote-tracking branch 'origin/main' into xunit-v3-full-upgrade

This commit is contained in:
Justin Baur
2025-12-12 16:00:18 -05:00
523 changed files with 34986 additions and 7245 deletions

View File

@@ -0,0 +1,63 @@
using System.Net;
using System.Text;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Seeder.Recipes;
using Xunit;
using Xunit.Abstractions;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class GroupsControllerPerformanceTests(ITestOutputHelper testOutputHelper)
{
/// <summary>
/// Tests PUT /organizations/{orgId}/groups/{id}
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10, 5)]
//[InlineData(100, 10)]
//[InlineData(1000, 20)]
public async Task UpdateGroup_WithUsersAndCollections(int userCount, int collectionCount)
{
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();
var collectionIds = collectionsSeeder.AddToOrganization(orgId, collectionCount, orgUserIds, 0);
var groupIds = groupsSeeder.AddToOrganization(orgId, 1, orgUserIds, 0);
var groupId = groupIds.First();
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var updateRequest = new GroupRequestModel
{
Name = "Updated Group Name",
Collections = collectionIds.Select(c => new SelectionReadOnlyRequestModel { Id = c, ReadOnly = false, HidePasswords = false, Manage = false }),
Users = orgUserIds
};
var requestContent = new StringContent(JsonSerializer.Serialize(updateRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PutAsync($"/organizations/{orgId}/groups/{groupId}", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"PUT /organizations/{{orgId}}/groups/{{id}} - Users: {orgUserIds.Count}; Collections: {collectionIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
}

View File

@@ -48,7 +48,7 @@ public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiAppli
[Fact]
public async Task AutoConfirm_WhenUserCannotManageOtherUsers_ThenShouldReturnForbidden()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
@@ -88,7 +88,7 @@ public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiAppli
[Fact]
public async Task AutoConfirm_WhenOwnerConfirmsValidUser_ThenShouldReturnNoContent()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;
@@ -152,7 +152,7 @@ public class OrganizationUserControllerAutoConfirmTests : IClassFixture<ApiAppli
[Fact]
public async Task AutoConfirm_WhenUserIsConfirmedMultipleTimes_ThenShouldSuccessAndOnlyConfirmOneUser()
{
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
var (organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
organization.UseAutomaticUserConfirmation = true;

View File

@@ -0,0 +1,347 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUserControllerBulkRevokeTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
public OrganizationUserControllerBulkRevokeTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.BulkRevokeUsersV2)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-user-bulk-revoke-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseMonthly,
ownerEmail: _ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task BulkRevoke_Success()
{
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(ownerEmail);
var (_, orgUser1) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var (_, orgUser2) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUser1.Id, orgUser2.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Equal(2, content.Data.Count());
Assert.All(content.Data, r => Assert.Empty(r.Error));
var actualUsers = await organizationUserRepository.GetManyAsync([orgUser1.Id, orgUser2.Id]);
Assert.All(actualUsers, u => Assert.Equal(OrganizationUserStatusType.Revoked, u.Status));
}
[Fact]
public async Task BulkRevoke_AsAdmin_Success()
{
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
await _loginHelper.LoginAsync(adminEmail);
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Single(content.Data);
Assert.All(content.Data, r => Assert.Empty(r.Error));
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
Assert.NotNull(actualUser);
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
}
[Fact]
public async Task BulkRevoke_CannotRevokeSelf_ReturnsError()
{
var (userEmail, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Single(content.Data);
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "You cannot revoke yourself.");
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(orgUser.Id);
Assert.NotNull(actualUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
}
[Fact]
public async Task BulkRevoke_AlreadyRevoked_ReturnsError()
{
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(ownerEmail);
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
await organizationUserRepository.RevokeAsync(orgUser.Id);
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Single(content.Data);
Assert.Contains(content.Data, r => r.Id == orgUser.Id && r.Error == "Already revoked.");
var actualUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(actualUser);
Assert.Equal(OrganizationUserStatusType.Revoked, actualUser.Status);
}
[Fact]
public async Task BulkRevoke_AdminCannotRevokeOwner_ReturnsError()
{
var (adminEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Admin);
await _loginHelper.LoginAsync(adminEmail);
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.Owner);
var request = new OrganizationUserBulkRequestModel
{
Ids = [ownerOrgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Single(content.Data);
Assert.Contains(content.Data, r => r.Id == ownerOrgUser.Id && r.Error == "Only owners can revoke other owners.");
var actualUser = await _factory.GetService<IOrganizationUserRepository>().GetByIdAsync(ownerOrgUser.Id);
Assert.NotNull(actualUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUser.Status);
}
[Fact]
public async Task BulkRevoke_MixedResults()
{
var (ownerEmail, requestingOwner) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(ownerEmail);
var (_, validOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var (_, alreadyRevokedOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var organizationUserRepository = _factory.GetService<IOrganizationUserRepository>();
await organizationUserRepository.RevokeAsync(alreadyRevokedOrgUser.Id);
var request = new OrganizationUserBulkRequestModel
{
Ids = [validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
var content = await httpResponse.Content.ReadFromJsonAsync<ListResponseModel<OrganizationUserBulkResponseModel>>();
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
Assert.NotNull(content);
Assert.Equal(3, content.Data.Count());
Assert.Contains(content.Data, r => r.Id == validOrgUser.Id && r.Error == string.Empty);
Assert.Contains(content.Data, r => r.Id == alreadyRevokedOrgUser.Id && r.Error == "Already revoked.");
Assert.Contains(content.Data, r => r.Id == requestingOwner.Id && r.Error == "You cannot revoke yourself.");
var actualUsers = await organizationUserRepository.GetManyAsync([validOrgUser.Id, alreadyRevokedOrgUser.Id, requestingOwner.Id]);
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == validOrgUser.Id).Status);
Assert.Equal(OrganizationUserStatusType.Revoked, actualUsers.First(u => u.Id == alreadyRevokedOrgUser.Id).Status);
Assert.Equal(OrganizationUserStatusType.Confirmed, actualUsers.First(u => u.Id == requestingOwner.Id).Status);
}
[Theory]
[InlineData(OrganizationUserType.User)]
[InlineData(OrganizationUserType.Custom)]
public async Task BulkRevoke_WithoutManageUsersPermission_ReturnsForbidden(OrganizationUserType organizationUserType)
{
var (userEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, organizationUserType, new Permissions { ManageUsers = false });
await _loginHelper.LoginAsync(userEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = [Guid.NewGuid()]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Fact]
public async Task BulkRevoke_WithEmptyIds_ReturnsBadRequest()
{
await _loginHelper.LoginAsync(_ownerEmail);
var request = new OrganizationUserBulkRequestModel
{
Ids = []
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
Assert.Equal(HttpStatusCode.BadRequest, httpResponse.StatusCode);
}
[Fact]
public async Task BulkRevoke_WithInvalidOrganizationId_ReturnsForbidden()
{
var (ownerEmail, _) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
await _loginHelper.LoginAsync(ownerEmail);
var (_, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id, OrganizationUserType.User);
var invalidOrgId = Guid.NewGuid();
var request = new OrganizationUserBulkRequestModel
{
Ids = [orgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{invalidOrgId}/users/revoke", request);
Assert.Equal(HttpStatusCode.Forbidden, httpResponse.StatusCode);
}
[Fact]
public async Task BulkRevoke_ProviderRevokesOwner_ReturnsOk()
{
var providerEmail = $"provider-user{Guid.NewGuid()}@example.com";
// create user for provider
await _factory.LoginWithNewAccount(providerEmail);
// create provider and provider user
await _factory.GetService<ICreateProviderCommand>()
.CreateBusinessUnitAsync(
new Provider
{
Name = "provider",
Type = ProviderType.BusinessUnit
},
providerEmail,
PlanType.EnterpriseAnnually2023,
10);
await _loginHelper.LoginAsync(providerEmail);
var providerUserUser = await _factory.GetService<IUserRepository>().GetByEmailAsync(providerEmail);
var providerUserCollection = await _factory.GetService<IProviderUserRepository>()
.GetManyByUserAsync(providerUserUser!.Id);
var providerUser = providerUserCollection.First();
await _factory.GetService<IProviderOrganizationRepository>().CreateAsync(new ProviderOrganization
{
ProviderId = providerUser.ProviderId,
OrganizationId = _organization.Id,
Key = null,
Settings = null
});
var (_, ownerOrgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory,
_organization.Id, OrganizationUserType.Owner);
var request = new OrganizationUserBulkRequestModel
{
Ids = [ownerOrgUser.Id]
};
var httpResponse = await _client.PutAsJsonAsync($"organizations/{_organization.Id}/users/revoke", request);
Assert.Equal(HttpStatusCode.OK, httpResponse.StatusCode);
}
}

View File

@@ -218,7 +218,7 @@ public class OrganizationUserControllerTests : IClassFixture<ApiApplicationFacto
_ownerEmail = $"org-user-integration-test-{Guid.NewGuid()}@bitwarden.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
}

View File

@@ -1,38 +1,592 @@
using System.Net;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Seeder.Recipes;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper)
public class OrganizationUsersControllerPerformanceTests(ITestOutputHelper testOutputHelper)
{
/// <summary>
/// Tests GET /organizations/{orgId}/users?includeCollections=true
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(100)]
[InlineData(60000)]
public async Task GetAsync(int seats)
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task GetAllUsers_WithCollections(int seats)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var seeder = new OrganizationWithUsersRecipe(db);
var orgSeeder = new OrganizationWithUsersRecipe(db);
var collectionsSeeder = new CollectionsRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
var orgId = seeder.Seed("Org", seats, "large.test");
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs=");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: seats);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadAsStringAsync();
Assert.NotEmpty(result);
stopwatch.Stop();
testOutputHelper.WriteLine($"GET /users - Seats: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms");
}
/// <summary>
/// Tests GET /organizations/{orgId}/users/mini-details
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task GetAllUsers_MiniDetails(int seats)
{
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: seats);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
collectionsSeeder.AddToOrganization(orgId, 10, orgUserIds);
groupsSeeder.AddToOrganization(orgId, 5, orgUserIds);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync($"/organizations/{orgId}/users/mini-details");
stopwatch.Stop();
testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms");
testOutputHelper.WriteLine($"GET /users/mini-details - Seats: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests GET /organizations/{orgId}/users/{id}?includeGroups=true
/// </summary>
[Fact(Skip = "Performance test")]
public async Task GetSingleUser_WithGroups()
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var groupsSeeder = new GroupsRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
groupsSeeder.AddToOrganization(orgId, 2, [orgUserId]);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync($"/organizations/{orgId}/users/{orgUserId}?includeGroups=true");
stopwatch.Stop();
testOutputHelper.WriteLine($"GET /users/{{id}} - Request duration: {stopwatch.ElapsedMilliseconds} ms");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests GET /organizations/{orgId}/users/{id}/reset-password-details
/// </summary>
[Fact(Skip = "Performance test")]
public async Task GetResetPasswordDetails_ForSingleUser()
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
var orgUserId = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).FirstOrDefault();
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.GetAsync($"/organizations/{orgId}/users/{orgUserId}/reset-password-details");
stopwatch.Stop();
testOutputHelper.WriteLine($"GET /users/{{id}}/reset-password-details - Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests POST /organizations/{orgId}/users/confirm
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkConfirmUsers(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: userCount,
usersStatus: OrganizationUserStatusType.Accepted);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var acceptedUserIds = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Status == OrganizationUserStatusType.Accepted)
.Select(ou => ou.Id)
.ToList();
var confirmRequest = new OrganizationUserBulkConfirmRequestModel
{
Keys = acceptedUserIds.Select(id => new OrganizationUserBulkConfirmRequestModelEntry { Id = id, Key = "test-key-" + id }),
DefaultUserCollectionName = "2.AOs41Hd8OQiCPXjyJKCiDA==|O6OHgt2U2hJGBSNGnimJmg==|iD33s8B69C8JhYYhSa4V1tArjvLr8eEaGqOV7BRo5Jk="
};
var requestContent = new StringContent(JsonSerializer.Serialize(confirmRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync($"/organizations/{orgId}/users/confirm", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /users/confirm - Users: {acceptedUserIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests POST /organizations/{orgId}/users/remove
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkRemoveUsers(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToRemove = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)
.Select(ou => ou.Id)
.ToList();
var removeRequest = new OrganizationUserBulkRequestModel { Ids = usersToRemove };
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var requestContent = new StringContent(JsonSerializer.Serialize(removeRequest), Encoding.UTF8, "application/json");
var response = await client.PostAsync($"/organizations/{orgId}/users/remove", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /users/remove - Users: {usersToRemove.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests PUT /organizations/{orgId}/users/revoke
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkRevokeUsers(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: userCount,
usersStatus: OrganizationUserStatusType.Confirmed);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToRevoke = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)
.Select(ou => ou.Id)
.ToList();
var revokeRequest = new OrganizationUserBulkRequestModel { Ids = usersToRevoke };
var requestContent = new StringContent(JsonSerializer.Serialize(revokeRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PutAsync($"/organizations/{orgId}/users/revoke", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"PUT /users/revoke - Users: {usersToRevoke.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests PUT /organizations/{orgId}/users/restore
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkRestoreUsers(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: userCount,
usersStatus: OrganizationUserStatusType.Revoked);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToRestore = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)
.Select(ou => ou.Id)
.ToList();
var restoreRequest = new OrganizationUserBulkRequestModel { Ids = usersToRestore };
var requestContent = new StringContent(JsonSerializer.Serialize(restoreRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PutAsync($"/organizations/{orgId}/users/restore", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"PUT /users/restore - Users: {usersToRestore.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests POST /organizations/{orgId}/users/delete-account
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkDeleteAccounts(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domainSeeder = new OrganizationDomainRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: userCount,
usersStatus: OrganizationUserStatusType.Confirmed);
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToDelete = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)
.Select(ou => ou.Id)
.ToList();
var deleteRequest = new OrganizationUserBulkRequestModel { Ids = usersToDelete };
var requestContent = new StringContent(JsonSerializer.Serialize(deleteRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync($"/organizations/{orgId}/users/delete-account", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /users/delete-account - Users: {usersToDelete.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests PUT /organizations/{orgId}/users/{id}
/// </summary>
[Fact(Skip = "Performance test")]
public async Task UpdateSingleUser_WithCollectionsAndGroups()
{
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: 1);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 3, orgUserIds, 0);
var groupIds = groupsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var userToUpdate = db.OrganizationUsers
.FirstOrDefault(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User);
var updateRequest = new OrganizationUserUpdateRequestModel
{
Type = OrganizationUserType.Custom,
Collections = collectionIds.Select(c => new SelectionReadOnlyRequestModel { Id = c, ReadOnly = false, HidePasswords = false, Manage = false }),
Groups = groupIds,
AccessSecretsManager = false,
Permissions = new Permissions { AccessEventLogs = true }
};
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PutAsync($"/organizations/{orgId}/users/{userToUpdate.Id}",
new StringContent(JsonSerializer.Serialize(updateRequest), Encoding.UTF8, "application/json"));
stopwatch.Stop();
testOutputHelper.WriteLine($"PUT /users/{{id}} - Collections: {collectionIds.Count}; Groups: {groupIds.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests PUT /organizations/{orgId}/users/enable-secrets-manager
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkEnableSecretsManager(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: userCount);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToEnable = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User)
.Select(ou => ou.Id)
.ToList();
var enableRequest = new OrganizationUserBulkRequestModel { Ids = usersToEnable };
var requestContent = new StringContent(JsonSerializer.Serialize(enableRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PutAsync($"/organizations/{orgId}/users/enable-secrets-manager", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"PUT /users/enable-secrets-manager - Users: {usersToEnable.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
/// <summary>
/// Tests DELETE /organizations/{orgId}/users/{id}/delete-account
/// </summary>
[Fact(Skip = "Performance test")]
public async Task DeleteSingleUserAccount_FromVerifiedDomain()
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domainSeeder = new OrganizationDomainRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: 2,
usersStatus: OrganizationUserStatusType.Confirmed);
domainSeeder.AddVerifiedDomainToOrganization(orgId, domain);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var userToDelete = db.OrganizationUsers
.FirstOrDefault(ou => ou.OrganizationId == orgId && ou.Type == OrganizationUserType.User);
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.DeleteAsync($"/organizations/{orgId}/users/{userToDelete.Id}/delete-account");
stopwatch.Stop();
testOutputHelper.WriteLine($"DELETE /users/{{id}}/delete-account - Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests POST /organizations/{orgId}/users/invite
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(1)]
//[InlineData(5)]
//[InlineData(20)]
public async Task InviteUsers(int emailCount)
{
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 domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(name: "Org", domain: domain, users: 1);
var orgUserIds = db.OrganizationUsers.Where(ou => ou.OrganizationId == orgId).Select(ou => ou.Id).ToList();
var collectionIds = collectionsSeeder.AddToOrganization(orgId, 2, orgUserIds, 0);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var emails = Enumerable.Range(0, emailCount).Select(i => $"{i:D4}@{domain}").ToArray();
var inviteRequest = new OrganizationUserInviteRequestModel
{
Emails = emails,
Type = OrganizationUserType.User,
AccessSecretsManager = false,
Collections = Array.Empty<SelectionReadOnlyRequestModel>(),
Groups = Array.Empty<Guid>(),
Permissions = null
};
var requestContent = new StringContent(JsonSerializer.Serialize(inviteRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync($"/organizations/{orgId}/users/invite", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /users/invite - Emails: {emails.Length}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
/// <summary>
/// Tests POST /organizations/{orgId}/users/reinvite
/// </summary>
[Theory(Skip = "Performance test")]
[InlineData(10)]
//[InlineData(100)]
//[InlineData(1000)]
public async Task BulkReinviteUsers(int userCount)
{
await using var factory = new SqlServerApiApplicationFactory();
var client = factory.CreateClient();
var db = factory.GetDatabaseContext();
var orgSeeder = new OrganizationWithUsersRecipe(db);
var domain = OrganizationTestHelpers.GenerateRandomDomain();
var orgId = orgSeeder.Seed(
name: "Org",
domain: domain,
users: userCount,
usersStatus: OrganizationUserStatusType.Invited);
await PerformanceTestHelpers.AuthenticateClientAsync(factory, client, $"owner@{domain}");
var usersToReinvite = db.OrganizationUsers
.Where(ou => ou.OrganizationId == orgId && ou.Status == OrganizationUserStatusType.Invited)
.Select(ou => ou.Id)
.ToList();
var reinviteRequest = new OrganizationUserBulkRequestModel { Ids = usersToReinvite };
var requestContent = new StringContent(JsonSerializer.Serialize(reinviteRequest), Encoding.UTF8, "application/json");
var stopwatch = System.Diagnostics.Stopwatch.StartNew();
var response = await client.PostAsync($"/organizations/{orgId}/users/reinvite", requestContent);
stopwatch.Stop();
testOutputHelper.WriteLine($"POST /users/reinvite - Users: {usersToReinvite.Count}; Request duration: {stopwatch.ElapsedMilliseconds} ms; Status: {response.StatusCode}");
Assert.True(response.IsSuccessStatusCode);
}
}

View File

@@ -3,7 +3,6 @@ using Bit.Api.AdminConsole.Authorization;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Api.Models.Request.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums;
@@ -14,8 +13,6 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
using NSubstitute;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
@@ -32,12 +29,6 @@ public class OrganizationUsersControllerPutResetPasswordTests : IClassFixture<Ap
public OrganizationUsersControllerPutResetPasswordTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_factory.SubstituteService<IFeatureService>(featureService =>
{
featureService
.IsEnabled(FeatureFlagKeys.AccountRecoveryCommand)
.Returns(true);
});
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
@@ -47,7 +38,7 @@ public class OrganizationUsersControllerPutResetPasswordTests : IClassFixture<Ap
_ownerEmail = $"reset-password-test-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail, passwordManagerSeats: 5, paymentMethod: PaymentMethodType.Card);
// Enable reset password and policies for the organization

View File

@@ -0,0 +1,163 @@
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);
}
}

View File

@@ -0,0 +1,196 @@
using System.Net;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.IntegrationTest.Factories;
using Bit.Api.IntegrationTest.Helpers;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Enums;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Api.IntegrationTest.AdminConsole.Controllers;
public class OrganizationsControllerTests : IClassFixture<ApiApplicationFactory>, IAsyncLifetime
{
private readonly HttpClient _client;
private readonly ApiApplicationFactory _factory;
private readonly LoginHelper _loginHelper;
private Organization _organization = null!;
private string _ownerEmail = null!;
private readonly string _billingEmail = "billing@example.com";
private readonly string _organizationName = "Organizations Controller Test Org";
public OrganizationsControllerTests(ApiApplicationFactory apiFactory)
{
_factory = apiFactory;
_client = _factory.CreateClient();
_loginHelper = new LoginHelper(_factory, _client);
}
public async Task InitializeAsync()
{
_ownerEmail = $"org-integration-test-{Guid.NewGuid()}@example.com";
await _factory.LoginWithNewAccount(_ownerEmail);
(_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory,
name: _organizationName,
billingEmail: _billingEmail,
plan: PlanType.EnterpriseAnnually,
ownerEmail: _ownerEmail,
passwordManagerSeats: 5,
paymentMethod: PaymentMethodType.Card);
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
[Fact]
public async Task Put_AsOwner_WithoutProvider_CanUpdateOrganization()
{
// Arrange - Regular organization owner (no provider)
await _loginHelper.LoginAsync(_ownerEmail);
var updateRequest = new OrganizationUpdateRequestModel
{
Name = "Updated Organization Name",
BillingEmail = "newbillingemail@example.com"
};
// Act
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Verify the organization name was updated
var organizationRepository = _factory.GetService<IOrganizationRepository>();
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
Assert.NotNull(updatedOrg);
Assert.Equal("Updated Organization Name", updatedOrg.Name);
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
}
[Fact]
public async Task Put_AsProvider_CanUpdateOrganization()
{
// Create and login as a new account to be the provider user (not the owner)
var providerUserEmail = $"provider-{Guid.NewGuid()}@example.com";
var (token, _) = await _factory.LoginWithNewAccount(providerUserEmail);
// Set up provider linked to org and ProviderUser entry
var provider = await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id,
ProviderType.Msp);
await ProviderTestHelpers.CreateProviderUserAsync(_factory, provider.Id, providerUserEmail,
ProviderUserType.ProviderAdmin);
await _loginHelper.LoginAsync(providerUserEmail);
var updateRequest = new OrganizationUpdateRequestModel
{
Name = "Updated Organization Name",
BillingEmail = "newbillingemail@example.com"
};
// Act
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Verify the organization name was updated
var organizationRepository = _factory.GetService<IOrganizationRepository>();
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
Assert.NotNull(updatedOrg);
Assert.Equal("Updated Organization Name", updatedOrg.Name);
Assert.Equal("newbillingemail@example.com", updatedOrg.BillingEmail);
}
[Fact]
public async Task Put_NotMemberOrProvider_CannotUpdateOrganization()
{
// Create and login as a new account to be unrelated to the org
var userEmail = "stranger@example.com";
await _factory.LoginWithNewAccount(userEmail);
await _loginHelper.LoginAsync(userEmail);
var updateRequest = new OrganizationUpdateRequestModel
{
Name = "Updated Organization Name",
BillingEmail = "newbillingemail@example.com"
};
// Act
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
// Verify the organization name was not updated
var organizationRepository = _factory.GetService<IOrganizationRepository>();
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
Assert.NotNull(updatedOrg);
Assert.Equal(_organizationName, updatedOrg.Name);
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
}
[Fact]
public async Task Put_AsOwner_WithProvider_CanRenameOrganization()
{
// Arrange - Create provider and link to organization
// The active user is ONLY an org owner, NOT a provider user
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
await _loginHelper.LoginAsync(_ownerEmail);
var updateRequest = new OrganizationUpdateRequestModel
{
Name = "Updated Organization Name",
BillingEmail = null
};
// Act
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
// Verify the organization name was actually updated
var organizationRepository = _factory.GetService<IOrganizationRepository>();
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
Assert.NotNull(updatedOrg);
Assert.Equal("Updated Organization Name", updatedOrg.Name);
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
}
[Fact]
public async Task Put_AsOwner_WithProvider_CannotChangeBillingEmail()
{
// Arrange - Create provider and link to organization
// The active user is ONLY an org owner, NOT a provider user
await ProviderTestHelpers.CreateProviderAndLinkToOrganizationAsync(_factory, _organization.Id, ProviderType.Msp);
await _loginHelper.LoginAsync(_ownerEmail);
var updateRequest = new OrganizationUpdateRequestModel
{
Name = "Updated Organization Name",
BillingEmail = "updatedbilling@example.com"
};
// Act
var response = await _client.PutAsJsonAsync($"/organizations/{_organization.Id}", updateRequest);
// Assert
Assert.Equal(HttpStatusCode.Unauthorized, response.StatusCode);
// Verify the organization was not updated
var organizationRepository = _factory.GetService<IOrganizationRepository>();
var updatedOrg = await organizationRepository.GetByIdAsync(_organization.Id);
Assert.NotNull(updatedOrg);
Assert.Equal(_organizationName, updatedOrg.Name);
Assert.Equal(_billingEmail, updatedOrg.BillingEmail);
}
}