mirror of
https://github.com/bitwarden/server.git
synced 2026-01-31 06:03:12 +08:00
[PM-30626] Fetch provided storage from Pricing Service when determining storage limit (#6845)
* Fetch provided storage from Pricing Service * Run dotnet format * Gbubemi's feedback
This commit is contained in:
@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@@ -27,6 +28,7 @@ public class SendValidationService : ISendValidationService
|
|||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
private readonly IPricingClient _pricingClient;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -38,7 +40,7 @@ public class SendValidationService : ISendValidationService
|
|||||||
IUserService userService,
|
IUserService userService,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
|
IPricingClient pricingClient,
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_userRepository = userRepository;
|
_userRepository = userRepository;
|
||||||
@@ -48,6 +50,7 @@ public class SendValidationService : ISendValidationService
|
|||||||
_userService = userService;
|
_userService = userService;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_pricingClient = pricingClient;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,10 +126,19 @@ public class SendValidationService : ISendValidationService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Users that get access to file storage/premium from their organization get the default
|
// Users that get access to file storage/premium from their organization get storage
|
||||||
// 1 GB max storage.
|
// based on the current premium plan from the pricing service
|
||||||
short limit = _globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1;
|
short provided;
|
||||||
storageBytesRemaining = user.StorageBytesRemaining(limit);
|
if (_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
provided = Constants.SelfHostedMaxStorageGb;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||||
|
provided = (short)premiumPlan.Storage.Provided;
|
||||||
|
}
|
||||||
|
storageBytesRemaining = user.StorageBytesRemaining(provided);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (send.OrganizationId.HasValue)
|
else if (send.OrganizationId.HasValue)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
@@ -46,6 +47,7 @@ public class CipherService : ICipherService
|
|||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
private readonly IApplicationCacheService _applicationCacheService;
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IPricingClient _pricingClient;
|
||||||
|
|
||||||
public CipherService(
|
public CipherService(
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
@@ -65,7 +67,8 @@ public class CipherService : ICipherService
|
|||||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IApplicationCacheService applicationCacheService,
|
IApplicationCacheService applicationCacheService,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IPricingClient pricingClient)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@@ -85,6 +88,7 @@ public class CipherService : ICipherService
|
|||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_pricingClient = pricingClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||||
@@ -943,10 +947,19 @@ public class CipherService : ICipherService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Users that get access to file storage/premium from their organization get the default
|
// Users that get access to file storage/premium from their organization get storage
|
||||||
// 1 GB max storage.
|
// based on the current premium plan from the pricing service
|
||||||
storageBytesRemaining = user.StorageBytesRemaining(
|
short provided;
|
||||||
_globalSettings.SelfHosted ? Constants.SelfHostedMaxStorageGb : (short)1);
|
if (_globalSettings.SelfHosted)
|
||||||
|
{
|
||||||
|
provided = Constants.SelfHostedMaxStorageGb;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var premiumPlan = await _pricingClient.GetAvailablePremiumPlan();
|
||||||
|
provided = (short)premiumPlan.Storage.Provided;
|
||||||
|
}
|
||||||
|
storageBytesRemaining = user.StorageBytesRemaining(provided);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (cipher.OrganizationId.HasValue)
|
else if (cipher.OrganizationId.HasValue)
|
||||||
|
|||||||
120
test/Core.Test/Tools/Services/SendValidationServiceTests.cs
Normal file
120
test/Core.Test/Tools/Services/SendValidationServiceTests.cs
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Pricing.Premium;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Enums;
|
||||||
|
using Bit.Core.Tools.Services;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Tools.Services;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class SendValidationServiceTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task StorageRemainingForSendAsync_OrgGrantedPremiumUser_UsesPricingService(
|
||||||
|
SutProvider<SendValidationService> sutProvider,
|
||||||
|
Send send,
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
send.UserId = user.Id;
|
||||||
|
send.OrganizationId = null;
|
||||||
|
send.Type = SendType.File;
|
||||||
|
user.Premium = false;
|
||||||
|
user.Storage = 1024L * 1024L * 1024L; // 1 GB used
|
||||||
|
user.EmailVerified = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||||
|
|
||||||
|
var premiumPlan = new Plan
|
||||||
|
{
|
||||||
|
Storage = new Purchasable { Provided = 5 }
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IPricingClient>().GetAvailablePremiumPlan().Returns(premiumPlan);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().Received(1).GetAvailablePremiumPlan();
|
||||||
|
Assert.True(result > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task StorageRemainingForSendAsync_IndividualPremium_DoesNotCallPricingService(
|
||||||
|
SutProvider<SendValidationService> sutProvider,
|
||||||
|
Send send,
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
send.UserId = user.Id;
|
||||||
|
send.OrganizationId = null;
|
||||||
|
send.Type = SendType.File;
|
||||||
|
user.Premium = true;
|
||||||
|
user.MaxStorageGb = 10;
|
||||||
|
user.EmailVerified = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
|
// Assert - should NOT call pricing service for individual premium users
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task StorageRemainingForSendAsync_SelfHosted_DoesNotCallPricingService(
|
||||||
|
SutProvider<SendValidationService> sutProvider,
|
||||||
|
Send send,
|
||||||
|
User user)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
send.UserId = user.Id;
|
||||||
|
send.OrganizationId = null;
|
||||||
|
send.Type = SendType.File;
|
||||||
|
user.Premium = false;
|
||||||
|
user.EmailVerified = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = true;
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(user.Id).Returns(user);
|
||||||
|
sutProvider.GetDependency<IUserService>().CanAccessPremium(user).Returns(true);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
|
// Assert - should NOT call pricing service for self-hosted
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task StorageRemainingForSendAsync_OrgSend_DoesNotCallPricingService(
|
||||||
|
SutProvider<SendValidationService> sutProvider,
|
||||||
|
Send send,
|
||||||
|
Organization org)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
send.UserId = null;
|
||||||
|
send.OrganizationId = org.Id;
|
||||||
|
send.Type = SendType.File;
|
||||||
|
org.MaxStorageGb = 100;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(org.Id).Returns(org);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.StorageRemainingForSendAsync(send);
|
||||||
|
|
||||||
|
// Assert - should NOT call pricing service for org sends
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Pricing.Premium;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@@ -2228,10 +2230,6 @@ public class CipherServiceTests
|
|||||||
.PushSyncCiphersAsync(deletingUserId);
|
.PushSyncCiphersAsync(deletingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationCipherCustomize]
|
[OrganizationCipherCustomize]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
@@ -2387,6 +2385,186 @@ public class CipherServiceTests
|
|||||||
ids.Count() == cipherIds.Length && ids.All(id => cipherIds.Contains(id))));
|
ids.Count() == cipherIds.Length && ids.All(id => cipherIds.Contains(id))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_UsesStorageFromPricingClient(
|
||||||
|
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(new byte[100]);
|
||||||
|
var fileName = "test.txt";
|
||||||
|
var key = "test-key";
|
||||||
|
|
||||||
|
// Setup cipher with user ownership
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
// Setup user WITHOUT personal premium (Premium = false), but with org-granted premium access
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = savingUserId,
|
||||||
|
Premium = false, // User does not have personal premium
|
||||||
|
MaxStorageGb = null, // No personal storage allocation
|
||||||
|
Storage = 0 // No storage used yet
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetByIdAsync(savingUserId)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
// User has premium access through their organization
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.CanAccessPremium(user)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Mock GlobalSettings to indicate cloud (not self-hosted)
|
||||||
|
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||||
|
|
||||||
|
// Mock the PricingClient to return a premium plan with 1 GB of storage
|
||||||
|
var premiumPlan = new Plan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
Seat = new Purchasable { StripePriceId = "price_123", Price = 10, Provided = 1 },
|
||||||
|
Storage = new Purchasable { StripePriceId = "price_456", Price = 4, Provided = 1 }
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetAvailablePremiumPlan()
|
||||||
|
.Returns(premiumPlan);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||||
|
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||||
|
.ValidateFileAsync(cipher, Arg.Any<CipherAttachment.MetaData>(), Arg.Any<long>())
|
||||||
|
.Returns((true, 100L));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate);
|
||||||
|
|
||||||
|
// Assert - PricingClient was called to get the premium plan storage
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().Received(1).GetAvailablePremiumPlan();
|
||||||
|
|
||||||
|
// Assert - Attachment was uploaded successfully
|
||||||
|
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||||
|
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_ExceedsStorage_ThrowsBadRequest(
|
||||||
|
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(new byte[100]);
|
||||||
|
var fileName = "test.txt";
|
||||||
|
var key = "test-key";
|
||||||
|
|
||||||
|
// Setup cipher with user ownership
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
// Setup user WITHOUT personal premium, with org-granted access, but storage is full
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = savingUserId,
|
||||||
|
Premium = false,
|
||||||
|
MaxStorageGb = null,
|
||||||
|
Storage = 1073741824 // 1 GB already used (equals the provided storage)
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetByIdAsync(savingUserId)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.CanAccessPremium(user)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = false;
|
||||||
|
|
||||||
|
// Premium plan provides 1 GB of storage
|
||||||
|
var premiumPlan = new Plan
|
||||||
|
{
|
||||||
|
Name = "Premium",
|
||||||
|
Available = true,
|
||||||
|
Seat = new Purchasable { StripePriceId = "price_123", Price = 10, Provided = 1 },
|
||||||
|
Storage = new Purchasable { StripePriceId = "price_456", Price = 4, Provided = 1 }
|
||||||
|
};
|
||||||
|
sutProvider.GetDependency<IPricingClient>()
|
||||||
|
.GetAvailablePremiumPlan()
|
||||||
|
.Returns(premiumPlan);
|
||||||
|
|
||||||
|
// Act & Assert - Should throw because storage is full
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate));
|
||||||
|
Assert.Contains("Not enough storage available", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task CreateAttachmentAsync_UserWithOrgGrantedPremium_SelfHosted_UsesConstantStorage(
|
||||||
|
SutProvider<CipherService> sutProvider, CipherDetails cipher, Guid savingUserId)
|
||||||
|
{
|
||||||
|
var stream = new MemoryStream(new byte[100]);
|
||||||
|
var fileName = "test.txt";
|
||||||
|
var key = "test-key";
|
||||||
|
|
||||||
|
// Setup cipher with user ownership
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
// Setup user WITHOUT personal premium, but with org-granted premium access
|
||||||
|
var user = new User
|
||||||
|
{
|
||||||
|
Id = savingUserId,
|
||||||
|
Premium = false,
|
||||||
|
MaxStorageGb = null,
|
||||||
|
Storage = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserRepository>()
|
||||||
|
.GetByIdAsync(savingUserId)
|
||||||
|
.Returns(user);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IUserService>()
|
||||||
|
.CanAccessPremium(user)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
// Mock GlobalSettings to indicate self-hosted
|
||||||
|
sutProvider.GetDependency<Bit.Core.Settings.GlobalSettings>().SelfHosted = true;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||||
|
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAttachmentStorageService>()
|
||||||
|
.ValidateFileAsync(cipher, Arg.Any<CipherAttachment.MetaData>(), Arg.Any<long>())
|
||||||
|
.Returns((true, 100L));
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.UpdateAttachmentAsync(Arg.Any<CipherAttachment>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.ReplaceAsync(Arg.Any<CipherDetails>())
|
||||||
|
.Returns(Task.CompletedTask);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await sutProvider.Sut.CreateAttachmentAsync(cipher, stream, fileName, key, 100, savingUserId, false, cipher.RevisionDate);
|
||||||
|
|
||||||
|
// Assert - PricingClient should NOT be called for self-hosted
|
||||||
|
await sutProvider.GetDependency<IPricingClient>().DidNotReceive().GetAvailablePremiumPlan();
|
||||||
|
|
||||||
|
// Assert - Attachment was uploaded successfully
|
||||||
|
await sutProvider.GetDependency<IAttachmentStorageService>().Received(1)
|
||||||
|
.UploadNewAttachmentAsync(Arg.Any<Stream>(), cipher, Arg.Any<CipherAttachment.MetaData>());
|
||||||
|
}
|
||||||
|
|
||||||
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
private async Task AssertNoActionsAsync(SutProvider<CipherService> sutProvider)
|
||||||
{
|
{
|
||||||
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
await sutProvider.GetDependency<ICipherRepository>().DidNotReceiveWithAnyArgs().GetManyOrganizationDetailsByOrganizationIdAsync(default);
|
||||||
|
|||||||
Reference in New Issue
Block a user