using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Auth.Repositories;
public class EmergencyAccessRepositoriesTests
{
[DatabaseTheory, DatabaseData]
public async Task DeleteAsync_UpdatesRevisionDate(IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Test Grantor User",
Email = $"test+grantor{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser = await userRepository.CreateAsync(new User
{
Name = "Test Grantee User",
Email = $"test+grantee{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var emergencyAccess = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = granteeUser.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
await emergencyAccessRepository.DeleteAsync(emergencyAccess);
var updatedGrantee = await userRepository.GetByIdAsync(granteeUser.Id);
Assert.NotNull(updatedGrantee);
Assert.NotEqual(updatedGrantee.AccountRevisionDate, granteeUser.AccountRevisionDate);
}
///
/// Creates 3 Emergency Access records all connected to a single grantor, but separate grantees.
/// All 3 records are then deleted in a single call to DeleteManyAsync.
///
[DatabaseTheory, DatabaseData]
public async Task DeleteManyAsync_DeletesMultipleGranteeRecordsAsync(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Test Grantor User",
Email = $"test+grantor{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var confirmedGranteeUser1 = await userRepository.CreateAsync(new User
{
Name = "Test Grantee User 1",
Email = $"test+grantee{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var invitedGranteeUser2 = await userRepository.CreateAsync(new User
{
Name = "Test Grantee User 2",
Email = $"test+grantee{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser3 = await userRepository.CreateAsync(new User
{
Name = "Test Grantee User 3",
Email = $"test+grantee{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var confirmedEmergencyAccess = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = confirmedGranteeUser1.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
var invitedEmergencyAccess = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = invitedGranteeUser2.Id,
Status = EmergencyAccessStatusType.Invited,
});
var acceptedEmergencyAccess = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = granteeUser3.Id,
Status = EmergencyAccessStatusType.Accepted,
});
// Act
await emergencyAccessRepository.DeleteManyAsync([confirmedEmergencyAccess.Id, invitedEmergencyAccess.Id, acceptedEmergencyAccess.Id]);
// Assert
// ensure Grantor records deleted
var grantorEmergencyAccess = await emergencyAccessRepository.GetManyDetailsByGrantorIdAsync(grantorUser.Id);
Assert.Empty(grantorEmergencyAccess);
// ensure Grantee records deleted
foreach (var grantee in (List)[confirmedGranteeUser1, invitedGranteeUser2, granteeUser3])
{
var granteeEmergencyAccess = await emergencyAccessRepository.GetManyDetailsByGranteeIdAsync(grantee.Id);
Assert.Empty(granteeEmergencyAccess);
}
}
///
/// Verifies GetManyDetailsByUserIdsAsync returns all emergency access records
/// where the user IDs are either grantors or grantees.
///
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserIdsAsync_ReturnsAllMatchingRecords(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange - Create users
var grantorUser1 = await userRepository.CreateAsync(new User
{
Name = "Grantor 1",
Email = $"test+grantor1{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var grantorUser2 = await userRepository.CreateAsync(new User
{
Name = "Grantor 2",
Email = $"test+grantor2{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser1 = await userRepository.CreateAsync(new User
{
Name = "Grantee 1",
Email = $"test+grantee1{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser2 = await userRepository.CreateAsync(new User
{
Name = "Grantee 2",
Email = $"test+grantee2{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
// Create emergency access records
// Grantor1 -> Grantee1 (matches both queried IDs)
var ea1 = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser1.Id,
GranteeId = granteeUser1.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Grantor2 -> Grantee1 (matches via grantee)
var ea2 = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser2.Id,
GranteeId = granteeUser1.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Grantor1 -> Grantee2 (matches via grantor)
var ea3 = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser1.Id,
GranteeId = granteeUser2.Id,
Status = EmergencyAccessStatusType.Accepted,
});
// Grantor2 -> Grantee2 (should NOT be returned - neither user is in the query)
await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser2.Id,
GranteeId = granteeUser2.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Act - Query with Grantor1 and Grantee1 user IDs
var userIds = new List { grantorUser1.Id, granteeUser1.Id };
var results = await emergencyAccessRepository.GetManyDetailsByUserIdsAsync(userIds);
// Assert - Should return exactly the 3 records involving Grantor1 or Grantee1:
// - Grantor1 -> Grantee1 (matches both, returned once)
// - Grantor2 -> Grantee1 (matches via grantee)
// - Grantor1 -> Grantee2 (matches via grantor)
Assert.NotNull(results);
Assert.Equal(3, results.Count);
var resultIds = results.Select(r => r.Id).ToHashSet();
Assert.Contains(ea1.Id, resultIds);
Assert.Contains(ea2.Id, resultIds);
Assert.Contains(ea3.Id, resultIds);
}
///
/// Verifies GetManyDetailsByUserIdsAsync handles an empty list gracefully
/// and returns an empty collection.
///
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserIdsAsync_HandlesEmptyList(
IEmergencyAccessRepository emergencyAccessRepository)
{
// Act
var results = await emergencyAccessRepository.GetManyDetailsByUserIdsAsync([]);
// Assert
Assert.NotNull(results);
Assert.Empty(results);
}
///
/// Verifies GetManyDetailsByUserIdsAsync includes full details from both
/// grantor and grantee users (emails, names populated via JOIN).
///
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserIdsAsync_IncludesDetailsFromBothUsers(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange
var grantorEmail = $"test+grantor{Guid.NewGuid()}@email.com";
var granteeEmail = $"test+grantee{Guid.NewGuid()}@email.com";
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Grantor Name",
Email = grantorEmail,
AvatarColor = "#ff0000",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser = await userRepository.CreateAsync(new User
{
Name = "Grantee Name",
Email = granteeEmail,
AvatarColor = "#0000ff",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = granteeUser.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Act
var results = await emergencyAccessRepository.GetManyDetailsByUserIdsAsync([grantorUser.Id]);
// Assert
Assert.NotNull(results);
Assert.Single(results);
var record = results.First();
Assert.Equal(grantorEmail, record.GrantorEmail);
Assert.Equal(granteeEmail, record.GranteeEmail);
Assert.Equal("Grantor Name", record.GrantorName);
Assert.Equal("Grantee Name", record.GranteeName);
Assert.Equal("#ff0000", record.GrantorAvatarColor);
Assert.Equal("#0000ff", record.GranteeAvatarColor);
}
///
/// Verifies GetManyDetailsByUserIdsAsync returns records when the queried user ID
/// appears only as a grantee (not as a grantor in any record).
///
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserIdsAsync_GranteeOnlyQuery_ReturnsMatchingRecordsAsync(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Grantor",
Email = $"test+grantor{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser = await userRepository.CreateAsync(new User
{
Name = "Grantee",
Email = $"test+grantee{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var unrelatedUser = await userRepository.CreateAsync(new User
{
Name = "Unrelated",
Email = $"test+unrelated{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var expectedRecord = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = granteeUser.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Record that should NOT be returned - granteeUser is not involved
await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = unrelatedUser.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Act - query using only the grantee's ID; granteeUser has no grantor records
var results = await emergencyAccessRepository.GetManyDetailsByUserIdsAsync([granteeUser.Id]);
// Assert
Assert.NotNull(results);
Assert.Single(results);
Assert.Equal(expectedRecord.Id, results.First().Id);
}
///
/// Verifies GetDetailsByIdAsync returns the correct EmergencyAccessDetails record,
/// including email and name fields populated via the view JOIN.
///
[DatabaseTheory, DatabaseData]
public async Task GetDetailsByIdAsync_ReturnsDetails_WhenRecordExistsAsync(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange
var grantorEmail = $"test+grantor{Guid.NewGuid()}@email.com";
var granteeEmail = $"test+grantee{Guid.NewGuid()}@email.com";
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Grantor Name",
Email = grantorEmail,
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var granteeUser = await userRepository.CreateAsync(new User
{
Name = "Grantee Name",
Email = granteeEmail,
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var ea = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = granteeUser.Id,
Status = EmergencyAccessStatusType.Confirmed,
});
// Act
var result = await emergencyAccessRepository.GetDetailsByIdAsync(ea.Id);
// Assert
Assert.NotNull(result);
Assert.Equal(ea.Id, result.Id);
Assert.Equal(grantorEmail, result.GrantorEmail);
Assert.Equal(granteeEmail, result.GranteeEmail);
}
///
/// Verifies GetDetailsByIdAsync returns null when no record matches the given ID.
///
[DatabaseTheory, DatabaseData]
public async Task GetDetailsByIdAsync_ReturnsNull_WhenRecordDoesNotExistAsync(
IEmergencyAccessRepository emergencyAccessRepository)
{
// Act
var result = await emergencyAccessRepository.GetDetailsByIdAsync(Guid.NewGuid());
// Assert
Assert.Null(result);
}
///
/// Verifies GetManyDetailsByUserIdsAsync returns invited emergency access records
/// (where GranteeId is null and only Email is set) when querying by grantor ID,
/// and that GranteeEmail falls back to the invite email address.
///
[DatabaseTheory, DatabaseData]
public async Task GetManyDetailsByUserIdsAsync_InvitedRecord_ReturnedByGrantorIdAsync(
IUserRepository userRepository,
IEmergencyAccessRepository emergencyAccessRepository)
{
// Arrange
var grantorUser = await userRepository.CreateAsync(new User
{
Name = "Grantor",
Email = $"test+grantor{Guid.NewGuid()}@email.com",
ApiKey = "TEST",
SecurityStamp = "stamp",
});
var inviteEmail = $"test+invited{Guid.NewGuid()}@email.com";
var invitedRecord = await emergencyAccessRepository.CreateAsync(new EmergencyAccess
{
GrantorId = grantorUser.Id,
GranteeId = null,
Email = inviteEmail,
Status = EmergencyAccessStatusType.Invited,
});
// Act
var results = await emergencyAccessRepository.GetManyDetailsByUserIdsAsync([grantorUser.Id]);
// Assert
Assert.NotNull(results);
Assert.Single(results);
var record = results.First();
Assert.Equal(invitedRecord.Id, record.Id);
Assert.Null(record.GranteeId);
Assert.Equal(inviteEmail, record.GranteeEmail); // falls back to EA.Email when no registered grantee
Assert.Null(record.GranteeName);
Assert.Null(record.GranteeAvatarColor);
}
}