From 3f5cad436acfb8cd64e48890ef693aff4b3686f9 Mon Sep 17 00:00:00 2001 From: Jordi Date: Sat, 20 Jan 2024 08:32:20 +0100 Subject: [PATCH 01/10] Adding subscription usage and plan info and validation, including CI checks. --- .../Exceptions/ConsiderUpgradingException.cs | 21 ++++ .../NoSubscriptionPlanInfoException.cs | 18 +++ .../Exceptions/NoUsageFoundException.cs | 18 +++ .../SubscriptionExpiredException.cs | 19 +++ .../CommercialLicense/StockKeepingUnits.cs | 38 ++++++ .../SubscriptionBillingCycleType.cs | 21 ++++ .../CommercialLicense/SubscriptionInfo.cs | 35 ++++++ .../CommercialLicense/SubscriptionUsage.cs | 21 ++++ .../CommercialLicense/SubscriptionUserInfo.cs | 25 ++++ .../SubscriptionValidator.cs | 108 ++++++++++++++++++ .../Middleware/MiddlewareBuilder.cs | 2 +- .../FakeEnvironmentReader.cs | 32 ++++++ .../SubscriptionValidatorTests.PlanInfo.cs | 79 +++++++++++++ .../SubscriptionValidatorTests.Usage.cs | 90 +++++++++++++++ .../Middleware/MiddlewareBuilderTests.cs | 8 ++ 15 files changed, 534 insertions(+), 1 deletion(-) create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/ConsiderUpgradingException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoSubscriptionPlanInfoException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoUsageFoundException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/SubscriptionExpiredException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/FakeEnvironmentReader.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/ConsiderUpgradingException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/ConsiderUpgradingException.cs new file mode 100644 index 00000000..bb9c1603 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/ConsiderUpgradingException.cs @@ -0,0 +1,21 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Exception raised when the current number of users calculated based on the usage of your current subscription is greater than the maximum number of users in your current subscription + /// + public class ConsiderUpgradingPlanException: Exception + { + /// + /// Default constructor + /// + /// + /// + public ConsiderUpgradingPlanException(long currentNumberOfUsers, long allowedNumberOfUsers) : + base($"Your current subscription allows up to {allowedNumberOfUsers.ToString()}, however, {currentNumberOfUsers.ToString()} are currently using it. Please consider upgrading your current plan.") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoSubscriptionPlanInfoException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoSubscriptionPlanInfoException.cs new file mode 100644 index 00000000..5d9c63e9 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoSubscriptionPlanInfoException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Exception thrown if the info about the current subscription plan is unknown + /// + public class NoSubscriptionPlanInfoException: Exception + { + /// + /// Default constructor + /// + public NoSubscriptionPlanInfoException() : base("The current subscription info is unknown") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoUsageFoundException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoUsageFoundException.cs new file mode 100644 index 00000000..0410493e --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/NoUsageFoundException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Throws an exception when your current usage of FakeXrmEasy could not be retrieved + /// + public class NoUsageFoundException: Exception + { + /// + /// Default constructor + /// + public NoUsageFoundException() : base("No info about your current usage of FakeXrmEasy was found") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/SubscriptionExpiredException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/SubscriptionExpiredException.cs new file mode 100644 index 00000000..067264f1 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/SubscriptionExpiredException.cs @@ -0,0 +1,19 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// The current subscription expired + /// + public class SubscriptionExpiredException: Exception + { + /// + /// Throws an exception where the current subscription expired + /// + /// + public SubscriptionExpiredException(DateTime expiredOn) : base($"The current subscription expired on {expiredOn.ToLongDateString()}") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs b/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs new file mode 100644 index 00000000..7d33b124 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs @@ -0,0 +1,38 @@ +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Contains an enumeration of all the available product stock keeping units + /// + internal enum StockKeepingUnits + { + /// + /// Monthly Boutique Subscription + /// + FXE_BOU_1 = 1, + /// + /// Monthly Team Subscription + /// + FXE_TEA_1 = 2, + /// + /// Monthly Large Subscription + /// + FXE_LAR_1 = 3, + + /// + /// Annual Boutique Subscription + /// + FXE_BOU_12 = 4, + /// + /// Annual Team Subscription + /// + FXE_TEA_12 = 5, + /// + /// Annual Large Subscription + /// + FXE_LAR_12 = 6, + + FXE_BUS_12 = 7, + FXE_ENT_12 = 8, + FXE_ULT_12 = 9, + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs new file mode 100644 index 00000000..56bce243 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs @@ -0,0 +1,21 @@ +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Contains info about the current subscription billing cycle type + /// + public enum SubscriptionBillingCycleType + { + /// + /// Monthly: subscription billed per user/month + /// + Monthly = 0, + /// + /// Annual: subscription billed per user/year + /// + Annual = 1, + /// + /// PrePaid: The subscription is prepaid for a period different than one month or one year and is set in the StartDate and EndDate fields + /// + PrePaid = 2 + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs new file mode 100644 index 00000000..f7c06c8b --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs @@ -0,0 +1,35 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Contains info about the current subscription + /// + internal class SubscriptionInfo + { + /// + /// True if the current subscription auto-renews + /// + internal bool AutoRenews { get; set; } + + /// + /// The current billing cycle type + /// + internal SubscriptionBillingCycleType BillingType { get; set; } + + /// + /// Max number of users allowed in the current subscription + /// + internal long NumberOfUsers { get; set; } + + /// + /// The subscription start date + /// + internal DateTime StartDate { get; set; } + + /// + /// The subscription's end date + /// + internal DateTime EndDate { get; set; } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs new file mode 100644 index 00000000..106bc8b6 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Contains info about the current subscription usage + /// + internal class SubscriptionUsage + { + /// + /// The last time the current subscription usage was checked + /// + internal DateTime LastTimeChecked { get; set; } + + /// + /// Information about all the users + /// + internal IEnumerable Users { get; set; } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs new file mode 100644 index 00000000..af60e9c3 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs @@ -0,0 +1,25 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Info about the last time a given user used FakeXrmEasy + /// + internal class SubscriptionUserInfo + { + /// + /// The last time this user used FakeXrmEasy + /// + internal DateTime LastTimeUsed { get; set; } + + /// + /// The user's username + /// + internal string UserName { get; set; } + + /// + /// This user that runs as part of a CI process (Continuous Integration, i.e. either a build or release pipeline) + /// + internal bool IsCI { get; set; } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs new file mode 100644 index 00000000..ea2287e7 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs @@ -0,0 +1,108 @@ +using System; +using System.Linq; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Validates the current subscription usage is within the current subscription plan + /// + public sealed class SubscriptionValidator + { + private readonly IEnvironmentReader _environmentReader; + + public SubscriptionValidator(IEnvironmentReader environmentReader) + { + _environmentReader = environmentReader; + } + + /// + /// The current subscription plan + /// + internal SubscriptionInfo SubscriptionPlan { get; set; } + + /// + /// The current usage of the subscription + /// + internal SubscriptionUsage CurrentUsage { get; set; } + + /// + /// Validates if the current usage is within the subscription limits + /// + /// + internal bool IsValid() + { + var isSubscriptionPlanValid = IsSubscriptionPlanValid(); + if (!isSubscriptionPlanValid) + { + return false; + } + + var isSubscriptionUsageValid = IsUsageValid(); + if (!isSubscriptionUsageValid) + { + return false; + } + + return true; + } + + /// + /// Returns valid if the current subscription didn't expire yet + /// + /// + /// + /// + internal bool IsSubscriptionPlanValid() + { + if (SubscriptionPlan == null) + { + throw new NoSubscriptionPlanInfoException(); + } + + if (SubscriptionPlan.AutoRenews) + { + return true; + } + + var expiryDate = SubscriptionPlan.BillingType == SubscriptionBillingCycleType.Annual + ? SubscriptionPlan.StartDate.AddYears(1) + : SubscriptionPlan.StartDate.AddMonths(1); + + if (expiryDate < DateTime.UtcNow) + { + throw new SubscriptionExpiredException(expiryDate); + } + + return true; + } + + internal bool IsUsageValid() + { + if (IsRunningInContinuousIntegration()) + { + return true; + } + if (CurrentUsage == null) + { + throw new NoUsageFoundException(); + } + + var currentNumberOfUsers = CurrentUsage + .Users + .Count(userInfo => userInfo.LastTimeUsed >= DateTime.UtcNow.AddMonths(-1)); + + if (currentNumberOfUsers > SubscriptionPlan.NumberOfUsers) + { + throw new ConsiderUpgradingPlanException(currentNumberOfUsers, SubscriptionPlan.NumberOfUsers); + } + return true; + } + + private bool IsRunningInContinuousIntegration() + { + return "1".Equals(_environmentReader.GetEnvironmentVariable("FAKE_XRM_EASY_CI")) + || "True".Equals(_environmentReader.GetEnvironmentVariable(("TF_BUILD"))); + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs index 3ad0b271..58376c0b 100644 --- a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs +++ b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs @@ -107,7 +107,7 @@ public IXrmFakedContext Build() } /// - /// + /// FakeXrmEasy can be used under 3 different licences, this method defines the license. More info at: https://dynamicsvalue.github.io/fake-xrm-easy-docs/licensing/license/ /// /// /// diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/FakeEnvironmentReader.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/FakeEnvironmentReader.cs new file mode 100644 index 00000000..6daaa53e --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/FakeEnvironmentReader.cs @@ -0,0 +1,32 @@ +using System.Collections.Concurrent; +using FakeXrmEasy.Core.CommercialLicense; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public class FakeEnvironmentReader : IEnvironmentReader + { + private readonly ConcurrentDictionary _variables; + + public FakeEnvironmentReader() + { + _variables = new ConcurrentDictionary(); + } + + public string GetEnvironmentVariable(string variableName) + { + string variableValue = ""; + var exists = _variables.TryGetValue(variableName, out variableValue); + if (!exists) + { + return null; + } + + return variableValue; + } + + public void SetEnvironmentVariable(string variableName, string variableValue) + { + _variables.AddOrUpdate(variableName, variableValue, (key, oldValue) => variableValue); + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs new file mode 100644 index 00000000..1e0e94a5 --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs @@ -0,0 +1,79 @@ +using System; +using FakeXrmEasy.Core.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public partial class SubscriptionValidatorTests + { + private readonly IEnvironmentReader _defaultEnvironmentReader; + private readonly SubscriptionValidator _subscriptionValidator; + + public SubscriptionValidatorTests() + { + _defaultEnvironmentReader = new FakeEnvironmentReader(); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader); + } + [Fact] + public void Should_return_error_if_current_subscription_is_unknown() + { + Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + } + + [Fact] + public void Should_return_subscription_expired_exception_if_monthly_expired() + { + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); + _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Monthly; + _subscriptionValidator.SubscriptionPlan.AutoRenews = false; + + Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + } + + [Fact] + public void Should_not_return_subscription_expired_exception_if_monthly_expired_but_autorenew_is_enabled() + { + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); + _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Monthly; + _subscriptionValidator.SubscriptionPlan.AutoRenews = true; + + Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); + } + + [Fact] + public void Should_return_subscription_expired_exception_if_annual_expired() + { + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1); + _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Annual; + _subscriptionValidator.SubscriptionPlan.AutoRenews = false; + + Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + } + + [Fact] + public void Should_not_return_subscription_expired_exception_if_annual_expired_but_autorenew_is_enabled() + { + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1); + _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Annual; + _subscriptionValidator.SubscriptionPlan.AutoRenews = true; + + Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); + } + + [Fact] + public void Should_return_subscription_expired_exception_if_prepaid_expired() + { + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); + _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.PrePaid; + _subscriptionValidator.SubscriptionPlan.AutoRenews = false; + + Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs new file mode 100644 index 00000000..65cfb325 --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs @@ -0,0 +1,90 @@ +using System; +using FakeXrmEasy.Core.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public partial class SubscriptionValidatorTests + { + [Fact] + public void Should_return_no_usage_found_exception() + { + _subscriptionValidator.CurrentUsage = null; + Assert.Throws(() => _subscriptionValidator.IsUsageValid()); + } + + [Fact] + public void Should_return_consider_upgrading_exception_if_the_number_of_users_exceeds_the_current_subscription() + { + _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + { + Users = new SubscriptionUserInfo[] + { + new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, + } + }; + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.NumberOfUsers = 2; + + Assert.Throws(() => _subscriptionValidator.IsUsageValid()); + } + + [Fact] + public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_month() + { + _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + { + Users = new SubscriptionUserInfo[] + { + new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddMonths(-1).AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, + } + }; + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); + _subscriptionValidator.SubscriptionPlan.NumberOfUsers = 2; + + Assert.True(_subscriptionValidator.IsUsageValid()); + } + + [Fact] + public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() + { + _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + { + Users = new SubscriptionUserInfo[] + { + new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, + } + }; + + _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo + { + NumberOfUsers = 3 + }; + + Assert.True(_subscriptionValidator.IsUsageValid()); + } + + [Theory] + [InlineData("FAKE_XRM_EASY_CI", "1")] + [InlineData("TF_BUILD", "True")] + public void Should_ignore_usage_if_running_inside_ci(string envVariableName, string envVariableValue) + { + var continuousIntegrationEnvironmentReader = new FakeEnvironmentReader(); + continuousIntegrationEnvironmentReader.SetEnvironmentVariable(envVariableName, envVariableValue); + + var currentSubscription = new SubscriptionValidator(continuousIntegrationEnvironmentReader) + { + CurrentUsage = null + }; + + Assert.True(currentSubscription.IsUsageValid()); + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs b/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs index 3f7b01d3..14b37792 100644 --- a/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs +++ b/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs @@ -138,6 +138,14 @@ public void Should_not_throw_exception_when_using_default_faked_context_construc #pragma warning restore CS0618 // Type or member is obsolete Assert.Null(exception); } + + [Fact] + public void Should_return_user_info() + { + var windowsIdentity = System.Security.Principal.WindowsIdentity.GetCurrent(); + string userName = windowsIdentity.Name; + Assert.NotNull(userName); + } } From 3dffafa49cf1b17cf1b464ff839fcf6ba2dbd4e3 Mon Sep 17 00:00:00 2001 From: Jordi Date: Sat, 20 Jan 2024 09:44:54 +0100 Subject: [PATCH 02/10] Adding environment reader and update subscription validator accessibility to internal --- .../CommercialLicense/EnvironmentReader.cs | 17 +++++++++++++++++ .../CommercialLicense/SubscriptionValidator.cs | 4 ++-- src/FakeXrmEasy.Core/XrmFakedContext.cs | 2 +- 3 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/EnvironmentReader.cs diff --git a/src/FakeXrmEasy.Core/CommercialLicense/EnvironmentReader.cs b/src/FakeXrmEasy.Core/CommercialLicense/EnvironmentReader.cs new file mode 100644 index 00000000..a92c1d8b --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/EnvironmentReader.cs @@ -0,0 +1,17 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + internal interface IEnvironmentReader + { + string GetEnvironmentVariable(string variableName); + } + + internal class EnvironmentReader: IEnvironmentReader + { + public string GetEnvironmentVariable(string variableName) + { + return Environment.GetEnvironmentVariable(variableName); + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs index ea2287e7..596fe8ce 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs @@ -7,11 +7,11 @@ namespace FakeXrmEasy.Core.CommercialLicense /// /// Validates the current subscription usage is within the current subscription plan /// - public sealed class SubscriptionValidator + internal sealed class SubscriptionValidator { private readonly IEnvironmentReader _environmentReader; - public SubscriptionValidator(IEnvironmentReader environmentReader) + internal SubscriptionValidator(IEnvironmentReader environmentReader) { _environmentReader = environmentReader; } diff --git a/src/FakeXrmEasy.Core/XrmFakedContext.cs b/src/FakeXrmEasy.Core/XrmFakedContext.cs index e37282da..2cf94404 100644 --- a/src/FakeXrmEasy.Core/XrmFakedContext.cs +++ b/src/FakeXrmEasy.Core/XrmFakedContext.cs @@ -311,7 +311,7 @@ public virtual void Initialize(IEnumerable entities) /// /// Initializes the context with a single entity record /// - /// + /// Entity record that will be used to initialize the In-Memory context public void Initialize(Entity entity) { this.Initialize(new List() { entity }); From b49e0ff2c7ecef53a4d6b337f1f058e166db636e Mon Sep 17 00:00:00 2001 From: Jordi Date: Wed, 24 Jan 2024 21:51:58 +0100 Subject: [PATCH 03/10] Introducing subscription monitoring to facilitate counting of users to commercial customers... too many changes to describe in a single commit... --- .../Exceptions/InvalidLicenseKeyException.cs | 18 ++++ .../CommercialLicense/StockKeepingUnits.cs | 38 -------- .../SubscriptionBillingCycleType.cs | 21 ----- .../CommercialLicense/SubscriptionInfo.cs | 32 +++++-- .../CommercialLicense/SubscriptionManager.cs | 94 +++++++++++++++++++ .../CommercialLicense/SubscriptionUsage.cs | 13 ++- .../SubscriptionUsageManager.cs | 44 +++++++++ .../CommercialLicense/SubscriptionUserInfo.cs | 12 +-- .../SubscriptionValidator.cs | 42 ++++----- .../CommercialLicense/UserReader.cs | 22 +++++ src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj | 14 +-- .../Middleware/MiddlewareBuilder.cs | 33 +++++++ src/FakeXrmEasy.Core/XrmFakedContext.cs | 2 + .../SubscriptionManagerTests.cs | 20 ++++ .../SubscriptionUsageManagerTests.cs | 72 ++++++++++++++ .../SubscriptionValidatorTests.PlanInfo.cs | 65 ++++++++----- .../SubscriptionValidatorTests.Usage.cs | 61 ++++++++---- .../FakeXrmEasy.Core.Tests.csproj | 26 ++--- 18 files changed, 471 insertions(+), 158 deletions(-) create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/InvalidLicenseKeyException.cs delete mode 100644 src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs delete mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionManagerTests.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/InvalidLicenseKeyException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/InvalidLicenseKeyException.cs new file mode 100644 index 00000000..7879e1e6 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/InvalidLicenseKeyException.cs @@ -0,0 +1,18 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Exception raised when the license key is invalid or malformed + /// + public class InvalidLicenseKeyException: Exception + { + /// + /// Default constructor + /// + public InvalidLicenseKeyException() : base("The license key is invalid") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs b/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs deleted file mode 100644 index 7d33b124..00000000 --- a/src/FakeXrmEasy.Core/CommercialLicense/StockKeepingUnits.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace FakeXrmEasy.Core.CommercialLicense -{ - /// - /// Contains an enumeration of all the available product stock keeping units - /// - internal enum StockKeepingUnits - { - /// - /// Monthly Boutique Subscription - /// - FXE_BOU_1 = 1, - /// - /// Monthly Team Subscription - /// - FXE_TEA_1 = 2, - /// - /// Monthly Large Subscription - /// - FXE_LAR_1 = 3, - - /// - /// Annual Boutique Subscription - /// - FXE_BOU_12 = 4, - /// - /// Annual Team Subscription - /// - FXE_TEA_12 = 5, - /// - /// Annual Large Subscription - /// - FXE_LAR_12 = 6, - - FXE_BUS_12 = 7, - FXE_ENT_12 = 8, - FXE_ULT_12 = 9, - } -} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs deleted file mode 100644 index 56bce243..00000000 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionBillingCycleType.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace FakeXrmEasy.Core.CommercialLicense -{ - /// - /// Contains info about the current subscription billing cycle type - /// - public enum SubscriptionBillingCycleType - { - /// - /// Monthly: subscription billed per user/month - /// - Monthly = 0, - /// - /// Annual: subscription billed per user/year - /// - Annual = 1, - /// - /// PrePaid: The subscription is prepaid for a period different than one month or one year and is set in the StartDate and EndDate fields - /// - PrePaid = 2 - } -} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs index f7c06c8b..605c454e 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs @@ -1,35 +1,55 @@ using System; +using FakeXrmEasy.Abstractions.CommercialLicense; namespace FakeXrmEasy.Core.CommercialLicense { /// /// Contains info about the current subscription /// - internal class SubscriptionInfo + internal class SubscriptionInfo: ISubscriptionInfo { + /// + /// The CustomerId + /// + public string CustomerId { get; set; } + + /// + /// SKU + /// + public StockKeepingUnits SKU { get; set; } + /// /// True if the current subscription auto-renews /// - internal bool AutoRenews { get; set; } + public bool AutoRenews { get; set; } /// /// The current billing cycle type /// - internal SubscriptionBillingCycleType BillingType { get; set; } + public SubscriptionBillingCycleType BillingType { get; set; } /// /// Max number of users allowed in the current subscription /// - internal long NumberOfUsers { get; set; } + public long NumberOfUsers { get; set; } /// /// The subscription start date /// - internal DateTime StartDate { get; set; } + public DateTime StartDate { get; set; } /// /// The subscription's end date /// - internal DateTime EndDate { get; set; } + public DateTime EndDate { get; set; } + + /// + /// + /// + /// + internal void FromLicenseKey(string licenseKey) + { + + } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs new file mode 100644 index 00000000..111e654e --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs @@ -0,0 +1,94 @@ +using System; +using System.Globalization; +using System.Linq; +using System.Text; +using FakeXrmEasy.Abstractions.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + internal static class SubscriptionManager + { + internal static ISubscriptionInfo _subscriptionInfo; + internal static readonly object _subscriptionInfoLock = new object(); + + internal static ISubscriptionUsage _subscriptionUsage; + internal static readonly object _subscriptionUsageLock = new object(); + + private static string GenerateHash(string input) + { + using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(input); + byte[] hashBytes = md5.ComputeHash(inputBytes); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes[i].ToString("X2")); + } + return sb.ToString(); + } + } + + private static ISubscriptionInfo GetSubscriptionInfoFromKey(string licenseKey) + { + try + { + var encodedBaseKey = licenseKey.Substring(0, licenseKey.Length - 32); + var hash = licenseKey.Substring(licenseKey.Length - 32, 32); + var computedHash = GenerateHash(encodedBaseKey); + + if (!computedHash.Equals(hash)) + { + throw new InvalidLicenseKeyException(); + } + + var decodedBaseKey = Encoding.UTF8.GetString(Convert.FromBase64String(encodedBaseKey)); + var baseKeyParts = decodedBaseKey.Split('-'); + + var expiryDate = DateTime.ParseExact(baseKeyParts[4], "yyyyMMdd", CultureInfo.InvariantCulture); + var numberOfUsers = int.Parse(baseKeyParts[3]); + + var sku = (StockKeepingUnits) Enum.Parse(typeof(StockKeepingUnits), baseKeyParts[0]); + var autoRenews = "1".Equals(baseKeyParts[2]); + + return new SubscriptionInfo() + { + SKU = sku, + CustomerId = baseKeyParts[1], + NumberOfUsers = numberOfUsers, + EndDate = expiryDate, + AutoRenews = autoRenews + }; + } + catch + { + throw new InvalidLicenseKeyException(); + } + } + + internal static void SetLicense(string licenseKey) + { + lock (_subscriptionInfoLock) + { + if (_subscriptionInfo == null) + { + _subscriptionInfo = SubscriptionManager.GetSubscriptionInfoFromKey(licenseKey); + } + } + } + + internal static void SetSubscriptionUsageStoreProvider(ISubscriptionStorageProvider subscriptionStorageProvider, IUserReader userReader) + { + lock (_subscriptionUsageLock) + { + if (_subscriptionUsage == null) + { + var usageManager = new SubscriptionUsageManager(); + _subscriptionUsage = usageManager.ReadAndUpdateUsage(subscriptionStorageProvider, userReader); + } + } + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs index 106bc8b6..b8687ded 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs @@ -1,21 +1,28 @@ using System; using System.Collections.Generic; +using FakeXrmEasy.Abstractions.CommercialLicense; namespace FakeXrmEasy.Core.CommercialLicense { /// /// Contains info about the current subscription usage /// - internal class SubscriptionUsage + internal class SubscriptionUsage: ISubscriptionUsage { /// /// The last time the current subscription usage was checked /// - internal DateTime LastTimeChecked { get; set; } + public DateTime LastTimeChecked { get; set; } /// /// Information about all the users /// - internal IEnumerable Users { get; set; } + public ICollection Users { get; set; } + + internal SubscriptionUsage() + { + Users = new List(); + LastTimeChecked = DateTime.UtcNow; + } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs new file mode 100644 index 00000000..61fbd0af --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using FakeXrmEasy.Abstractions.CommercialLicense; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + internal class SubscriptionUsageManager + { + internal ISubscriptionUsage _subscriptionUsage; + + internal ISubscriptionUsage ReadAndUpdateUsage(ISubscriptionStorageProvider subscriptionStorageProvider, + IUserReader userReader) + { + _subscriptionUsage = subscriptionStorageProvider.Read(); + if (_subscriptionUsage == null) + { + _subscriptionUsage = new SubscriptionUsage(); + } + + var currentUserName = userReader.GetCurrentUserName(); + + var existingUser = _subscriptionUsage + .Users + .FirstOrDefault(user => currentUserName.Equals(user.UserName)); + + if (existingUser == null) + { + _subscriptionUsage.Users.Add(new SubscriptionUserInfo() + { + UserName = currentUserName, + LastTimeUsed = DateTime.UtcNow + }); + } + else + { + existingUser.LastTimeUsed = DateTime.UtcNow; + } + + subscriptionStorageProvider.Write(_subscriptionUsage); + + return _subscriptionUsage; + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs index af60e9c3..5d999af1 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUserInfo.cs @@ -1,25 +1,21 @@ using System; +using FakeXrmEasy.Abstractions.CommercialLicense; namespace FakeXrmEasy.Core.CommercialLicense { /// /// Info about the last time a given user used FakeXrmEasy /// - internal class SubscriptionUserInfo + internal class SubscriptionUserInfo: ISubscriptionUserInfo { /// /// The last time this user used FakeXrmEasy /// - internal DateTime LastTimeUsed { get; set; } + public DateTime LastTimeUsed { get; set; } /// /// The user's username /// - internal string UserName { get; set; } - - /// - /// This user that runs as part of a CI process (Continuous Integration, i.e. either a build or release pipeline) - /// - internal bool IsCI { get; set; } + public string UserName { get; set; } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs index 596fe8ce..c1afeafb 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs @@ -1,5 +1,6 @@ using System; using System.Linq; +using FakeXrmEasy.Abstractions.CommercialLicense; using FakeXrmEasy.Core.CommercialLicense.Exceptions; namespace FakeXrmEasy.Core.CommercialLicense @@ -10,22 +11,19 @@ namespace FakeXrmEasy.Core.CommercialLicense internal sealed class SubscriptionValidator { private readonly IEnvironmentReader _environmentReader; - - internal SubscriptionValidator(IEnvironmentReader environmentReader) + private readonly ISubscriptionInfo _subscriptionInfo; + private readonly ISubscriptionUsage _subscriptionUsage; + + internal SubscriptionValidator( + IEnvironmentReader environmentReader, + ISubscriptionInfo subscriptionInfo, + ISubscriptionUsage subscriptionUsage) { _environmentReader = environmentReader; + _subscriptionInfo = subscriptionInfo; + _subscriptionUsage = subscriptionUsage; } - /// - /// The current subscription plan - /// - internal SubscriptionInfo SubscriptionPlan { get; set; } - - /// - /// The current usage of the subscription - /// - internal SubscriptionUsage CurrentUsage { get; set; } - /// /// Validates if the current usage is within the subscription limits /// @@ -55,19 +53,17 @@ internal bool IsValid() /// internal bool IsSubscriptionPlanValid() { - if (SubscriptionPlan == null) + if (_subscriptionInfo == null) { throw new NoSubscriptionPlanInfoException(); } - if (SubscriptionPlan.AutoRenews) + if (_subscriptionInfo.AutoRenews) { return true; } - - var expiryDate = SubscriptionPlan.BillingType == SubscriptionBillingCycleType.Annual - ? SubscriptionPlan.StartDate.AddYears(1) - : SubscriptionPlan.StartDate.AddMonths(1); + + var expiryDate = _subscriptionInfo.EndDate; if (expiryDate < DateTime.UtcNow) { @@ -83,18 +79,18 @@ internal bool IsUsageValid() { return true; } - if (CurrentUsage == null) + if (_subscriptionUsage == null) { throw new NoUsageFoundException(); } - var currentNumberOfUsers = CurrentUsage + var currentNumberOfUsers = _subscriptionUsage .Users .Count(userInfo => userInfo.LastTimeUsed >= DateTime.UtcNow.AddMonths(-1)); - if (currentNumberOfUsers > SubscriptionPlan.NumberOfUsers) + if (currentNumberOfUsers > _subscriptionInfo.NumberOfUsers) { - throw new ConsiderUpgradingPlanException(currentNumberOfUsers, SubscriptionPlan.NumberOfUsers); + throw new ConsiderUpgradingPlanException(currentNumberOfUsers, _subscriptionInfo.NumberOfUsers); } return true; } @@ -102,7 +98,7 @@ internal bool IsUsageValid() private bool IsRunningInContinuousIntegration() { return "1".Equals(_environmentReader.GetEnvironmentVariable("FAKE_XRM_EASY_CI")) - || "True".Equals(_environmentReader.GetEnvironmentVariable(("TF_BUILD"))); + || "True".Equals(_environmentReader.GetEnvironmentVariable("TF_BUILD")); } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs b/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs new file mode 100644 index 00000000..57ac2b96 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs @@ -0,0 +1,22 @@ +namespace FakeXrmEasy.Core.CommercialLicense +{ + /// + /// Returns info about the current user + /// + public interface IUserReader + { + /// + /// Gets the current username + /// + /// + string GetCurrentUserName(); + } + + internal class UserReader: IUserReader + { + public string GetCurrentUserName() + { + return System.Security.Principal.WindowsIdentity.GetCurrent().Name; + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj b/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj index 3e8e07da..e901975f 100644 --- a/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj +++ b/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj @@ -8,7 +8,7 @@ net452 net452 FakeXrmEasy.Core - 2.3.3 + 2.4.0 Jordi Montaña Dynamics Value FakeXrmEasy Core @@ -102,22 +102,22 @@ - + - + - + - + - + - + diff --git a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs index 58376c0b..f26b3939 100644 --- a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs +++ b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs @@ -6,9 +6,11 @@ using System.Linq; using Microsoft.Xrm.Sdk; using FakeItEasy; +using FakeXrmEasy.Abstractions.CommercialLicense; using FakeXrmEasy.Abstractions.Integrity; using FakeXrmEasy.Abstractions.Enums; using FakeXrmEasy.Abstractions.Exceptions; +using FakeXrmEasy.Core.CommercialLicense; using FakeXrmEasy.Core.Exceptions; namespace FakeXrmEasy.Middleware @@ -87,6 +89,15 @@ public IXrmFakedContext Build() throw new LicenseException("Please, you need to choose a FakeXrmEasy license. More info at https://dynamicsvalue.github.io/fake-xrm-easy-docs/licensing/licensing-exception/"); } + if (_context.LicenseContext == FakeXrmEasyLicense.Commercial) + { + var subscriptionInfo = SubscriptionManager._subscriptionInfo; + if (subscriptionInfo != null) + { + + } + } + OrganizationRequestDelegate app = (context, request) => { //return default PullRequestException at the end of the pipeline @@ -116,5 +127,27 @@ public IMiddlewareBuilder SetLicense(FakeXrmEasyLicense license) _context.LicenseContext = license; return this; } + + /// + /// Sets the current subscription license key + /// + /// the license key that was provided to you + /// + public IMiddlewareBuilder SetLicenseKey(string licenseKey) + { + SubscriptionManager.SetLicense(licenseKey); + return this; + } + + /// + /// Sets the subscription storage provider that will be used to read / write subscription usage data + /// + /// + /// + public IMiddlewareBuilder SetSubscriptionUsageStorage(ISubscriptionStorageProvider storageProvider) + { + SubscriptionManager.SetSubscriptionUsageStoreProvider(storageProvider, new UserReader()); + return this; + } } } diff --git a/src/FakeXrmEasy.Core/XrmFakedContext.cs b/src/FakeXrmEasy.Core/XrmFakedContext.cs index 2cf94404..0e61a5e4 100644 --- a/src/FakeXrmEasy.Core/XrmFakedContext.cs +++ b/src/FakeXrmEasy.Core/XrmFakedContext.cs @@ -20,6 +20,8 @@ using System.Reflection; using System.Runtime.CompilerServices; +using FakeXrmEasy.Abstractions.CommercialLicense; + [assembly: InternalsVisibleTo("FakeXrmEasy.Core.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c124cb50761165a765adf6078bde555a7c5a2b692ed6e6ec9df0bd7d20da69170bae9bf95e874fa50995cc080af404ccad36515fa509c4ea6599a0502c1642db254a293e023c47c79ce69889c6ba921d124d896d87f0baaa9ea1d87b28589ffbe7b08492606bacef19dc4bc4cefb0d525be63ee722b02dc8c79688a7a8f623a2")] namespace FakeXrmEasy diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionManagerTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionManagerTests.cs new file mode 100644 index 00000000..171e3eab --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionManagerTests.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using FakeXrmEasy.Abstractions.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public class SubscriptionManagerTests + { + + + + + + + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs new file mode 100644 index 00000000..ef12ae06 --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using FakeItEasy; +using FakeXrmEasy.Abstractions.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public class SubscriptionUsageManagerTests + { + private readonly SubscriptionUsageManager _usageManager; + private readonly ISubscriptionStorageProvider _subscriptionStorageProvider; + private readonly IUserReader _userReader; + private const string cUserName = "CurrentDomain\\CurrentUser"; + + public SubscriptionUsageManagerTests() + { + _subscriptionStorageProvider = A.Fake(); + _userReader = A.Fake(); + A.CallTo(() => _userReader.GetCurrentUserName()).ReturnsLazily(() => cUserName); + _usageManager = new SubscriptionUsageManager(); + } + + [Fact] + public void Should_initialize_new_subscription_usage_data_if_the_provider_returns_null_and_add_current_user() + { + A.CallTo(() => _subscriptionStorageProvider.Read()).ReturnsLazily(() => null); + + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionStorageProvider, _userReader); + + Assert.NotNull(usage); + Assert.Single(usage.Users); + + var userInfo = usage.Users.First(); + Assert.Equal(cUserName, userInfo.UserName); + Assert.True(userInfo.LastTimeUsed > DateTime.UtcNow.AddDays(-1)); + + A.CallTo(() => _subscriptionStorageProvider.Write(usage)) + .MustHaveHappened(); + } + + [Fact] + public void Should_update_last_used_date_if_current_user_already_exists() + { + A.CallTo(() => _subscriptionStorageProvider.Read()).ReturnsLazily(() => new SubscriptionUsage() + { + Users = new List() + { + new SubscriptionUserInfo() + { + UserName = cUserName, + LastTimeUsed = DateTime.UtcNow.AddMonths(-2) + } + } + }); + + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionStorageProvider, _userReader); + + Assert.NotNull(usage); + Assert.Single(usage.Users); + + var userInfo = usage.Users.First(); + Assert.Equal(cUserName, userInfo.UserName); + Assert.True(userInfo.LastTimeUsed > DateTime.UtcNow.AddDays(-1)); + + A.CallTo(() => _subscriptionStorageProvider.Write(usage)) + .MustHaveHappened(); + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs index 1e0e94a5..7e0da145 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs @@ -1,4 +1,6 @@ using System; +using FakeItEasy; +using FakeXrmEasy.Abstractions.CommercialLicense; using FakeXrmEasy.Core.CommercialLicense; using FakeXrmEasy.Core.CommercialLicense.Exceptions; using Xunit; @@ -8,71 +10,88 @@ namespace FakeXrmEasy.Core.Tests.CommercialLicense public partial class SubscriptionValidatorTests { private readonly IEnvironmentReader _defaultEnvironmentReader; - private readonly SubscriptionValidator _subscriptionValidator; + private ISubscriptionInfo _subscriptionInfo; + private SubscriptionValidator _subscriptionValidator; + public SubscriptionValidatorTests() { _defaultEnvironmentReader = new FakeEnvironmentReader(); - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader); } + [Fact] public void Should_return_error_if_current_subscription_is_unknown() { + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] public void Should_return_subscription_expired_exception_if_monthly_expired() { - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); - _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Monthly; - _subscriptionValidator.SubscriptionPlan.AutoRenews = false; - + _subscriptionInfo = new SubscriptionInfo + { + StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), + BillingType = SubscriptionBillingCycleType.Monthly, + AutoRenews = false + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] public void Should_not_return_subscription_expired_exception_if_monthly_expired_but_autorenew_is_enabled() { - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); - _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Monthly; - _subscriptionValidator.SubscriptionPlan.AutoRenews = true; + _subscriptionInfo = new SubscriptionInfo + { + StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), + BillingType = SubscriptionBillingCycleType.Monthly, + AutoRenews = true + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] public void Should_return_subscription_expired_exception_if_annual_expired() { - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1); - _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Annual; - _subscriptionValidator.SubscriptionPlan.AutoRenews = false; + _subscriptionInfo = new SubscriptionInfo + { + StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1), + BillingType = SubscriptionBillingCycleType.Annual, + AutoRenews = false + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] public void Should_not_return_subscription_expired_exception_if_annual_expired_but_autorenew_is_enabled() { - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1); - _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.Annual; - _subscriptionValidator.SubscriptionPlan.AutoRenews = true; + _subscriptionInfo = new SubscriptionInfo + { + StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1), + BillingType = SubscriptionBillingCycleType.Annual, + AutoRenews = true + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] public void Should_return_subscription_expired_exception_if_prepaid_expired() { - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1); - _subscriptionValidator.SubscriptionPlan.BillingType = SubscriptionBillingCycleType.PrePaid; - _subscriptionValidator.SubscriptionPlan.AutoRenews = false; + _subscriptionInfo = new SubscriptionInfo + { + StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), + BillingType = SubscriptionBillingCycleType.PrePaid, + AutoRenews = false + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } } diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs index 65cfb325..0cdcb46e 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs @@ -1,4 +1,5 @@ using System; +using FakeXrmEasy.Abstractions.CommercialLicense; using FakeXrmEasy.Core.CommercialLicense; using FakeXrmEasy.Core.CommercialLicense.Exceptions; using Xunit; @@ -7,17 +8,19 @@ namespace FakeXrmEasy.Core.Tests.CommercialLicense { public partial class SubscriptionValidatorTests { + private ISubscriptionUsage _subscriptionUsage; + [Fact] public void Should_return_no_usage_found_exception() { - _subscriptionValidator.CurrentUsage = null; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null); Assert.Throws(() => _subscriptionValidator.IsUsageValid()); } [Fact] public void Should_return_consider_upgrading_exception_if_the_number_of_users_exceeds_the_current_subscription() { - _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + _subscriptionUsage = new SubscriptionUsage() //3 valid users { Users = new SubscriptionUserInfo[] { @@ -26,16 +29,19 @@ public void Should_return_consider_upgrading_exception_if_the_number_of_users_ex new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, } }; - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.NumberOfUsers = 2; + _subscriptionInfo = new SubscriptionInfo() + { + NumberOfUsers = 2 + }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); Assert.Throws(() => _subscriptionValidator.IsUsageValid()); } [Fact] public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_month() { - _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + _subscriptionUsage = new SubscriptionUsage() //3 valid users { Users = new SubscriptionUserInfo[] { @@ -44,16 +50,19 @@ public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_ new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, } }; - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo(); - _subscriptionValidator.SubscriptionPlan.NumberOfUsers = 2; - + _subscriptionInfo = new SubscriptionInfo() + { + NumberOfUsers = 2 + }; + + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); Assert.True(_subscriptionValidator.IsUsageValid()); } [Fact] public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() { - _subscriptionValidator.CurrentUsage = new SubscriptionUsage() //3 valid users + _subscriptionUsage = new SubscriptionUsage() //3 existing valid users { Users = new SubscriptionUserInfo[] { @@ -63,14 +72,38 @@ public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() } }; - _subscriptionValidator.SubscriptionPlan = new SubscriptionInfo + _subscriptionInfo = new SubscriptionInfo { NumberOfUsers = 3 }; + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); Assert.True(_subscriptionValidator.IsUsageValid()); } + [Fact] + public void Should_add_current_user_to_usage_if_not_already_there_and_write_back() + { + _subscriptionUsage = new SubscriptionUsage() //3 valid users + { + Users = new SubscriptionUserInfo[] + { + new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, + } + }; + + _subscriptionInfo = new SubscriptionInfo + { + NumberOfUsers = 4 + }; + + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); + Assert.True(_subscriptionValidator.IsUsageValid()); + + } + [Theory] [InlineData("FAKE_XRM_EASY_CI", "1")] [InlineData("TF_BUILD", "True")] @@ -79,12 +112,8 @@ public void Should_ignore_usage_if_running_inside_ci(string envVariableName, str var continuousIntegrationEnvironmentReader = new FakeEnvironmentReader(); continuousIntegrationEnvironmentReader.SetEnvironmentVariable(envVariableName, envVariableValue); - var currentSubscription = new SubscriptionValidator(continuousIntegrationEnvironmentReader) - { - CurrentUsage = null - }; - - Assert.True(currentSubscription.IsUsageValid()); + _subscriptionValidator = new SubscriptionValidator(continuousIntegrationEnvironmentReader, null, _subscriptionUsage); + Assert.True(_subscriptionValidator.IsUsageValid()); } } } \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj b/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj index 16bf17b6..53170db7 100644 --- a/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj +++ b/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj @@ -11,7 +11,7 @@ true FakeXrmEasy.CoreTests - 2.3.3 + 2.4.0 Jordi Montaña Dynamics Value S.L. Internal Unit test suite for FakeXrmEasy.Core package @@ -114,22 +114,22 @@ - + - + - + - + - + - + @@ -137,22 +137,22 @@ - + - + - + - + - + - + From 6fa4bf919aee929f3600281a359773761d80d1d6 Mon Sep 17 00:00:00 2001 From: Jordi Date: Sun, 28 Jan 2024 23:35:26 +0100 Subject: [PATCH 04/10] Updates to subscription usage and validation --- .../RenewalRequestExpiredException.cs | 19 ++++++ .../UpgradeRequestExpiredException.cs | 20 ++++++ .../CommercialLicense/SubscriptionManager.cs | 21 ++++-- .../SubscriptionUpgradeRequest.cs | 11 +++ .../CommercialLicense/SubscriptionUsage.cs | 6 ++ .../SubscriptionUsageManager.cs | 18 ++++- .../SubscriptionValidator.cs | 32 +++++++-- .../Middleware/MiddlewareBuilder.cs | 29 ++++---- .../SubscriptionUsageManagerTests.cs | 67 ++++++++++++++++++- .../SubscriptionValidatorTests.PlanInfo.cs | 50 +++++++------- .../SubscriptionValidatorTests.Usage.cs | 66 +++++++++++++----- 11 files changed, 264 insertions(+), 75 deletions(-) create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/RenewalRequestExpiredException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/Exceptions/UpgradeRequestExpiredException.cs create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUpgradeRequest.cs diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/RenewalRequestExpiredException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/RenewalRequestExpiredException.cs new file mode 100644 index 00000000..97c360d8 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/RenewalRequestExpiredException.cs @@ -0,0 +1,19 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Exception raised when your current subscription expired and you exceeded the allowed renewal time window + /// + public class RenewalRequestExpiredException: Exception + { + /// + /// Throws an exception where the current subscription expired + /// + /// + public RenewalRequestExpiredException(DateTime expiredOn) : base($"The current subscription expired on '{expiredOn.ToLongDateString()}' and a renewal license was not applied on time. Please request a new subscription license.") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/UpgradeRequestExpiredException.cs b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/UpgradeRequestExpiredException.cs new file mode 100644 index 00000000..dffc10a3 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/Exceptions/UpgradeRequestExpiredException.cs @@ -0,0 +1,20 @@ +using System; + +namespace FakeXrmEasy.Core.CommercialLicense.Exceptions +{ + /// + /// Exception raised when the grace period for requesting an upgrade has expired + /// + public class UpgradeRequestExpiredException: Exception + { + /// + /// Default constructor + /// + /// + public UpgradeRequestExpiredException(DateTime firstRequested) : + base($"You requested a subscription upgrade on '{firstRequested.ToShortDateString()}', however, the new subscription details or upgrade progress has not been completed within the allowed upgrade window. Please contact your line manager and raise a support ticket") + { + + } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs index 111e654e..cecc00da 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs @@ -14,7 +14,10 @@ internal static class SubscriptionManager internal static ISubscriptionUsage _subscriptionUsage; internal static readonly object _subscriptionUsageLock = new object(); - + + internal static bool _renewalRequested = false; + internal static bool _upgradeRequested = false; + private static string GenerateHash(string input) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) @@ -68,25 +71,33 @@ private static ISubscriptionInfo GetSubscriptionInfoFromKey(string licenseKey) } } - internal static void SetLicense(string licenseKey) + private static void SetLicenseKey(string licenseKey) { lock (_subscriptionInfoLock) { if (_subscriptionInfo == null) { - _subscriptionInfo = SubscriptionManager.GetSubscriptionInfoFromKey(licenseKey); + _subscriptionInfo = GetSubscriptionInfoFromKey(licenseKey); } } } - internal static void SetSubscriptionUsageStoreProvider(ISubscriptionStorageProvider subscriptionStorageProvider, IUserReader userReader) + internal static void SetSubscriptionStorageProvider(ISubscriptionStorageProvider subscriptionStorageProvider, + IUserReader userReader, + bool upgradeRequested, + bool renewalRequested) { + SetLicenseKey(subscriptionStorageProvider.GetLicenseKey()); + lock (_subscriptionUsageLock) { if (_subscriptionUsage == null) { + _upgradeRequested = upgradeRequested; + _renewalRequested = renewalRequested; + var usageManager = new SubscriptionUsageManager(); - _subscriptionUsage = usageManager.ReadAndUpdateUsage(subscriptionStorageProvider, userReader); + _subscriptionUsage = usageManager.ReadAndUpdateUsage(_subscriptionInfo, subscriptionStorageProvider, userReader, _upgradeRequested); } } } diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUpgradeRequest.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUpgradeRequest.cs new file mode 100644 index 00000000..1082a57e --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUpgradeRequest.cs @@ -0,0 +1,11 @@ +using System; +using FakeXrmEasy.Abstractions.CommercialLicense; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + internal class SubscriptionUpgradeRequest: ISubscriptionUpgradeRequest + { + public DateTime FirstRequestDate { get; set; } + public long PreviousNumberOfUsers { get; set; } + } +} \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs index b8687ded..b9c4df98 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsage.cs @@ -19,10 +19,16 @@ internal class SubscriptionUsage: ISubscriptionUsage /// public ICollection Users { get; set; } + /// + /// Contains info for a requested upgrade + /// + public ISubscriptionUpgradeRequest UpgradeInfo { get; set; } + internal SubscriptionUsage() { Users = new List(); LastTimeChecked = DateTime.UtcNow; + UpgradeInfo = null; } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs index 61fbd0af..f86c68c3 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionUsageManager.cs @@ -8,8 +8,11 @@ internal class SubscriptionUsageManager { internal ISubscriptionUsage _subscriptionUsage; - internal ISubscriptionUsage ReadAndUpdateUsage(ISubscriptionStorageProvider subscriptionStorageProvider, - IUserReader userReader) + internal ISubscriptionUsage ReadAndUpdateUsage( + ISubscriptionInfo subscriptionInfo, + ISubscriptionStorageProvider subscriptionStorageProvider, + IUserReader userReader, + bool upgradeRequested) { _subscriptionUsage = subscriptionStorageProvider.Read(); if (_subscriptionUsage == null) @@ -35,7 +38,16 @@ internal ISubscriptionUsage ReadAndUpdateUsage(ISubscriptionStorageProvider subs { existingUser.LastTimeUsed = DateTime.UtcNow; } - + + if (upgradeRequested && _subscriptionUsage.UpgradeInfo == null) + { + _subscriptionUsage.UpgradeInfo = new SubscriptionUpgradeRequest() + { + FirstRequestDate = DateTime.UtcNow, + PreviousNumberOfUsers = subscriptionInfo.NumberOfUsers + }; + } + subscriptionStorageProvider.Write(_subscriptionUsage); return _subscriptionUsage; diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs index c1afeafb..220c97a4 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionValidator.cs @@ -13,15 +13,18 @@ internal sealed class SubscriptionValidator private readonly IEnvironmentReader _environmentReader; private readonly ISubscriptionInfo _subscriptionInfo; private readonly ISubscriptionUsage _subscriptionUsage; - + private readonly bool _renewalRequested; + internal SubscriptionValidator( IEnvironmentReader environmentReader, ISubscriptionInfo subscriptionInfo, - ISubscriptionUsage subscriptionUsage) + ISubscriptionUsage subscriptionUsage, + bool renewalRequested) { _environmentReader = environmentReader; _subscriptionInfo = subscriptionInfo; _subscriptionUsage = subscriptionUsage; + _renewalRequested = renewalRequested; } /// @@ -67,7 +70,17 @@ internal bool IsSubscriptionPlanValid() if (expiryDate < DateTime.UtcNow) { - throw new SubscriptionExpiredException(expiryDate); + if (!_renewalRequested) + { + throw new SubscriptionExpiredException(expiryDate); + } + else + { + if (expiryDate.AddMonths(1) < DateTime.UtcNow) + { + throw new RenewalRequestExpiredException(expiryDate); + } + } } return true; @@ -90,7 +103,18 @@ internal bool IsUsageValid() if (currentNumberOfUsers > _subscriptionInfo.NumberOfUsers) { - throw new ConsiderUpgradingPlanException(currentNumberOfUsers, _subscriptionInfo.NumberOfUsers); + if (_subscriptionUsage.UpgradeInfo == null) + { + throw new ConsiderUpgradingPlanException(currentNumberOfUsers, _subscriptionInfo.NumberOfUsers); + } + else + { + if (_subscriptionUsage.UpgradeInfo.FirstRequestDate <= DateTime.UtcNow.AddMonths(-1)) + { + throw new UpgradeRequestExpiredException(_subscriptionUsage.UpgradeInfo.FirstRequestDate); + } + } + } return true; } diff --git a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs index f26b3939..87c7f89f 100644 --- a/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs +++ b/src/FakeXrmEasy.Core/Middleware/MiddlewareBuilder.cs @@ -94,7 +94,13 @@ public IXrmFakedContext Build() var subscriptionInfo = SubscriptionManager._subscriptionInfo; if (subscriptionInfo != null) { - + var subscriptionValidator = new SubscriptionValidator( + new EnvironmentReader(), + SubscriptionManager._subscriptionInfo, + SubscriptionManager._subscriptionUsage, + SubscriptionManager._renewalRequested); + + subscriptionValidator.IsValid(); } } @@ -129,24 +135,15 @@ public IMiddlewareBuilder SetLicense(FakeXrmEasyLicense license) } /// - /// Sets the current subscription license key - /// - /// the license key that was provided to you - /// - public IMiddlewareBuilder SetLicenseKey(string licenseKey) - { - SubscriptionManager.SetLicense(licenseKey); - return this; - } - - /// - /// Sets the subscription storage provider that will be used to read / write subscription usage data + /// Use this method to provide an implementation for a subscription storage provider when you are using a commercial license and have a license key /// - /// + /// An implementation of a ISubscriptionStorageProvider that is capable of reading and writing subscription usage data as well as your license key + /// Set to true if you exceeded the number of users that your current subscription allows and you have already requested an upgrade to DynamicsValue via your organisation's established process + /// Set to true if your subscription expired and you have already requested an renewal to DynamicsValue via your organisation's established process /// - public IMiddlewareBuilder SetSubscriptionUsageStorage(ISubscriptionStorageProvider storageProvider) + public IMiddlewareBuilder SetSubscriptionStorageProvider(ISubscriptionStorageProvider storageProvider, bool upgradeRequested = false, bool renewalRequested = false) { - SubscriptionManager.SetSubscriptionUsageStoreProvider(storageProvider, new UserReader()); + SubscriptionManager.SetSubscriptionStorageProvider(storageProvider, new UserReader(), upgradeRequested, renewalRequested); return this; } } diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs index ef12ae06..3646336a 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionUsageManagerTests.cs @@ -14,6 +14,7 @@ public class SubscriptionUsageManagerTests private readonly ISubscriptionStorageProvider _subscriptionStorageProvider; private readonly IUserReader _userReader; private const string cUserName = "CurrentDomain\\CurrentUser"; + private readonly ISubscriptionInfo _subscriptionInfo; public SubscriptionUsageManagerTests() { @@ -21,6 +22,10 @@ public SubscriptionUsageManagerTests() _userReader = A.Fake(); A.CallTo(() => _userReader.GetCurrentUserName()).ReturnsLazily(() => cUserName); _usageManager = new SubscriptionUsageManager(); + _subscriptionInfo = new SubscriptionInfo() + { + NumberOfUsers = 10 + }; } [Fact] @@ -28,7 +33,7 @@ public void Should_initialize_new_subscription_usage_data_if_the_provider_return { A.CallTo(() => _subscriptionStorageProvider.Read()).ReturnsLazily(() => null); - var usage = _usageManager.ReadAndUpdateUsage(_subscriptionStorageProvider, _userReader); + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionInfo, _subscriptionStorageProvider, _userReader, false); Assert.NotNull(usage); Assert.Single(usage.Users); @@ -56,7 +61,25 @@ public void Should_update_last_used_date_if_current_user_already_exists() } }); - var usage = _usageManager.ReadAndUpdateUsage(_subscriptionStorageProvider, _userReader); + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionInfo, _subscriptionStorageProvider, _userReader, false); + + Assert.NotNull(usage); + Assert.Single(usage.Users); + + var userInfo = usage.Users.First(); + Assert.Equal(cUserName, userInfo.UserName); + Assert.True(userInfo.LastTimeUsed > DateTime.UtcNow.AddDays(-1)); + + A.CallTo(() => _subscriptionStorageProvider.Write(usage)) + .MustHaveHappened(); + } + + [Fact] + public void Should_add_upgrade_requested_info_upgrade_requested_and_no_previous_upgrade_info_existed() + { + A.CallTo(() => _subscriptionStorageProvider.Read()).ReturnsLazily(() => null); + + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionInfo, _subscriptionStorageProvider, _userReader, true); Assert.NotNull(usage); Assert.Single(usage.Users); @@ -64,6 +87,46 @@ public void Should_update_last_used_date_if_current_user_already_exists() var userInfo = usage.Users.First(); Assert.Equal(cUserName, userInfo.UserName); Assert.True(userInfo.LastTimeUsed > DateTime.UtcNow.AddDays(-1)); + + var upgradeInfo = usage.UpgradeInfo; + Assert.NotNull(upgradeInfo); + Assert.Equal(_subscriptionInfo.NumberOfUsers, upgradeInfo.PreviousNumberOfUsers); + + A.CallTo(() => _subscriptionStorageProvider.Write(usage)) + .MustHaveHappened(); + } + + [Fact] + public void Should_not_update_upgrade_info_if_upgrade_info_existed_previously() + { + var upgradeDate = DateTime.UtcNow.AddMonths(-1); + + A.CallTo(() => _subscriptionStorageProvider.Read()).ReturnsLazily(() => new SubscriptionUsage() + { + UpgradeInfo = new SubscriptionUpgradeRequest() + { + FirstRequestDate = upgradeDate, + PreviousNumberOfUsers = _subscriptionInfo.NumberOfUsers + }, + Users = new List() + { + new SubscriptionUserInfo() + { + UserName = cUserName, + LastTimeUsed = DateTime.UtcNow.AddMonths(-2) + } + } + }); + + var usage = _usageManager.ReadAndUpdateUsage(_subscriptionInfo, _subscriptionStorageProvider, _userReader, true); + + Assert.NotNull(usage); + Assert.Single(usage.Users); + + var upgradeInfo = usage.UpgradeInfo; + Assert.NotNull(upgradeInfo); + Assert.Equal(_subscriptionInfo.NumberOfUsers, upgradeInfo.PreviousNumberOfUsers); + Assert.Equal(upgradeDate, upgradeInfo.FirstRequestDate); A.CallTo(() => _subscriptionStorageProvider.Write(usage)) .MustHaveHappened(); diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs index 7e0da145..ffecb287 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.PlanInfo.cs @@ -12,7 +12,7 @@ public partial class SubscriptionValidatorTests private readonly IEnvironmentReader _defaultEnvironmentReader; private ISubscriptionInfo _subscriptionInfo; private SubscriptionValidator _subscriptionValidator; - + public SubscriptionValidatorTests() { @@ -22,77 +22,73 @@ public SubscriptionValidatorTests() [Fact] public void Should_return_error_if_current_subscription_is_unknown() { - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null, false); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } - + [Fact] - public void Should_return_subscription_expired_exception_if_monthly_expired() + public void Should_not_return_subscription_expired_if_still_valid() { _subscriptionInfo = new SubscriptionInfo { - StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), - BillingType = SubscriptionBillingCycleType.Monthly, - AutoRenews = false + EndDate = DateTime.UtcNow.AddDays(20), + AutoRenews = true }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); - Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null, false); + Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] - public void Should_not_return_subscription_expired_exception_if_monthly_expired_but_autorenew_is_enabled() + public void Should_not_return_subscription_expired_exception_if_expired_but_autorenew_is_enabled() { _subscriptionInfo = new SubscriptionInfo { - StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), - BillingType = SubscriptionBillingCycleType.Monthly, + EndDate = DateTime.UtcNow.AddDays(-1), AutoRenews = true }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null, false); Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] - public void Should_return_subscription_expired_exception_if_annual_expired() + public void Should_return_subscription_expired_exception_if_expired_with_no_autorenewal() { _subscriptionInfo = new SubscriptionInfo { - StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1), - BillingType = SubscriptionBillingCycleType.Annual, + EndDate = DateTime.UtcNow.AddDays(-1), AutoRenews = false }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null, false); Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] - public void Should_not_return_subscription_expired_exception_if_annual_expired_but_autorenew_is_enabled() + public void Should_not_return_subscription_expired_exception_if_expired_but_a_renewal_was_requested_within_a_valid_time_frame() { _subscriptionInfo = new SubscriptionInfo { - StartDate = DateTime.UtcNow.AddYears(-1).AddDays(-1), - BillingType = SubscriptionBillingCycleType.Annual, - AutoRenews = true + EndDate = DateTime.UtcNow.AddDays(-15), + AutoRenews = false }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null, true); Assert.True(_subscriptionValidator.IsSubscriptionPlanValid()); } [Fact] - public void Should_return_subscription_expired_exception_if_prepaid_expired() + public void Should_return_renewal_request_expired_exception_if_expired_and_exceeded_the_valid_time_frame_for_renewal() { _subscriptionInfo = new SubscriptionInfo { - StartDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), - BillingType = SubscriptionBillingCycleType.PrePaid, + EndDate = DateTime.UtcNow.AddMonths(-1).AddDays(-1), AutoRenews = false }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null); - Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, null, true); + Assert.Throws(() => _subscriptionValidator.IsSubscriptionPlanValid()); } } } \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs index 0cdcb46e..7fb67012 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionValidatorTests.Usage.cs @@ -13,7 +13,7 @@ public partial class SubscriptionValidatorTests [Fact] public void Should_return_no_usage_found_exception() { - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, null, null, false); Assert.Throws(() => _subscriptionValidator.IsUsageValid()); } @@ -34,19 +34,24 @@ public void Should_return_consider_upgrading_exception_if_the_number_of_users_ex NumberOfUsers = 2 }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage, false); Assert.Throws(() => _subscriptionValidator.IsUsageValid()); } [Fact] - public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_month() + public void Should_not_return_consider_upgrading_exception_if_the_number_of_users_exceeds_the_current_subscription_and_upgrade_was_requested_within_30days() { _subscriptionUsage = new SubscriptionUsage() //3 valid users { + UpgradeInfo = new SubscriptionUpgradeRequest() + { + FirstRequestDate = DateTime.UtcNow.AddMonths(-1).AddDays(1), + PreviousNumberOfUsers = 2 + }, Users = new SubscriptionUserInfo[] { new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, - new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddMonths(-1).AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddDays(-10) }, new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, } }; @@ -54,16 +59,21 @@ public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_ { NumberOfUsers = 2 }; - - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); + + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage, false); Assert.True(_subscriptionValidator.IsUsageValid()); } [Fact] - public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() + public void Should_return_upgrade_request_expired_exception_if_the_number_of_users_exceeds_the_current_subscription_and_upgrade_was_requested_but_took_longer_thab_30days() { - _subscriptionUsage = new SubscriptionUsage() //3 existing valid users + _subscriptionUsage = new SubscriptionUsage() //3 valid users { + UpgradeInfo = new SubscriptionUpgradeRequest() + { + FirstRequestDate = DateTime.UtcNow.AddMonths(-1).AddDays(-3), + PreviousNumberOfUsers = 2 + }, Users = new SubscriptionUserInfo[] { new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, @@ -71,20 +81,41 @@ public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, } }; - - _subscriptionInfo = new SubscriptionInfo + _subscriptionInfo = new SubscriptionInfo() { - NumberOfUsers = 3 + NumberOfUsers = 2 }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); - Assert.True(_subscriptionValidator.IsUsageValid()); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage, false); + Assert.Throws(() => _subscriptionValidator.IsUsageValid()); } + [Fact] - public void Should_add_current_user_to_usage_if_not_already_there_and_write_back() + public void Should_not_count_users_where_the_last_time_used_is_greater_than_one_month() { _subscriptionUsage = new SubscriptionUsage() //3 valid users + { + Users = new SubscriptionUserInfo[] + { + new SubscriptionUserInfo() { UserName = "user1", LastTimeUsed = DateTime.UtcNow.AddDays(-1) }, + new SubscriptionUserInfo() { UserName = "user2", LastTimeUsed = DateTime.UtcNow.AddMonths(-1).AddDays(-10) }, + new SubscriptionUserInfo() { UserName = "user3", LastTimeUsed = DateTime.UtcNow.AddDays(-3) }, + } + }; + _subscriptionInfo = new SubscriptionInfo() + { + NumberOfUsers = 2 + }; + + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage, false); + Assert.True(_subscriptionValidator.IsUsageValid()); + } + + [Fact] + public void Should_return_usage_is_valid_if_it_is_within_the_allowed_range() + { + _subscriptionUsage = new SubscriptionUsage() //3 existing valid users { Users = new SubscriptionUserInfo[] { @@ -96,12 +127,11 @@ public void Should_add_current_user_to_usage_if_not_already_there_and_write_back _subscriptionInfo = new SubscriptionInfo { - NumberOfUsers = 4 + NumberOfUsers = 3 }; - _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage); + _subscriptionValidator = new SubscriptionValidator(_defaultEnvironmentReader, _subscriptionInfo, _subscriptionUsage, false); Assert.True(_subscriptionValidator.IsUsageValid()); - } [Theory] @@ -112,7 +142,7 @@ public void Should_ignore_usage_if_running_inside_ci(string envVariableName, str var continuousIntegrationEnvironmentReader = new FakeEnvironmentReader(); continuousIntegrationEnvironmentReader.SetEnvironmentVariable(envVariableName, envVariableValue); - _subscriptionValidator = new SubscriptionValidator(continuousIntegrationEnvironmentReader, null, _subscriptionUsage); + _subscriptionValidator = new SubscriptionValidator(continuousIntegrationEnvironmentReader, null, _subscriptionUsage, false); Assert.True(_subscriptionValidator.IsUsageValid()); } } From f49c5badb7176d19534510ccba19b7d5d34f411b Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 29 Jan 2024 00:12:50 +0100 Subject: [PATCH 05/10] Increase code coverage for subscription logic --- .../CommercialLicense/SubscriptionInfo.cs | 9 --- .../CommercialLicense/SubscriptionManager.cs | 56 +--------------- .../SubscriptionPlanManager.cs | 64 +++++++++++++++++++ .../SubscriptionPlanManagerTests.cs | 25 ++++++++ .../CommercialLicense/UserReaderTests.cs | 21 ++++++ 5 files changed, 112 insertions(+), 63 deletions(-) create mode 100644 src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs create mode 100644 tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs index 605c454e..d237ce0d 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionInfo.cs @@ -42,14 +42,5 @@ internal class SubscriptionInfo: ISubscriptionInfo /// The subscription's end date /// public DateTime EndDate { get; set; } - - /// - /// - /// - /// - internal void FromLicenseKey(string licenseKey) - { - - } } } \ No newline at end of file diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs index cecc00da..00a9a9a2 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionManager.cs @@ -17,67 +17,15 @@ internal static class SubscriptionManager internal static bool _renewalRequested = false; internal static bool _upgradeRequested = false; - - private static string GenerateHash(string input) - { - using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) - { - byte[] inputBytes = Encoding.UTF8.GetBytes(input); - byte[] hashBytes = md5.ComputeHash(inputBytes); - - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < hashBytes.Length; i++) - { - sb.Append(hashBytes[i].ToString("X2")); - } - return sb.ToString(); - } - } - private static ISubscriptionInfo GetSubscriptionInfoFromKey(string licenseKey) - { - try - { - var encodedBaseKey = licenseKey.Substring(0, licenseKey.Length - 32); - var hash = licenseKey.Substring(licenseKey.Length - 32, 32); - var computedHash = GenerateHash(encodedBaseKey); - - if (!computedHash.Equals(hash)) - { - throw new InvalidLicenseKeyException(); - } - - var decodedBaseKey = Encoding.UTF8.GetString(Convert.FromBase64String(encodedBaseKey)); - var baseKeyParts = decodedBaseKey.Split('-'); - - var expiryDate = DateTime.ParseExact(baseKeyParts[4], "yyyyMMdd", CultureInfo.InvariantCulture); - var numberOfUsers = int.Parse(baseKeyParts[3]); - - var sku = (StockKeepingUnits) Enum.Parse(typeof(StockKeepingUnits), baseKeyParts[0]); - var autoRenews = "1".Equals(baseKeyParts[2]); - - return new SubscriptionInfo() - { - SKU = sku, - CustomerId = baseKeyParts[1], - NumberOfUsers = numberOfUsers, - EndDate = expiryDate, - AutoRenews = autoRenews - }; - } - catch - { - throw new InvalidLicenseKeyException(); - } - } - private static void SetLicenseKey(string licenseKey) { lock (_subscriptionInfoLock) { if (_subscriptionInfo == null) { - _subscriptionInfo = GetSubscriptionInfoFromKey(licenseKey); + var subscriptionPlanManager = new SubscriptionPlanManager(); + _subscriptionInfo = subscriptionPlanManager.GetSubscriptionInfoFromKey(licenseKey); } } } diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs new file mode 100644 index 00000000..2af6e3d3 --- /dev/null +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs @@ -0,0 +1,64 @@ +using System; +using System.Globalization; +using System.Text; +using FakeXrmEasy.Abstractions.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; + +namespace FakeXrmEasy.Core.CommercialLicense +{ + internal class SubscriptionPlanManager + { + private static string GenerateHash(string input) + { + using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) + { + byte[] inputBytes = Encoding.UTF8.GetBytes(input); + byte[] hashBytes = md5.ComputeHash(inputBytes); + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < hashBytes.Length; i++) + { + sb.Append(hashBytes[i].ToString("X2")); + } + return sb.ToString(); + } + } + + internal ISubscriptionInfo GetSubscriptionInfoFromKey(string licenseKey) + { + try + { + var encodedBaseKey = licenseKey.Substring(0, licenseKey.Length - 32); + var hash = licenseKey.Substring(licenseKey.Length - 32, 32); + var computedHash = GenerateHash(encodedBaseKey); + + if (!computedHash.Equals(hash)) + { + throw new InvalidLicenseKeyException(); + } + + var decodedBaseKey = Encoding.UTF8.GetString(Convert.FromBase64String(encodedBaseKey)); + var baseKeyParts = decodedBaseKey.Split('-'); + + var expiryDate = DateTime.ParseExact(baseKeyParts[4], "yyyyMMdd", CultureInfo.InvariantCulture); + var numberOfUsers = int.Parse(baseKeyParts[3]); + + var sku = (StockKeepingUnits) Enum.Parse(typeof(StockKeepingUnits), baseKeyParts[0]); + var autoRenews = "1".Equals(baseKeyParts[2]); + + return new SubscriptionInfo() + { + SKU = sku, + CustomerId = baseKeyParts[1], + NumberOfUsers = numberOfUsers, + EndDate = expiryDate, + AutoRenews = autoRenews + }; + } + catch + { + throw new InvalidLicenseKeyException(); + } + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs new file mode 100644 index 00000000..9112b3dd --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs @@ -0,0 +1,25 @@ +using FakeXrmEasy.Core.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public class SubscriptionPlanManagerTests + { + private readonly SubscriptionPlanManager _subscriptionPlanManager; + + public SubscriptionPlanManagerTests() + { + _subscriptionPlanManager = new SubscriptionPlanManager(); + } + + [Fact] + public void Should_raise_invalid_license_key_exception() + { + var invalidKey = + "asdasdkjakdhu38768a79aysdaiushdakjshdajshda79878s97d89as7d9a87sda98sdyausydusydausdajbdahsdjhasgdahsgda78sda8s7d6a986d98as6d9a8d"; + + Assert.Throws(() => _subscriptionPlanManager.GetSubscriptionInfoFromKey(invalidKey)); + } + } +} \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs new file mode 100644 index 00000000..79d4f6d2 --- /dev/null +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs @@ -0,0 +1,21 @@ +using FakeXrmEasy.Core.CommercialLicense; +using Xunit; + +namespace FakeXrmEasy.Core.Tests.CommercialLicense +{ + public class UserReaderTests + { + private readonly UserReader _userReader; + + public UserReaderTests() + { + _userReader = new UserReader(); + } + + [Fact] + public void Should_return_current_user() + { + Assert.Equal(System.Security.Principal.WindowsIdentity.GetCurrent().Name, _userReader.GetCurrentUserName()); + } + } +} \ No newline at end of file From dfd13532c29d2c0a6966c0733d4d57b724d808b2 Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 29 Jan 2024 00:25:23 +0100 Subject: [PATCH 06/10] More tests added --- .../CommercialLicense/SubscriptionPlanManager.cs | 2 +- .../CommercialLicense/SubscriptionPlanManagerTests.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs index 2af6e3d3..612e28d1 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/SubscriptionPlanManager.cs @@ -8,7 +8,7 @@ namespace FakeXrmEasy.Core.CommercialLicense { internal class SubscriptionPlanManager { - private static string GenerateHash(string input) + internal string GenerateHash(string input) { using (System.Security.Cryptography.MD5 md5 = System.Security.Cryptography.MD5.Create()) { diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs index 9112b3dd..4cb5e73c 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/SubscriptionPlanManagerTests.cs @@ -21,5 +21,14 @@ public void Should_raise_invalid_license_key_exception() Assert.Throws(() => _subscriptionPlanManager.GetSubscriptionInfoFromKey(invalidKey)); } + + [Fact] + public void Should_raise_invalid_license_key_exception_even_if_hash_matches_but_subscription_data_is_not_valid() + { + var invalidKey = + "asdasdkjakdhu38768a79aysdaiushdakjshdajshda79878s97d89as7d9a87sda98sdyausydusydausdajbdahsdj"; + + Assert.Throws(() => _subscriptionPlanManager.GetSubscriptionInfoFromKey($"{invalidKey}{_subscriptionPlanManager.GenerateHash(invalidKey)}")); + } } } \ No newline at end of file From 95da511e7a2ee529debeaa902b6ef61134bebae6 Mon Sep 17 00:00:00 2001 From: Jordi Date: Mon, 29 Jan 2024 01:00:44 +0100 Subject: [PATCH 07/10] Increase code coverage --- .../Middleware/MiddlewareBuilderTests.cs | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs b/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs index 14b37792..2df5de5a 100644 --- a/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs +++ b/tests/FakeXrmEasy.Core.Tests/Middleware/MiddlewareBuilderTests.cs @@ -6,13 +6,36 @@ using Crm; using FakeXrmEasy.Abstractions.Middleware; using FakeXrmEasy.Abstractions; +using FakeXrmEasy.Abstractions.CommercialLicense; using FakeXrmEasy.Abstractions.Enums; using FakeXrmEasy.Abstractions.Exceptions; +using FakeXrmEasy.Core.CommercialLicense; +using FakeXrmEasy.Core.CommercialLicense.Exceptions; namespace FakeXrmEasy.Core.Tests.Middleware { - public partial class MiddlewareBuilderTests + public partial class MiddlewareBuilderTests { + private readonly ISubscriptionStorageProvider _fakeSubscriptionStorageProvider; + private readonly ISubscriptionInfo _subscriptionInfo; + private readonly ISubscriptionUsage _subscriptionUsage; + public MiddlewareBuilderTests() + { + _subscriptionInfo = new SubscriptionInfo() + { + EndDate = DateTime.UtcNow.AddDays(1), + NumberOfUsers = 1 + }; + + _subscriptionUsage = new SubscriptionUsage() + { + + }; + + _fakeSubscriptionStorageProvider = A.Fake(); + A.CallTo(() => _fakeSubscriptionStorageProvider.Read()).ReturnsLazily(() => _subscriptionUsage); + } + [Fact] public void Should_create_new_instance() { @@ -121,7 +144,21 @@ public void Should_throw_exception_when_building_without_a_license() { Assert.Throws(() => MiddlewareBuilder.New().Build()); } + + [Fact] + public void Should_not_throw_exception_when_using_commercial_license_with_custom_storage_and_valid_data() + { + SubscriptionManager._subscriptionInfo = _subscriptionInfo; + SubscriptionManager._subscriptionUsage = _subscriptionUsage; + + var ctx = MiddlewareBuilder + .New() + .SetLicense(FakeXrmEasyLicense.Commercial) + .Build(); + Assert.Equal(FakeXrmEasyLicense.Commercial, ctx.LicenseContext); + } + [Fact] public void Should_throw_exception_when_using_default_faked_context_constructor_without_a_license() { @@ -138,14 +175,7 @@ public void Should_not_throw_exception_when_using_default_faked_context_construc #pragma warning restore CS0618 // Type or member is obsolete Assert.Null(exception); } - - [Fact] - public void Should_return_user_info() - { - var windowsIdentity = System.Security.Principal.WindowsIdentity.GetCurrent(); - string userName = windowsIdentity.Name; - Assert.NotNull(userName); - } + } From ad7ccaaadd2aebd0b0e84ab043f85a9d291f9bf9 Mon Sep 17 00:00:00 2001 From: Jordi Date: Wed, 31 Jan 2024 20:39:51 +0100 Subject: [PATCH 08/10] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f1f48d1..9474dbd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2.4.0] + +## Added + +- **Alpha**: Introduced subscription usage monitoring based on customer feedback + ## [2.3.3] ### Added From a43f7843d0c36d1ce5b5f4cd0f4f6fc71e96b313 Mon Sep 17 00:00:00 2001 From: Jordi Date: Fri, 2 Feb 2024 16:47:12 +0100 Subject: [PATCH 09/10] Increment version --- src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj | 14 +++++----- .../FakeXrmEasy.Core.Tests.csproj | 28 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj b/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj index 15f0ddf7..72fee4ad 100644 --- a/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj +++ b/src/FakeXrmEasy.Core/FakeXrmEasy.Core.csproj @@ -70,25 +70,25 @@ - + - + - + - + - + - + - + diff --git a/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj b/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj index d5692389..55f0235a 100644 --- a/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj +++ b/tests/FakeXrmEasy.Core.Tests/FakeXrmEasy.Core.Tests.csproj @@ -11,7 +11,7 @@ true FakeXrmEasy.CoreTests - 3.3.3 + 3.4.0 Jordi Montaña Dynamics Value S.L. Internal Unit test suite for FakeXrmEasy.Core package @@ -82,25 +82,25 @@ - + - + - + - + - + - + - + @@ -108,22 +108,22 @@ - + - + - + - + - + - + From b0d4ea85298ae983a1938aaabeea3fe7801ea968 Mon Sep 17 00:00:00 2001 From: Jordi Date: Fri, 2 Feb 2024 16:57:57 +0100 Subject: [PATCH 10/10] Windows.Security principal is not supported on non Windows OSs --- src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs | 4 +++- .../CommercialLicense/UserReaderTests.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs b/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs index 57ac2b96..e65e0dee 100644 --- a/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs +++ b/src/FakeXrmEasy.Core/CommercialLicense/UserReader.cs @@ -1,3 +1,5 @@ +using System; + namespace FakeXrmEasy.Core.CommercialLicense { /// @@ -16,7 +18,7 @@ internal class UserReader: IUserReader { public string GetCurrentUserName() { - return System.Security.Principal.WindowsIdentity.GetCurrent().Name; + return Environment.UserName; } } } \ No newline at end of file diff --git a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs index 79d4f6d2..883c4e1f 100644 --- a/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs +++ b/tests/FakeXrmEasy.Core.Tests/CommercialLicense/UserReaderTests.cs @@ -1,3 +1,4 @@ +using System; using FakeXrmEasy.Core.CommercialLicense; using Xunit; @@ -15,7 +16,7 @@ public UserReaderTests() [Fact] public void Should_return_current_user() { - Assert.Equal(System.Security.Principal.WindowsIdentity.GetCurrent().Name, _userReader.GetCurrentUserName()); + Assert.Equal(Environment.UserName, _userReader.GetCurrentUserName()); } } } \ No newline at end of file