Vijay Oommen 599fbc0efd
[PM-28616] Add flag UsePhishingBlocker to dbo.Organization (#6625)
* PM-28616 Add flag UsePhishingBlocker to dbo.Organization

* PM-28616 updated as per comments from claude

* PM-28616 updated ToLicense Method to copy the license file

* PM-28616 allow phishing blocker to be imported via license files for self-hosted

* PM-28616 updated PR comments - added more views to be refreshed

* PM-28616 removed proeprty from constructor as it is not used anymore. We have moved to claims based properties
2025-12-01 13:31:36 -05:00

1600 lines
63 KiB
C#

using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
public class OrganizationUserRepositoryTests
{
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_Works(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user.Email
});
await organizationUserRepository.DeleteAsync(orgUser);
var newUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(newUser);
Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_Migrates_UserDefaultCollection(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user1.Email
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user2.Email
});
var defaultUserCollection1 = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection 1",
Id = user1.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
var defaultUserCollection2 = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection 2",
Id = user2.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
// Create the CollectionUser entry for the defaultUserCollection
await collectionRepository.UpdateUsersAsync(defaultUserCollection1.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser1.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection2.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser2.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteManyAsync(new List<Guid> { orgUser1.Id, orgUser2.Id });
var newUser = await userRepository.GetByIdAsync(user1.Id);
Assert.NotNull(newUser);
Assert.NotEqual(newUser.AccountRevisionDate, user1.AccountRevisionDate);
var updatedCollection1 = await collectionRepository.GetByIdAsync(defaultUserCollection1.Id);
Assert.NotNull(updatedCollection1);
Assert.Equal(CollectionType.SharedCollection, updatedCollection1.Type);
Assert.Equal(user1.Email, updatedCollection1.DefaultUserCollectionEmail);
var updatedCollection2 = await collectionRepository.GetByIdAsync(defaultUserCollection2.Id);
Assert.NotNull(updatedCollection2);
Assert.Equal(CollectionType.SharedCollection, updatedCollection2.Type);
Assert.Equal(user2.Email, updatedCollection2.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_Migrates_UserDefaultCollection(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user.Email
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
// Create the CollectionUser entry for the defaultUserCollection
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var newUser = await userRepository.GetByIdAsync(user.Id);
Assert.NotNull(newUser);
Assert.NotEqual(newUser.AccountRevisionDate, user.AccountRevisionDate);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_Works(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user1.Email
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = user2.Email
});
await organizationUserRepository.DeleteManyAsync(new List<Guid>
{
orgUser1.Id,
orgUser2.Id,
});
var updatedUser1 = await userRepository.GetByIdAsync(user1.Id);
Assert.NotNull(updatedUser1);
var updatedUser2 = await userRepository.GetByIdAsync(user2.Id);
Assert.NotNull(updatedUser2);
Assert.NotEqual(updatedUser1.AccountRevisionDate, user1.AccountRevisionDate);
Assert.NotEqual(updatedUser2.AccountRevisionDate, user2.AccountRevisionDate);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyAccountRecoveryDetailsByOrganizationUserAsync_Works(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.Argon2id,
KdfIterations = 4,
KdfMemory = 5,
KdfParallelism = 6
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey2",
AccessSecretsManager = true
});
var recoveryDetails = await organizationUserRepository.GetManyAccountRecoveryDetailsByOrganizationUserAsync(
organization.Id,
new[]
{
orgUser1.Id,
orgUser2.Id,
});
Assert.NotNull(recoveryDetails);
Assert.Equal(2, recoveryDetails.Count());
Assert.Contains(recoveryDetails, r =>
r.OrganizationUserId == orgUser1.Id &&
r.Kdf == KdfType.PBKDF2_SHA256 &&
r.KdfIterations == 1 &&
r.KdfMemory == 2 &&
r.KdfParallelism == 3 &&
r.ResetPasswordKey == "resetpasswordkey1" &&
r.EncryptedPrivateKey == "privatekey");
Assert.Contains(recoveryDetails, r =>
r.OrganizationUserId == orgUser2.Id &&
r.Kdf == KdfType.Argon2id &&
r.KdfIterations == 4 &&
r.KdfMemory == 5 &&
r.KdfParallelism == 6 &&
r.ResetPasswordKey == "resetpasswordkey2" &&
r.EncryptedPrivateKey == "privatekey");
}
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByOrganizationAsync_WithIncludeCollections_ExcludesDefaultCollections(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email,
Plan = "Test",
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
});
// Create a regular collection
var regularCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = organization.Id,
Name = "Regular Collection",
Type = CollectionType.SharedCollection
});
// Create a default user collection
var defaultCollection = await collectionRepository.CreateAsync(new Collection
{
OrganizationId = organization.Id,
Name = "Default Collection",
Type = CollectionType.DefaultUserCollection,
DefaultUserCollectionEmail = user.Email
});
// Assign the organization user to both collections
await organizationUserRepository.ReplaceAsync(orgUser, new List<CollectionAccessSelection>
{
new CollectionAccessSelection
{
Id = regularCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
},
new CollectionAccessSelection
{
Id = defaultCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
}
});
// Get organization users with collections included
var organizationUsers = await organizationUserRepository.GetManyDetailsByOrganizationAsync(
organization.Id, includeGroups: false, includeCollections: true);
Assert.NotNull(organizationUsers);
Assert.Single(organizationUsers);
var orgUserWithCollections = organizationUsers.First();
Assert.NotNull(orgUserWithCollections.Collections);
// Should only include the regular collection, not the default collection
Assert.Single(orgUserWithCollections.Collections);
Assert.Equal(regularCollection.Id, orgUserWithCollections.Collections.First().Id);
Assert.DoesNotContain(orgUserWithCollections.Collections, c => c.Id == defaultCollection.Id);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserAsync_Works(IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ISsoConfigRepository ssoConfigRepository)
{
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization = await organizationRepository.CreateTestOrganizationAsync();
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
var ssoConfigData = new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption
};
var ssoConfig = await ssoConfigRepository.CreateAsync(new SsoConfig
{
OrganizationId = organization.Id,
Enabled = true,
Data = ssoConfigData.Serialize()
});
var responseModel = await organizationUserRepository.GetManyDetailsByUserAsync(user1.Id);
Assert.NotNull(responseModel);
Assert.Single(responseModel);
var result = responseModel.Single();
Assert.Equal(organization.Id, result.OrganizationId);
Assert.Equal(user1.Id, result.UserId);
Assert.Equal(orgUser1.Id, result.OrganizationUserId);
Assert.Equal(organization.Name, result.Name);
Assert.Equal(organization.UsePolicies, result.UsePolicies);
Assert.Equal(organization.UseSso, result.UseSso);
Assert.Equal(organization.UseKeyConnector, result.UseKeyConnector);
Assert.Equal(ssoConfig.Enabled, result.SsoEnabled);
Assert.Equal(ssoConfig.Data, result.SsoConfig);
Assert.Equal(organization.UseScim, result.UseScim);
Assert.Equal(organization.UseGroups, result.UseGroups);
Assert.Equal(organization.UseDirectory, result.UseDirectory);
Assert.Equal(organization.UseEvents, result.UseEvents);
Assert.Equal(organization.UseTotp, result.UseTotp);
Assert.Equal(organization.Use2fa, result.Use2fa);
Assert.Equal(organization.UseApi, result.UseApi);
Assert.Equal(organization.UseResetPassword, result.UseResetPassword);
Assert.Equal(organization.UseSecretsManager, result.UseSecretsManager);
Assert.Equal(organization.UsePasswordManager, result.UsePasswordManager);
Assert.Equal(organization.UsersGetPremium, result.UsersGetPremium);
Assert.Equal(organization.UseCustomPermissions, result.UseCustomPermissions);
Assert.Equal(organization.SelfHost, result.SelfHost);
Assert.Equal(organization.Seats, result.Seats);
Assert.Equal(organization.MaxCollections, result.MaxCollections);
Assert.Equal(organization.MaxStorageGb, result.MaxStorageGb);
Assert.Equal(organization.Identifier, result.Identifier);
Assert.Equal(orgUser1.Key, result.Key);
Assert.Equal(orgUser1.ResetPasswordKey, result.ResetPasswordKey);
Assert.Equal(organization.PublicKey, result.PublicKey);
Assert.Equal(organization.PrivateKey, result.PrivateKey);
Assert.Equal(orgUser1.Status, result.Status);
Assert.Equal(orgUser1.Type, result.Type);
Assert.Equal(organization.Enabled, result.Enabled);
Assert.Equal(organization.PlanType, result.PlanType);
Assert.Equal(orgUser1.Permissions, result.Permissions);
Assert.Equal(organization.SmSeats, result.SmSeats);
Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts);
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
Assert.Equal(organization.LimitItemDeletion, result.LimitItemDeletion);
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
Assert.Equal(organization.UseRiskInsights, result.UseRiskInsights);
Assert.Equal(organization.UseOrganizationDomains, result.UseOrganizationDomains);
Assert.Equal(organization.UseAdminSponsoredFamilies, result.UseAdminSponsoredFamilies);
Assert.Equal(organization.UseAutomaticUserConfirmation, result.UseAutomaticUserConfirmation);
}
[Theory, DatabaseData]
public async Task GetManyDetailsByUserAsync_ShouldPopulateSsoPropertiesCorrectly(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ISsoConfigRepository ssoConfigRepository)
{
var user = await userRepository.CreateTestUserAsync();
var organizationWithSso = await organizationRepository.CreateTestOrganizationAsync();
var organizationWithoutSso = await organizationRepository.CreateTestOrganizationAsync();
var orgUserWithSso = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organizationWithSso.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
Email = user.Email
});
var orgUserWithoutSso = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organizationWithoutSso.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Email = user.Email
});
// Create SSO configuration for first organization only
var serializedSsoConfigData = new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.KeyConnector,
KeyConnectorUrl = "https://keyconnector.example.com"
}.Serialize();
var ssoConfig = await ssoConfigRepository.CreateAsync(new SsoConfig
{
OrganizationId = organizationWithSso.Id,
Enabled = true,
Data = serializedSsoConfigData
});
var results = (await organizationUserRepository.GetManyDetailsByUserAsync(user.Id)).ToList();
Assert.Equal(2, results.Count);
var orgWithSsoDetails = results.Single(r => r.OrganizationId == organizationWithSso.Id);
var orgWithoutSsoDetails = results.Single(r => r.OrganizationId == organizationWithoutSso.Id);
// Organization with SSO should have SSO properties populated
Assert.True(orgWithSsoDetails.SsoEnabled);
Assert.NotNull(orgWithSsoDetails.SsoConfig);
Assert.Equal(serializedSsoConfigData, orgWithSsoDetails.SsoConfig);
// Organization without SSO should have null SSO properties
Assert.Null(orgWithoutSsoDetails.SsoEnabled);
Assert.Null(orgWithoutSsoDetails.SsoConfig);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithVerifiedDomain_WithOneMatchingEmailDomain_ReturnsSingle(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var user1 = await userRepository.CreateAsync(new User
{
Name = "Test User 1",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user2 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{id}@x-{domainName}", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user3 = await userRepository.CreateAsync(new User
{
Name = "Test User 2",
Email = $"test+{id}@{domainName}.example.com", // Different domain
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
PrivateKey = "privatekey",
UsePolicies = false,
UseSso = false,
UseKeyConnector = false,
UseScim = false,
UseGroups = false,
UseDirectory = false,
UseEvents = false,
UseTotp = false,
Use2fa = false,
UseApi = false,
UseResetPassword = false,
UseSecretsManager = false,
SelfHost = false,
UsersGetPremium = false,
UseCustomPermissions = false,
Enabled = true,
UsePasswordManager = false,
LimitCollectionCreation = false,
LimitCollectionDeletion = false,
LimitItemDeletion = false,
AllowAdminAccessToAllCollectionItems = false,
UseRiskInsights = false,
UseAdminSponsoredFamilies = false,
UsePhishingBlocker = false,
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345",
};
organizationDomain.SetVerifiedDate();
organizationDomain.SetNextRunDate(12);
organizationDomain.SetJobRunCount();
await organizationDomainRepository.CreateAsync(organizationDomain);
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user3.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
Assert.NotNull(responseModel);
Assert.Single(responseModel);
Assert.Equal(orgUser1.Id, responseModel.Single().Id);
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_NoId_Works(IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user1 = await userRepository.CreateTestUserAsync("user1");
var user2 = await userRepository.CreateTestUserAsync("user2");
var user3 = await userRepository.CreateTestUserAsync("user3");
List<User> users = [user1, user2, user3];
var org = await organizationRepository.CreateAsync(new Organization
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUsers = users.Select(u => new OrganizationUser
{
OrganizationId = org.Id,
UserId = u.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner
});
var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers);
var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null);
var readOrgUserIds = readOrgUsers.Select(ou => ou.Id);
Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet());
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_WithId_Works(IOrganizationRepository organizationRepository,
IUserRepository userRepository,
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var user1 = await userRepository.CreateTestUserAsync("user1");
var user2 = await userRepository.CreateTestUserAsync("user2");
var user3 = await userRepository.CreateTestUserAsync("user3");
List<User> users = [user1, user2, user3];
var org = await organizationRepository.CreateAsync(new Organization
{
Name = $"test-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL
});
var orgUsers = users.Select(u => new OrganizationUser
{
Id = CoreHelpers.GenerateComb(), // generate ID ahead of time
OrganizationId = org.Id,
UserId = u.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner
});
var createdOrgUserIds = await organizationUserRepository.CreateManyAsync(orgUsers);
var readOrgUsers = await organizationUserRepository.GetManyByOrganizationAsync(org.Id, null);
var readOrgUserIds = readOrgUsers.Select(ou => ou.Id);
Assert.Equal(createdOrgUserIds.ToHashSet(), readOrgUserIds.ToHashSet());
}
[DatabaseTheory, DatabaseData]
public async Task CreateManyAsync_WithCollectionAndGroup_SaveSuccessfully(
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
ICollectionRepository collectionRepository,
IGroupRepository groupRepository)
{
var requestTime = DateTime.UtcNow;
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = "billing@test.com", // TODO: EF does not enforce this being NOT NULL
Plan = "Test", // TODO: EF does not enforce this being NOT NULL,
CreationDate = requestTime
});
var collection1 = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Collection",
ExternalId = "external-collection-1",
CreationDate = requestTime,
RevisionDate = requestTime
});
var collection2 = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Collection",
ExternalId = "external-collection-1",
CreationDate = requestTime,
RevisionDate = requestTime
});
var collection3 = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Collection",
ExternalId = "external-collection-1",
CreationDate = requestTime,
RevisionDate = requestTime
});
// Create a default user collection that should be excluded from admin results
var defaultCollection = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "My Items",
Type = CollectionType.DefaultUserCollection,
CreationDate = requestTime,
RevisionDate = requestTime
});
var group1 = await groupRepository.CreateAsync(new Group
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Group",
ExternalId = "external-group-1"
});
var group2 = await groupRepository.CreateAsync(new Group
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Group",
ExternalId = "external-group-1"
});
var group3 = await groupRepository.CreateAsync(new Group
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Group",
ExternalId = "external-group-1"
});
var orgUserCollection = new List<CreateOrganizationUser>
{
new()
{
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Email = "test-user@test.com",
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.Owner,
ExternalId = "externalid-1",
Permissions = CoreHelpers.ClassToJsonData(new Permissions()),
AccessSecretsManager = false
},
Collections =
[
new CollectionAccessSelection
{
Id = collection1.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
},
new CollectionAccessSelection
{
Id = defaultCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
}
],
Groups = [group1.Id]
},
new()
{
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Email = "test-user@test.com",
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.Owner,
ExternalId = "externalid-1",
Permissions = CoreHelpers.ClassToJsonData(new Permissions()),
AccessSecretsManager = false
},
Collections =
[
new CollectionAccessSelection
{
Id = collection2.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
}
],
Groups = [group2.Id]
},
new()
{
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Email = "test-user@test.com",
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.Owner,
ExternalId = "externalid-1",
Permissions = CoreHelpers.ClassToJsonData(new Permissions()),
AccessSecretsManager = false
},
Collections =
[
new CollectionAccessSelection
{
Id = collection3.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
}
],
Groups = [group3.Id]
}
};
await organizationUserRepository.CreateManyAsync(orgUserCollection);
var orgUser1 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[0].OrganizationUser.Id);
var group1Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[0].OrganizationUser.Id);
Assert.Equal(orgUserCollection[0].OrganizationUser.Id, orgUser1.OrganizationUser.Id);
// Should only return the regular collection, not the default collection (even though both were assigned)
Assert.Single(orgUser1.Collections);
Assert.Equal(collection1.Id, orgUser1.Collections.First().Id);
Assert.DoesNotContain(orgUser1.Collections, c => c.Id == defaultCollection.Id);
Assert.Equal(group1.Id, group1Database.First());
var orgUser2 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[1].OrganizationUser.Id);
var group2Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[1].OrganizationUser.Id);
Assert.Equal(orgUserCollection[1].OrganizationUser.Id, orgUser2.OrganizationUser.Id);
Assert.Equal(collection2.Id, orgUser2.Collections.First().Id);
Assert.Equal(group2.Id, group2Database.First());
var orgUser3 = await organizationUserRepository.GetDetailsByIdWithCollectionsAsync(orgUserCollection[2].OrganizationUser.Id);
var group3Database = await groupRepository.GetManyIdsByUserIdAsync(orgUserCollection[2].OrganizationUser.Id);
Assert.Equal(orgUserCollection[2].OrganizationUser.Id, orgUser3.OrganizationUser.Id);
Assert.Equal(collection3.Id, orgUser3.Collections.First().Id);
Assert.Equal(group3.Id, group3Database.First());
}
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByOrganizationAsync_vNext_WithoutGroupsAndCollections_ReturnsBasicUserDetails(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository)
{
var id = Guid.NewGuid();
var user1 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 1",
Email = $"test1+{id}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var user2 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 2",
Email = $"test2+{id}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.Argon2id,
KdfIterations = 4,
KdfMemory = 5,
KdfParallelism = 6
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Id = CoreHelpers.GenerateComb(),
Name = $"Test Org {id}",
BillingEmail = user1.Email,
Plan = "Test",
PrivateKey = "privatekey",
PublicKey = "publickey",
UseGroups = true,
Enabled = true,
UsePasswordManager = true
});
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
ResetPasswordKey = "resetpasswordkey1",
AccessSecretsManager = false
});
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user2.Id,
Status = OrganizationUserStatusType.Invited,
Type = OrganizationUserType.User,
ResetPasswordKey = "resetpasswordkey2",
AccessSecretsManager = true
});
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: false, includeCollections: false);
Assert.NotNull(responseModel);
Assert.Equal(2, responseModel.Count);
var user1Result = responseModel.FirstOrDefault(u => u.Id == orgUser1.Id);
Assert.NotNull(user1Result);
Assert.Equal(user1.Name, user1Result.Name);
Assert.Equal(user1.Email, user1Result.Email);
Assert.Equal(orgUser1.Status, user1Result.Status);
Assert.Equal(orgUser1.Type, user1Result.Type);
Assert.Equal(organization.Id, user1Result.OrganizationId);
Assert.Equal(user1.Id, user1Result.UserId);
Assert.Empty(user1Result.Groups);
Assert.Empty(user1Result.Collections);
var user2Result = responseModel.FirstOrDefault(u => u.Id == orgUser2.Id);
Assert.NotNull(user2Result);
Assert.Equal(user2.Name, user2Result.Name);
Assert.Equal(user2.Email, user2Result.Email);
Assert.Equal(orgUser2.Status, user2Result.Status);
Assert.Equal(orgUser2.Type, user2Result.Type);
Assert.Equal(organization.Id, user2Result.OrganizationId);
Assert.Equal(user2.Id, user2Result.UserId);
Assert.Empty(user2Result.Groups);
Assert.Empty(user2Result.Collections);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByOrganizationAsync_vNext_WithGroupsAndCollections_ReturnsUserDetailsWithBoth(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IGroupRepository groupRepository,
ICollectionRepository collectionRepository)
{
var id = Guid.NewGuid();
var requestTime = DateTime.UtcNow;
var user1 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 1",
Email = $"test1+{id}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Id = CoreHelpers.GenerateComb(),
Name = $"Test Org {id}",
BillingEmail = user1.Email,
Plan = "Test",
PrivateKey = "privatekey",
PublicKey = "publickey",
UseGroups = true,
Enabled = true
});
var group1 = await groupRepository.CreateAsync(new Group
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Group 1",
ExternalId = "external-group-1"
});
var group2 = await groupRepository.CreateAsync(new Group
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Group 2",
ExternalId = "external-group-2"
});
var collection1 = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Collection 1",
ExternalId = "external-collection-1",
CreationDate = requestTime,
RevisionDate = requestTime
});
var collection2 = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "Test Collection 2",
ExternalId = "external-collection-2",
CreationDate = requestTime,
RevisionDate = requestTime
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
Name = "My Items",
Type = CollectionType.DefaultUserCollection,
DefaultUserCollectionEmail = user1.Email,
CreationDate = requestTime,
RevisionDate = requestTime
});
// Create organization user with both groups and collections using CreateManyAsync
var createOrgUserWithCollections = new List<CreateOrganizationUser>
{
new()
{
OrganizationUser = new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
AccessSecretsManager = false
},
Collections =
[
new CollectionAccessSelection
{
Id = collection1.Id,
ReadOnly = true,
HidePasswords = false,
Manage = false
},
new CollectionAccessSelection
{
Id = collection2.Id,
ReadOnly = false,
HidePasswords = true,
Manage = true
},
new CollectionAccessSelection
{
Id = defaultUserCollection.Id,
ReadOnly = false,
HidePasswords = false,
Manage = true
}
],
Groups = [group1.Id, group2.Id]
}
};
await organizationUserRepository.CreateManyAsync(createOrgUserWithCollections);
var responseModel = await organizationUserRepository.GetManyDetailsByOrganizationAsync_vNext(organization.Id, includeGroups: true, includeCollections: true);
Assert.NotNull(responseModel);
Assert.Single(responseModel);
var user1Result = responseModel.First();
Assert.Equal(user1.Name, user1Result.Name);
Assert.Equal(user1.Email, user1Result.Email);
Assert.Equal(organization.Id, user1Result.OrganizationId);
Assert.Equal(user1.Id, user1Result.UserId);
Assert.NotNull(user1Result.Groups);
Assert.Equal(2, user1Result.Groups.Count());
Assert.Contains(group1.Id, user1Result.Groups);
Assert.Contains(group2.Id, user1Result.Groups);
Assert.NotNull(user1Result.Collections);
Assert.Equal(2, user1Result.Collections.Count());
Assert.Contains(user1Result.Collections, c => c.Id == collection1.Id);
Assert.Contains(user1Result.Collections, c => c.Id == collection2.Id);
Assert.DoesNotContain(user1Result.Collections, c => c.Id == defaultUserCollection.Id);
}
[DatabaseTheory, DatabaseData]
public async Task GetManyByOrganizationWithClaimedDomainsAsync_WithNoVerifiedDomain_ReturnsEmpty(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var requestTime = DateTime.UtcNow;
var user1 = await userRepository.CreateAsync(new User
{
Id = CoreHelpers.GenerateComb(),
Name = "Test User 1",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
CreationDate = requestTime,
RevisionDate = requestTime,
AccountRevisionDate = requestTime
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Id = CoreHelpers.GenerateComb(),
Name = $"Test Org {id}",
BillingEmail = user1.Email,
Plan = "Test",
Enabled = true,
CreationDate = requestTime,
RevisionDate = requestTime
});
// Create domain but do NOT verify it
var organizationDomain = new OrganizationDomain
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345",
CreationDate = requestTime
};
organizationDomain.SetNextRunDate(12);
// Note: NOT calling SetVerifiedDate()
await organizationDomainRepository.CreateAsync(organizationDomain);
await organizationUserRepository.CreateAsync(new OrganizationUser
{
Id = CoreHelpers.GenerateComb(),
OrganizationId = organization.Id,
UserId = user1.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.Owner,
CreationDate = requestTime,
RevisionDate = requestTime
});
var responseModel = await organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organization.Id);
Assert.NotNull(responseModel);
Assert.Empty(responseModel);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_WithNullEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email,
Plan = "Test",
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = null
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_WithEmptyEmail_DoesNotSetDefaultUserCollectionEmail(IUserRepository userRepository,
ICollectionRepository collectionRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository
)
{
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{Guid.NewGuid()}@example.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
BillingEmail = user.Email,
Plan = "Test",
});
var orgUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Email = "" // Empty string email
});
var defaultUserCollection = await collectionRepository.CreateAsync(new Collection
{
Name = "Test Collection",
Id = user.Id,
Type = CollectionType.DefaultUserCollection,
OrganizationId = organization.Id
});
await collectionRepository.UpdateUsersAsync(defaultUserCollection.Id, new List<CollectionAccessSelection>()
{
new CollectionAccessSelection
{
Id = orgUser.Id,
HidePasswords = false,
ReadOnly = false,
Manage = true
},
});
await organizationUserRepository.DeleteAsync(orgUser);
var updatedCollection = await collectionRepository.GetByIdAsync(defaultUserCollection.Id);
Assert.NotNull(updatedCollection);
Assert.Equal(CollectionType.SharedCollection, updatedCollection.Type);
Assert.Equal(user.Email, updatedCollection.DefaultUserCollectionEmail);
}
[DatabaseTheory, DatabaseData]
public async Task ReplaceAsync_PreservesDefaultCollections_WhenUpdatingCollectionAccess(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
ICollectionRepository collectionRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
// Create a regular collection and a default collection
var regularCollection = await collectionRepository.CreateTestCollectionAsync(organization);
// Manually create default collection since CreateTestCollectionAsync doesn't support type parameter
var defaultCollection = new Collection
{
OrganizationId = organization.Id,
Name = $"Default Collection {Guid.NewGuid()}",
Type = CollectionType.DefaultUserCollection
};
await collectionRepository.CreateAsync(defaultCollection);
var newCollection = await collectionRepository.CreateTestCollectionAsync(organization);
// Set up initial collection access: user has access to both regular and default collections
await organizationUserRepository.ReplaceAsync(orgUser, [
new CollectionAccessSelection { Id = regularCollection.Id, ReadOnly = false, HidePasswords = false, Manage = false },
new CollectionAccessSelection { Id = defaultCollection.Id, ReadOnly = false, HidePasswords = false, Manage = true }
]);
// Verify initial state
var (_, initialCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
Assert.Equal(2, initialCollections.Count);
Assert.Contains(initialCollections, c => c.Id == regularCollection.Id);
Assert.Contains(initialCollections, c => c.Id == defaultCollection.Id);
// Act: Update collection access with only the new collection
// This should preserve the default collection but remove the regular collection
await organizationUserRepository.ReplaceAsync(orgUser, [
new CollectionAccessSelection { Id = newCollection.Id, ReadOnly = false, HidePasswords = false, Manage = true }
]);
// Assert
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
Assert.NotNull(actualOrgUser);
Assert.Equal(2, actualCollections.Count); // Should have default collection + new collection
// Default collection should be preserved
var preservedDefaultCollection = actualCollections.FirstOrDefault(c => c.Id == defaultCollection.Id);
Assert.NotNull(preservedDefaultCollection);
Assert.True(preservedDefaultCollection.Manage); // Original permissions preserved
// New collection should be added
var addedNewCollection = actualCollections.FirstOrDefault(c => c.Id == newCollection.Id);
Assert.NotNull(addedNewCollection);
Assert.True(addedNewCollection.Manage);
// Regular collection should be removed
Assert.DoesNotContain(actualCollections, c => c.Id == regularCollection.Id);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserIsAccepted_ReturnsTrue(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user);
const string key = "test-key";
orgUser.Key = key;
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = key
};
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.True(result);
var updatedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(updatedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, updatedUser.Status);
Assert.Equal(key, updatedUser.Key);
// Annul
await organizationRepository.DeleteAsync(organization);
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserIsAlreadyConfirmed_ReturnsFalse(IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateConfirmedTestOrganizationUserAsync(organization, user);
orgUser.Status = OrganizationUserStatusType.Accepted; // To simulate a second call to ConfirmOrganizationUserAsync
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = "test-key"
};
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.False(result);
var unchangedUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(unchangedUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, unchangedUser.Status);
// Annul
await organizationRepository.DeleteAsync(organization);
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_IsIdempotent_WhenCalledMultipleTimes(
IOrganizationUserRepository organizationUserRepository,
IOrganizationRepository organizationRepository,
IUserRepository userRepository)
{
// Arrange
var organization = await organizationRepository.CreateTestOrganizationAsync();
var user = await userRepository.CreateTestUserAsync();
var orgUser = await organizationUserRepository.CreateAcceptedTestOrganizationUserAsync(organization, user);
var acceptedOrganizationUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = orgUser.Id,
UserId = user.Id,
Key = "test-key"
};
// Act - First call should confirm
var firstResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
var secondResult = await organizationUserRepository.ConfirmOrganizationUserAsync(acceptedOrganizationUser);
// Assert
Assert.True(firstResult);
Assert.False(secondResult);
var finalUser = await organizationUserRepository.GetByIdAsync(orgUser.Id);
Assert.NotNull(finalUser);
Assert.Equal(OrganizationUserStatusType.Confirmed, finalUser.Status);
// Annul
await organizationRepository.DeleteAsync(organization);
await userRepository.DeleteAsync(user);
}
[Theory, DatabaseData]
public async Task ConfirmOrganizationUserAsync_WhenUserDoesNotExist_ReturnsFalse(
IOrganizationUserRepository organizationUserRepository)
{
// Arrange
var nonExistentUser = new AcceptedOrganizationUserToConfirm
{
OrganizationUserId = Guid.NewGuid(),
UserId = Guid.NewGuid(),
Key = "test-key"
};
// Act
var result = await organizationUserRepository.ConfirmOrganizationUserAsync(nonExistentUser);
// Assert
Assert.False(result);
}
}