Refactor HasPremiumAccessQuery and IHasPremiumAccessQuery to streamline premium access checks. Updated method names for clarity and improved documentation. Adjusted test cases to reflect changes in user premium access retrieval logic.

This commit is contained in:
Rui Tome 2025-12-08 16:30:22 +00:00
parent 0a42c9e5d7
commit 651d5e3aac
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
3 changed files with 91 additions and 87 deletions

View File

@ -3,10 +3,6 @@ using Bit.Core.Repositories;
namespace Bit.Core.Billing.Premium.Queries;
/// <summary>
/// Query for checking premium access status for users using the existing stored procedure
/// that calculates premium access from personal subscriptions and organization memberships.
/// </summary>
public class HasPremiumAccessQuery : IHasPremiumAccessQuery
{
private readonly IUserRepository _userRepository;
@ -18,17 +14,18 @@ public class HasPremiumAccessQuery : IHasPremiumAccessQuery
public async Task<bool> HasPremiumAccessAsync(Guid userId)
{
var user = await _userRepository.GetCalculatedPremiumAsync(userId);
var user = await _userRepository.GetPremiumAccessAsync(userId);
if (user == null)
{
throw new NotFoundException();
}
return user.HasPremiumAccess;
}
public async Task<Dictionary<Guid, bool>> HasPremiumAccessAsync(IEnumerable<Guid> userIds)
{
var usersWithPremium = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds);
var usersWithPremium = await _userRepository.GetPremiumAccessByIdsAsync(userIds);
if (usersWithPremium.Count() != userIds.Count())
{
@ -40,16 +37,12 @@ public class HasPremiumAccessQuery : IHasPremiumAccessQuery
public async Task<bool> HasPremiumFromOrganizationAsync(Guid userId)
{
var user = await _userRepository.GetCalculatedPremiumAsync(userId);
var user = await _userRepository.GetPremiumAccessAsync(userId);
if (user == null)
{
throw new NotFoundException();
}
// Has org premium if has premium access but not personal premium
return user.HasPremiumAccess && !user.Premium;
return user.OrganizationPremium;
}
}

View File

@ -1,38 +1,30 @@
namespace Bit.Core.Billing.Premium.Queries;
/// <summary>
/// Query for checking premium access status for users.
/// This is the centralized location for determining if a user can access premium features
/// (either through personal subscription or organization membership).
///
/// <para>
/// <strong>Note:</strong> This is different from checking User.Premium, which only indicates
/// personal subscription status. Use these methods to check actual premium feature access.
/// </para>
/// Centralized query for checking if users have premium access through personal subscriptions or organizations.
/// Note: Different from User.Premium which only checks personal subscriptions.
/// </summary>
public interface IHasPremiumAccessQuery
{
/// <summary>
/// Checks if a user has access to premium features (personal subscription or organization).
/// This is the definitive way to check premium access for a single user.
/// Checks if a user has premium access (personal or organization).
/// </summary>
/// <param name="userId">The user ID to check for premium access</param>
/// <returns>True if user can access premium features; false otherwise</returns>
/// <param name="userId">The user ID to check</param>
/// <returns>True if user can access premium features</returns>
Task<bool> HasPremiumAccessAsync(Guid userId);
/// <summary>
/// Checks if a user has access to premium features through organization membership only.
/// This is useful for determining the source of premium access (personal vs organization).
/// Checks premium access for multiple users.
/// </summary>
/// <param name="userId">The user ID to check for organization premium access</param>
/// <returns>True if user has premium access through any organization; false otherwise</returns>
Task<bool> HasPremiumFromOrganizationAsync(Guid userId);
/// <param name="userIds">The user IDs to check</param>
/// <returns>Dictionary mapping user IDs to their premium access status</returns>
Task<Dictionary<Guid, bool>> HasPremiumAccessAsync(IEnumerable<Guid> userIds);
/// <summary>
/// Checks if multiple users have access to premium features (optimized bulk operation).
/// Uses existing stored procedure that calculates premium from personal subscriptions and organizations.
/// Checks if a user belongs to any organization that grants premium (enabled org with UsersGetPremium).
/// Returns true regardless of personal subscription. Useful for UI decisions like showing subscription options.
/// </summary>
/// <param name="userIds">The user IDs to check for premium access</param>
/// <returns>Dictionary mapping user IDs to their premium access status (personal or through organization)</returns>
Task<Dictionary<Guid, bool>> HasPremiumAccessAsync(IEnumerable<Guid> userIds);
/// <param name="userId">The user ID to check</param>
/// <returns>True if user is in any organization that grants premium</returns>
Task<bool> HasPremiumFromOrganizationAsync(Guid userId);
}

View File

@ -1,5 +1,5 @@
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Models.Data;
using Bit.Core.Billing.Premium.Models;
using Bit.Core.Billing.Premium.Queries;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
@ -13,15 +13,15 @@ public class HasPremiumAccessQueryTests
{
[Theory, BitAutoData]
public async Task HasPremiumAccessAsync_WhenUserHasPersonalPremium_ReturnsTrue(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = true;
user.HasPremiumAccess = true;
user.PersonalPremium = true;
user.OrganizationPremium = false;
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
@ -33,15 +33,15 @@ public class HasPremiumAccessQueryTests
[Theory, BitAutoData]
public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumButHasOrgPremium_ReturnsTrue(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = false;
user.HasPremiumAccess = true; // Has org premium
user.PersonalPremium = false;
user.OrganizationPremium = true; // Has org premium
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
@ -53,15 +53,15 @@ public class HasPremiumAccessQueryTests
[Theory, BitAutoData]
public async Task HasPremiumAccessAsync_WhenUserHasNoPersonalPremiumAndNoOrgPremium_ReturnsFalse(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = false;
user.HasPremiumAccess = false;
user.PersonalPremium = false;
user.OrganizationPremium = false;
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
@ -72,33 +72,31 @@ public class HasPremiumAccessQueryTests
}
[Theory, BitAutoData]
public async Task HasPremiumAccessAsync_WhenUserNotFound_ReturnsFalse(
public async Task HasPremiumAccessAsync_WhenUserNotFound_ThrowsNotFoundException(
Guid userId,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(userId)
.Returns((UserWithCalculatedPremium)null);
.GetPremiumAccessAsync(userId)
.Returns<UserPremiumAccess>(_ => throw new Bit.Core.Exceptions.NotFoundException());
// Act
var result = await sutProvider.Sut.HasPremiumAccessAsync(userId);
// Assert
Assert.False(result);
// Act & Assert
await Assert.ThrowsAsync<Bit.Core.Exceptions.NotFoundException>(
() => sutProvider.Sut.HasPremiumAccessAsync(userId));
}
[Theory, BitAutoData]
public async Task HasPremiumFromOrganizationAsync_WhenUserHasNoOrganizations_ReturnsFalse(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = false;
user.HasPremiumAccess = false; // No premium from anywhere
user.PersonalPremium = false;
user.OrganizationPremium = false; // No premium from anywhere
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
@ -110,15 +108,15 @@ public class HasPremiumAccessQueryTests
[Theory, BitAutoData]
public async Task HasPremiumFromOrganizationAsync_WhenUserHasPremiumFromOrg_ReturnsTrue(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = false; // No personal premium
user.HasPremiumAccess = true; // But has premium from org
user.PersonalPremium = false; // No personal premium
user.OrganizationPremium = true; // But has premium from org
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
@ -130,39 +128,57 @@ public class HasPremiumAccessQueryTests
[Theory, BitAutoData]
public async Task HasPremiumFromOrganizationAsync_WhenUserHasOnlyPersonalPremium_ReturnsFalse(
UserWithCalculatedPremium user,
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.Premium = true; // Has personal premium
user.HasPremiumAccess = true;
user.PersonalPremium = true; // Has personal premium
user.OrganizationPremium = false; // Not in any org that grants premium
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(user.Id)
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
// Assert
Assert.False(result); // Should return false because premium is from personal, not org
Assert.False(result); // Should return false because user is not in an org that grants premium
}
[Theory, BitAutoData]
public async Task HasPremiumFromOrganizationAsync_WhenUserNotFound_ReturnsFalse(
public async Task HasPremiumFromOrganizationAsync_WhenUserHasBothPersonalAndOrgPremium_ReturnsTrue(
UserPremiumAccess user,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
user.PersonalPremium = true; // Has personal premium
user.OrganizationPremium = true; // Also in an org that grants premium
sutProvider.GetDependency<IUserRepository>()
.GetPremiumAccessAsync(user.Id)
.Returns(user);
// Act
var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(user.Id);
// Assert
Assert.True(result); // Should return true because user IS in an org that grants premium (regardless of personal premium)
}
[Theory, BitAutoData]
public async Task HasPremiumFromOrganizationAsync_WhenUserNotFound_ThrowsNotFoundException(
Guid userId,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
sutProvider.GetDependency<IUserRepository>()
.GetCalculatedPremiumAsync(userId)
.Returns((UserWithCalculatedPremium)null);
.GetPremiumAccessAsync(userId)
.Returns<UserPremiumAccess>(_ => throw new Bit.Core.Exceptions.NotFoundException());
// Act
var result = await sutProvider.Sut.HasPremiumFromOrganizationAsync(userId);
// Assert
Assert.False(result);
// Act & Assert
await Assert.ThrowsAsync<Bit.Core.Exceptions.NotFoundException>(
() => sutProvider.Sut.HasPremiumFromOrganizationAsync(userId));
}
[Theory, BitAutoData]
@ -173,8 +189,8 @@ public class HasPremiumAccessQueryTests
var userIds = new List<Guid>();
sutProvider.GetDependency<IUserRepository>()
.GetManyWithCalculatedPremiumAsync(userIds)
.Returns(new List<UserWithCalculatedPremium>());
.GetPremiumAccessByIdsAsync(userIds)
.Returns(new List<UserPremiumAccess>());
// Act
var result = await sutProvider.Sut.HasPremiumAccessAsync(userIds);
@ -185,18 +201,21 @@ public class HasPremiumAccessQueryTests
[Theory, BitAutoData]
public async Task HasPremiumAccessAsync_Bulk_ReturnsCorrectStatus(
List<UserWithCalculatedPremium> users,
List<UserPremiumAccess> users,
SutProvider<HasPremiumAccessQuery> sutProvider)
{
// Arrange
users[0].HasPremiumAccess = true;
users[1].HasPremiumAccess = false;
users[2].HasPremiumAccess = true;
users[0].PersonalPremium = true;
users[0].OrganizationPremium = false;
users[1].PersonalPremium = false;
users[1].OrganizationPremium = false;
users[2].PersonalPremium = false;
users[2].OrganizationPremium = true;
var userIds = users.Select(u => u.Id).ToList();
sutProvider.GetDependency<IUserRepository>()
.GetManyWithCalculatedPremiumAsync(userIds)
.GetPremiumAccessByIdsAsync(userIds)
.Returns(users);
// Act
@ -204,9 +223,9 @@ public class HasPremiumAccessQueryTests
// Assert
Assert.Equal(users.Count, result.Count);
Assert.True(result[users[0].Id]);
Assert.False(result[users[1].Id]);
Assert.True(result[users[2].Id]);
Assert.True(result[users[0].Id]); // Personal premium
Assert.False(result[users[1].Id]); // No premium
Assert.True(result[users[2].Id]); // Organization premium
}
}