From 4c68a0371eaa3562d059b2af7efeeeb998e56c88 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 16 May 2023 15:34:34 +0100 Subject: [PATCH 01/37] Adding the Secret manager to the Plan List --- src/Core/Enums/BitwardenProductType..cs | 11 ++ src/Core/Models/StaticStore/Plan.cs | 4 + src/Core/Utilities/StaticStore.cs | 186 +++++++++++++++++++++++- 3 files changed, 199 insertions(+), 2 deletions(-) create mode 100644 src/Core/Enums/BitwardenProductType..cs diff --git a/src/Core/Enums/BitwardenProductType..cs b/src/Core/Enums/BitwardenProductType..cs new file mode 100644 index 000000000000..729a64577df6 --- /dev/null +++ b/src/Core/Enums/BitwardenProductType..cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Enums; + +public enum BitwardenProductType : byte +{ + [Display(Name = "PasswordManager")] + PasswordManager = 0, + [Display(Name = "SecretManager")] + SecretManager = 1, +} diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs index 4a0313790dc4..8dc60cd468c0 100644 --- a/src/Core/Models/StaticStore/Plan.cs +++ b/src/Core/Models/StaticStore/Plan.cs @@ -52,4 +52,8 @@ public class Plan public decimal SeatPrice { get; set; } public decimal AdditionalStoragePricePerGb { get; set; } public decimal PremiumAccessOptionPrice { get; set; } + public decimal AdditionalPricePerServiceAccount { get; set; } + public short? BaseServiceAccount { get; set; } + public bool HasAdditionalServiceAccountOption { get; set; } + public BitwardenProductType BitwardenProduct { get; set; } } diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index d425524c32a7..0f603497f816 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -111,6 +111,7 @@ static StaticStore() { Type = PlanType.Free, Product = ProductType.Free, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Free", NameLocalizationKey = "planNameFree", DescriptionLocalizationKey = "planDescFree", @@ -127,6 +128,7 @@ static StaticStore() { Type = PlanType.FamiliesAnnually2019, Product = ProductType.Families, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Families 2019", IsAnnual = true, NameLocalizationKey = "planNameFamilies", @@ -159,6 +161,7 @@ static StaticStore() { Type = PlanType.TeamsAnnually2019, Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Teams (Annually) 2019", IsAnnual = true, NameLocalizationKey = "planNameTeams", @@ -190,6 +193,7 @@ static StaticStore() { Type = PlanType.TeamsMonthly2019, Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Teams (Monthly) 2019", NameLocalizationKey = "planNameTeams", DescriptionLocalizationKey = "planDescTeams", @@ -222,6 +226,7 @@ static StaticStore() Name = "Enterprise (Annually) 2019", IsAnnual = true, Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, NameLocalizationKey = "planNameEnterprise", DescriptionLocalizationKey = "planDescEnterprise", CanBeUsedByBusiness = true, @@ -260,6 +265,7 @@ static StaticStore() { Type = PlanType.EnterpriseMonthly2019, Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Enterprise (Monthly) 2019", NameLocalizationKey = "planNameEnterprise", DescriptionLocalizationKey = "planDescEnterprise", @@ -299,6 +305,7 @@ static StaticStore() { Type = PlanType.FamiliesAnnually, Product = ProductType.Families, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Families", IsAnnual = true, NameLocalizationKey = "planNameFamilies", @@ -328,6 +335,7 @@ static StaticStore() { Type = PlanType.TeamsAnnually, Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Teams (Annually)", IsAnnual = true, NameLocalizationKey = "planNameTeams", @@ -362,6 +370,7 @@ static StaticStore() { Type = PlanType.TeamsMonthly, Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Teams (Monthly)", NameLocalizationKey = "planNameTeams", DescriptionLocalizationKey = "planDescTeams", @@ -396,6 +405,7 @@ static StaticStore() Type = PlanType.EnterpriseAnnually, Name = "Enterprise (Annually)", Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, IsAnnual = true, NameLocalizationKey = "planNameEnterprise", DescriptionLocalizationKey = "planDescEnterprise", @@ -437,6 +447,7 @@ static StaticStore() { Type = PlanType.EnterpriseMonthly, Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, Name = "Enterprise (Monthly)", NameLocalizationKey = "planNameEnterprise", DescriptionLocalizationKey = "planDescEnterprise", @@ -474,6 +485,177 @@ static StaticStore() AllowSeatAutoscale = true, }, + new Plan + { + Type = PlanType.EnterpriseMonthly, + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.SecretManager, + Name = "Enterprise (Monthly)", + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 100, + + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSelfHost = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "sm-enterprise-seat-monthly", + StripeStoragePlanId = "service-account-monthly", + BasePrice = 0, + SeatPrice = 28, + AdditionalPricePerServiceAccount = 1, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseAnnually, + Name = "Enterprise (Annually)", + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.SecretManager, + IsAnnual = true, + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 100, + + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "sm-enterprise-seat-annually", + StripeStoragePlanId = "service-account-annually", + BasePrice = 0, + SeatPrice = 24, + AdditionalPricePerServiceAccount = 1, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsMonthly, + Name = "Teams (Monthly)", + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.SecretManager, + IsAnnual = true, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 40, + + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "sm-teams-seat-monthly", + StripeStoragePlanId = "service-account-monthly", + BasePrice = 0, + SeatPrice = 14, + AdditionalPricePerServiceAccount = 1, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsAnnually, + Name = "Teams (Annually)", + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.SecretManager, + IsAnnual = true, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 40, + + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "sm-teams-seat-annually", + StripeStoragePlanId = "service-account-annually", + BasePrice = 0, + SeatPrice = 12, + AdditionalPricePerServiceAccount = 1, + + AllowSeatAutoscale = true, + }, new Plan { Type = PlanType.Custom, @@ -499,8 +681,8 @@ static StaticStore() GetPlan(org.PlanType).Product == ProductType.Enterprise, } }; - public static Plan GetPlan(PlanType planType) => - Plans.FirstOrDefault(p => p.Type == planType); + public static Plan GetPlan(PlanType planType, BitwardenProductType bitwardenProduct = BitwardenProductType.PasswordManager) => + Plans.FirstOrDefault(p => p.Type == planType && p.BitwardenProduct == bitwardenProduct); public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); } From eeca1d55af5928e4d8cecca98258cb12f2684a86 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 17 May 2023 15:27:00 +0100 Subject: [PATCH 02/37] Adding the unit test for the StaticStoreTests class --- src/Core/Models/StaticStore/Plan.cs | 1 + src/Core/Utilities/StaticStore.cs | 20 +++++++++- test/Core.Test/Utilities/StaticStoreTests.cs | 42 ++++++++++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 test/Core.Test/Utilities/StaticStoreTests.cs diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs index 8dc60cd468c0..295e28468fe6 100644 --- a/src/Core/Models/StaticStore/Plan.cs +++ b/src/Core/Models/StaticStore/Plan.cs @@ -54,6 +54,7 @@ public class Plan public decimal PremiumAccessOptionPrice { get; set; } public decimal AdditionalPricePerServiceAccount { get; set; } public short? BaseServiceAccount { get; set; } + public short? MaxServiceAccount { get; set; } public bool HasAdditionalServiceAccountOption { get; set; } public BitwardenProductType BitwardenProduct { get; set; } } diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 0f603497f816..4b802951ab1f 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -576,7 +576,6 @@ static StaticStore() Name = "Teams (Monthly)", Product = ProductType.Teams, BitwardenProduct = BitwardenProductType.SecretManager, - IsAnnual = true, NameLocalizationKey = "planNameTeams", DescriptionLocalizationKey = "planDescTeams", CanBeUsedByBusiness = true, @@ -657,6 +656,25 @@ static StaticStore() AllowSeatAutoscale = true, }, new Plan + { + Type = PlanType.Free, + Product = ProductType.Free, + BitwardenProduct = BitwardenProductType.SecretManager, + Name = "Free", + NameLocalizationKey = "planNameFree", + DescriptionLocalizationKey = "planDescFree", + BaseSeats = 2, + BaseServiceAccount = 3, + MaxCollections = 2, + MaxUsers = 2, + MaxServiceAccount = 3, + + UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to + DisplaySortOrder = -1, + + AllowSeatAutoscale = false, + }, + new Plan { Type = PlanType.Custom, diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs new file mode 100644 index 000000000000..ec395985b7ac --- /dev/null +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -0,0 +1,42 @@ +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Test.Utilities; +using Xunit; + +public class StaticStoreTests +{ + [Fact] + public void StaticStore_Initialization_Success() + { + var plans = StaticStore.Plans; + Assert.NotNull(plans); + Assert.NotEmpty(plans); + Assert.Equal(17, plans.Count()); + } + + [Fact] + public void StaticStore_GetPlanByPlanType_Success() + { + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + + Assert.NotNull(plan); + Assert.Equal(plan.Type,PlanType.EnterpriseAnnually); + } + + [Fact] + public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans() + { + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + Assert.NotNull(plan); + Assert.Equal(plan.BitwardenProduct,BitwardenProductType.PasswordManager); + } + + [Fact] + public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans() + { + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually,BitwardenProductType.SecretManager); + Assert.NotNull(plan); + Assert.Equal(plan.BitwardenProduct,BitwardenProductType.SecretManager); + } +} From d00934c436a321ca9274e51c36e63dc1c7138265 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 17 May 2023 15:38:47 +0100 Subject: [PATCH 03/37] Fix whitespace formatting --- src/Core/Enums/BitwardenProductType..cs | 2 +- test/Core.Test/Utilities/StaticStoreTests.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Core/Enums/BitwardenProductType..cs b/src/Core/Enums/BitwardenProductType..cs index 729a64577df6..0db5692bc706 100644 --- a/src/Core/Enums/BitwardenProductType..cs +++ b/src/Core/Enums/BitwardenProductType..cs @@ -2,7 +2,7 @@ namespace Bit.Core.Enums; -public enum BitwardenProductType : byte +public enum BitwardenProductType : byte { [Display(Name = "PasswordManager")] PasswordManager = 0, diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index ec395985b7ac..7a24e845c852 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -12,7 +12,7 @@ public void StaticStore_Initialization_Success() var plans = StaticStore.Plans; Assert.NotNull(plans); Assert.NotEmpty(plans); - Assert.Equal(17, plans.Count()); + Assert.Equal(17, plans.Count()); } [Fact] @@ -21,7 +21,7 @@ public void StaticStore_GetPlanByPlanType_Success() var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); Assert.NotNull(plan); - Assert.Equal(plan.Type,PlanType.EnterpriseAnnually); + Assert.Equal(PlanType.EnterpriseAnnually,plan.Type); } [Fact] @@ -29,14 +29,14 @@ public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans() { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); Assert.NotNull(plan); - Assert.Equal(plan.BitwardenProduct,BitwardenProductType.PasswordManager); + Assert.Equal(plan.BitwardenProduct, BitwardenProductType.PasswordManager); } - + [Fact] public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans() { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually,BitwardenProductType.SecretManager); + var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually, BitwardenProductType.SecretManager); Assert.NotNull(plan); - Assert.Equal(plan.BitwardenProduct,BitwardenProductType.SecretManager); + Assert.Equal(BitwardenProductType.SecretManager,plan.BitwardenProduct); } } From 52f44be6fd15467a582cfb47dfe331e21c23a9ff Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 17 May 2023 15:48:30 +0100 Subject: [PATCH 04/37] Fix whitespace formatting --- test/Core.Test/Utilities/StaticStoreTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 7a24e845c852..804f17abc184 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -21,7 +21,7 @@ public void StaticStore_GetPlanByPlanType_Success() var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); Assert.NotNull(plan); - Assert.Equal(PlanType.EnterpriseAnnually,plan.Type); + Assert.Equal(PlanType.EnterpriseAnnually, plan.Type); } [Fact] @@ -29,7 +29,7 @@ public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans() { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); Assert.NotNull(plan); - Assert.Equal(plan.BitwardenProduct, BitwardenProductType.PasswordManager); + Assert.Equal(BitwardenProductType.PasswordManager, plan.BitwardenProduct); } [Fact] @@ -37,6 +37,6 @@ public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerP { var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually, BitwardenProductType.SecretManager); Assert.NotNull(plan); - Assert.Equal(BitwardenProductType.SecretManager,plan.BitwardenProduct); + Assert.Equal(BitwardenProductType.SecretManager, plan.BitwardenProduct); } } From 0bd73e6dc757340b50236814f0e2d932def7333d Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Thu, 18 May 2023 11:37:41 +0100 Subject: [PATCH 05/37] Price update --- src/Core/Utilities/StaticStore.cs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 4b802951ab1f..1216ce6d91bc 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -495,7 +495,7 @@ static StaticStore() DescriptionLocalizationKey = "planDescEnterprise", CanBeUsedByBusiness = true, BaseSeats = 0, - BaseServiceAccount = 100, + BaseServiceAccount = 200, HasAdditionalSeatsOption = true, HasAdditionalServiceAccountOption = true, @@ -522,8 +522,8 @@ static StaticStore() StripeSeatPlanId = "sm-enterprise-seat-monthly", StripeStoragePlanId = "service-account-monthly", BasePrice = 0, - SeatPrice = 28, - AdditionalPricePerServiceAccount = 1, + SeatPrice = 12, + AdditionalPricePerServiceAccount = 0.5M, AllowSeatAutoscale = true, }, @@ -538,7 +538,7 @@ static StaticStore() DescriptionLocalizationKey = "planDescEnterprise", CanBeUsedByBusiness = true, BaseSeats = 0, - BaseServiceAccount = 100, + BaseServiceAccount = 200, HasAdditionalSeatsOption = true, HasAdditionalServiceAccountOption = true, @@ -565,8 +565,8 @@ static StaticStore() StripeSeatPlanId = "sm-enterprise-seat-annually", StripeStoragePlanId = "service-account-annually", BasePrice = 0, - SeatPrice = 24, - AdditionalPricePerServiceAccount = 1, + SeatPrice = 10, + AdditionalPricePerServiceAccount = 0.5M, AllowSeatAutoscale = true, }, @@ -580,7 +580,7 @@ static StaticStore() DescriptionLocalizationKey = "planDescTeams", CanBeUsedByBusiness = true, BaseSeats = 0, - BaseServiceAccount = 40, + BaseServiceAccount = 50, HasAdditionalSeatsOption = true, HasAdditionalServiceAccountOption = true, @@ -607,8 +607,8 @@ static StaticStore() StripeSeatPlanId = "sm-teams-seat-monthly", StripeStoragePlanId = "service-account-monthly", BasePrice = 0, - SeatPrice = 14, - AdditionalPricePerServiceAccount = 1, + SeatPrice = 6, + AdditionalPricePerServiceAccount = 0.5M, AllowSeatAutoscale = true, }, @@ -623,7 +623,7 @@ static StaticStore() DescriptionLocalizationKey = "planDescTeams", CanBeUsedByBusiness = true, BaseSeats = 0, - BaseServiceAccount = 40, + BaseServiceAccount = 50, HasAdditionalSeatsOption = true, HasAdditionalServiceAccountOption = true, @@ -650,8 +650,8 @@ static StaticStore() StripeSeatPlanId = "sm-teams-seat-annually", StripeStoragePlanId = "service-account-annually", BasePrice = 0, - SeatPrice = 12, - AdditionalPricePerServiceAccount = 1, + SeatPrice = 4, + AdditionalPricePerServiceAccount = 0.5M, AllowSeatAutoscale = true, }, From 169809b79a2efc209f1901ec199d499fb8a2b06d Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 08:42:56 +0100 Subject: [PATCH 06/37] Resolving the PR comments --- .../Utilities/PasswordManagerPlanStore.cs | 6 + src/Core/Utilities/SecretsManagerPlanStore.cs | 182 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/Core/Utilities/PasswordManagerPlanStore.cs create mode 100644 src/Core/Utilities/SecretsManagerPlanStore.cs diff --git a/src/Core/Utilities/PasswordManagerPlanStore.cs b/src/Core/Utilities/PasswordManagerPlanStore.cs new file mode 100644 index 000000000000..6d5c3173e353 --- /dev/null +++ b/src/Core/Utilities/PasswordManagerPlanStore.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Utilities; + +public class PasswordManagerPlanStore +{ + +} diff --git a/src/Core/Utilities/SecretsManagerPlanStore.cs b/src/Core/Utilities/SecretsManagerPlanStore.cs new file mode 100644 index 000000000000..820f5604e0fe --- /dev/null +++ b/src/Core/Utilities/SecretsManagerPlanStore.cs @@ -0,0 +1,182 @@ +using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; + +namespace Bit.Core.Utilities; + +public static class SecretManagerPlanStore +{ + public static IEnumerable CreatePlan() + { + return new List + { + new Plan + { + Type = PlanType.EnterpriseMonthly, + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.SecretsManager, + Name = "Enterprise (Monthly)", + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 200, + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + HasPolicies = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSelfHost = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + StripeSeatPlanId = "sm-enterprise-seat-monthly", + StripeStoragePlanId = "service-account-monthly", + BasePrice = 0, + SeatPrice = 12, + AdditionalPricePerServiceAccount = 0.5M, + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseAnnually, + Name = "Enterprise (Annually)", + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.SecretsManager, + IsAnnual = true, + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 200, + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + StripeSeatPlanId = "sm-enterprise-seat-annually", + StripeStoragePlanId = "service-account-annually", + BasePrice = 0, + SeatPrice = 10, + AdditionalPricePerServiceAccount = 0.5M, + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsMonthly, + Name = "Teams (Monthly)", + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.SecretsManager, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 50, + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + StripeSeatPlanId = "sm-teams-seat-monthly", + StripeStoragePlanId = "service-account-monthly", + BasePrice = 0, + SeatPrice = 6, + AdditionalPricePerServiceAccount = 0.5M, + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsAnnually, + Name = "Teams (Annually)", + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.SecretsManager, + IsAnnual = true, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseServiceAccount = 50, + HasAdditionalSeatsOption = true, + HasAdditionalServiceAccountOption = true, + TrialPeriodDays = 7, + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + StripeSeatPlanId = "sm-teams-seat-annually", + StripeStoragePlanId = "service-account-annually", + BasePrice = 0, + SeatPrice = 4, + AdditionalPricePerServiceAccount = 0.5M, + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.Free, + Product = ProductType.Free, + BitwardenProduct = BitwardenProductType.SecretsManager, + Name = "Free", + NameLocalizationKey = "planNameFree", + DescriptionLocalizationKey = "planDescFree", + BaseSeats = 2, + BaseServiceAccount = 3, + MaxCollections = 2, + MaxUsers = 2, + MaxServiceAccount = 3, + UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to + DisplaySortOrder = -1, + AllowSeatAutoscale = false, + }, + new Plan { Type = PlanType.Custom, AllowSeatAutoscale = true, }, + }; + } +} From 9722ae5f2e0e7d5ee8fbd6be12c76a0514509edd Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 08:43:29 +0100 Subject: [PATCH 07/37] Resolving PR comments --- src/Core/Core.csproj | 9 + src/Core/Enums/BitwardenProductType..cs | 6 +- src/Core/Models/StaticStore/Plan.cs | 2 +- .../Utilities/PasswordManagerPlanStore.cs | 398 +++++++++++- src/Core/Utilities/SecretsManagerPlanStore.cs | 2 +- src/Core/Utilities/StaticStore.cs | 581 +----------------- test/Core.Test/Utilities/StaticStoreTests.cs | 29 +- 7 files changed, 430 insertions(+), 597 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 5a6b771c8fa2..d2fed4bac748 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -17,6 +17,7 @@ + @@ -63,4 +64,12 @@ + + + + + + + + diff --git a/src/Core/Enums/BitwardenProductType..cs b/src/Core/Enums/BitwardenProductType..cs index 0db5692bc706..d0c358671f74 100644 --- a/src/Core/Enums/BitwardenProductType..cs +++ b/src/Core/Enums/BitwardenProductType..cs @@ -4,8 +4,8 @@ namespace Bit.Core.Enums; public enum BitwardenProductType : byte { - [Display(Name = "PasswordManager")] + [Display(Name = "Password Manager")] PasswordManager = 0, - [Display(Name = "SecretManager")] - SecretManager = 1, + [Display(Name = "Secrets Manager")] + SecretsManager = 1, } diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs index 295e28468fe6..56ffe9b2a705 100644 --- a/src/Core/Models/StaticStore/Plan.cs +++ b/src/Core/Models/StaticStore/Plan.cs @@ -52,7 +52,7 @@ public class Plan public decimal SeatPrice { get; set; } public decimal AdditionalStoragePricePerGb { get; set; } public decimal PremiumAccessOptionPrice { get; set; } - public decimal AdditionalPricePerServiceAccount { get; set; } + public decimal? AdditionalPricePerServiceAccount { get; set; } public short? BaseServiceAccount { get; set; } public short? MaxServiceAccount { get; set; } public bool HasAdditionalServiceAccountOption { get; set; } diff --git a/src/Core/Utilities/PasswordManagerPlanStore.cs b/src/Core/Utilities/PasswordManagerPlanStore.cs index 6d5c3173e353..da372e4c3497 100644 --- a/src/Core/Utilities/PasswordManagerPlanStore.cs +++ b/src/Core/Utilities/PasswordManagerPlanStore.cs @@ -1,6 +1,398 @@ -namespace Bit.Core.Utilities; +using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; -public class PasswordManagerPlanStore +namespace Bit.Core.Utilities; + +public static class PasswordManagerPlanStore { - + public static IEnumerable CreatePlan() + { + return new List + { + new Plan + { + Type = PlanType.Free, + Product = ProductType.Free, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Free", + NameLocalizationKey = "planNameFree", + DescriptionLocalizationKey = "planDescFree", + BaseSeats = 2, + MaxCollections = 2, + MaxUsers = 2, + + UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to + DisplaySortOrder = -1, + + AllowSeatAutoscale = false, + }, + new Plan + { + Type = PlanType.FamiliesAnnually2019, + Product = ProductType.Families, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Families 2019", + IsAnnual = true, + NameLocalizationKey = "planNameFamilies", + DescriptionLocalizationKey = "planDescFamilies", + BaseSeats = 5, + BaseStorageGb = 1, + MaxUsers = 5, + + HasAdditionalStorageOption = true, + HasPremiumAccessOption = true, + TrialPeriodDays = 7, + + HasSelfHost = true, + HasTotp = true, + + UpgradeSortOrder = 1, + DisplaySortOrder = 1, + LegacyYear = 2020, + + StripePlanId = "personal-org-annually", + StripeStoragePlanId = "storage-gb-annually", + StripePremiumAccessPlanId = "personal-org-premium-access-annually", + BasePrice = 12, + AdditionalStoragePricePerGb = 4, + PremiumAccessOptionPrice = 40, + + AllowSeatAutoscale = false, + }, + new Plan + { + Type = PlanType.TeamsAnnually2019, + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Teams (Annually) 2019", + IsAnnual = true, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 5, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasTotp = true, + + UpgradeSortOrder = 2, + DisplaySortOrder = 2, + LegacyYear = 2020, + + StripePlanId = "teams-org-annually", + StripeSeatPlanId = "teams-org-seat-annually", + StripeStoragePlanId = "storage-gb-annually", + BasePrice = 60, + SeatPrice = 24, + AdditionalStoragePricePerGb = 4, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsMonthly2019, + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Teams (Monthly) 2019", + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseSeats = 5, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasTotp = true, + + UpgradeSortOrder = 2, + DisplaySortOrder = 2, + LegacyYear = 2020, + + StripePlanId = "teams-org-monthly", + StripeSeatPlanId = "teams-org-seat-monthly", + StripeStoragePlanId = "storage-gb-monthly", + BasePrice = 8, + SeatPrice = 2.5M, + AdditionalStoragePricePerGb = 0.5M, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseAnnually2019, + Name = "Enterprise (Annually) 2019", + IsAnnual = true, + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + LegacyYear = 2020, + + StripePlanId = null, + StripeSeatPlanId = "enterprise-org-seat-annually", + StripeStoragePlanId = "storage-gb-annually", + BasePrice = 0, + SeatPrice = 36, + AdditionalStoragePricePerGb = 4, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseMonthly2019, + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Enterprise (Monthly) 2019", + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSelfHost = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + LegacyYear = 2020, + + StripePlanId = null, + StripeSeatPlanId = "enterprise-org-seat-monthly", + StripeStoragePlanId = "storage-gb-monthly", + BasePrice = 0, + SeatPrice = 4M, + AdditionalStoragePricePerGb = 0.5M, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.FamiliesAnnually, + Product = ProductType.Families, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Families", + IsAnnual = true, + NameLocalizationKey = "planNameFamilies", + DescriptionLocalizationKey = "planDescFamilies", + BaseSeats = 6, + BaseStorageGb = 1, + MaxUsers = 6, + + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasSelfHost = true, + HasTotp = true, + UsersGetPremium = true, + + UpgradeSortOrder = 1, + DisplaySortOrder = 1, + + StripePlanId = "2020-families-org-annually", + StripeStoragePlanId = "storage-gb-annually", + BasePrice = 40, + AdditionalStoragePricePerGb = 4, + + AllowSeatAutoscale = false, + }, + new Plan + { + Type = PlanType.TeamsAnnually, + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Teams (Annually)", + IsAnnual = true, + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseStorageGb = 1, + BaseSeats = 0, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + Has2fa = true, + HasApi = true, + HasDirectory = true, + HasEvents = true, + HasGroups = true, + HasTotp = true, + UsersGetPremium = true, + + UpgradeSortOrder = 2, + DisplaySortOrder = 2, + + StripeSeatPlanId = "2020-teams-org-seat-annually", + StripeStoragePlanId = "storage-gb-annually", + SeatPrice = 36, + AdditionalStoragePricePerGb = 4, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.TeamsMonthly, + Product = ProductType.Teams, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Teams (Monthly)", + NameLocalizationKey = "planNameTeams", + DescriptionLocalizationKey = "planDescTeams", + CanBeUsedByBusiness = true, + BaseStorageGb = 1, + BaseSeats = 0, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + Has2fa = true, + HasApi = true, + HasDirectory = true, + HasEvents = true, + HasGroups = true, + HasTotp = true, + UsersGetPremium = true, + + UpgradeSortOrder = 2, + DisplaySortOrder = 2, + + StripeSeatPlanId = "2020-teams-org-seat-monthly", + StripeStoragePlanId = "storage-gb-monthly", + SeatPrice = 4, + AdditionalStoragePricePerGb = 0.5M, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseAnnually, + Name = "Enterprise (Annually)", + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, + IsAnnual = true, + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasSelfHost = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "2020-enterprise-org-seat-annually", + StripeStoragePlanId = "storage-gb-annually", + BasePrice = 0, + SeatPrice = 60, + AdditionalStoragePricePerGb = 4, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.EnterpriseMonthly, + Product = ProductType.Enterprise, + BitwardenProduct = BitwardenProductType.PasswordManager, + Name = "Enterprise (Monthly)", + NameLocalizationKey = "planNameEnterprise", + DescriptionLocalizationKey = "planDescEnterprise", + CanBeUsedByBusiness = true, + BaseSeats = 0, + BaseStorageGb = 1, + + HasAdditionalSeatsOption = true, + HasAdditionalStorageOption = true, + TrialPeriodDays = 7, + + HasPolicies = true, + HasGroups = true, + HasDirectory = true, + HasEvents = true, + HasTotp = true, + Has2fa = true, + HasApi = true, + HasSelfHost = true, + HasSso = true, + HasKeyConnector = true, + HasScim = true, + HasResetPassword = true, + UsersGetPremium = true, + HasCustomPermissions = true, + + UpgradeSortOrder = 3, + DisplaySortOrder = 3, + + StripeSeatPlanId = "2020-enterprise-seat-monthly", + StripeStoragePlanId = "storage-gb-monthly", + BasePrice = 0, + SeatPrice = 6, + AdditionalStoragePricePerGb = 0.5M, + + AllowSeatAutoscale = true, + }, + new Plan + { + Type = PlanType.Custom, + + AllowSeatAutoscale = true, + }, + }; + } } diff --git a/src/Core/Utilities/SecretsManagerPlanStore.cs b/src/Core/Utilities/SecretsManagerPlanStore.cs index 820f5604e0fe..857740e6dd17 100644 --- a/src/Core/Utilities/SecretsManagerPlanStore.cs +++ b/src/Core/Utilities/SecretsManagerPlanStore.cs @@ -3,7 +3,7 @@ namespace Bit.Core.Utilities; -public static class SecretManagerPlanStore +public static class SecretsManagerPlanStore { public static IEnumerable CreatePlan() { diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 4f785f7a26e2..273f660fd260 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -105,583 +105,12 @@ static StaticStore() #endregion #region Plans + + var passwordManagerPlans = PasswordManagerPlanStore.CreatePlan(); + var secretManagerPlans = SecretsManagerPlanStore.CreatePlan(); - Plans = new List - { - new Plan - { - Type = PlanType.Free, - Product = ProductType.Free, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Free", - NameLocalizationKey = "planNameFree", - DescriptionLocalizationKey = "planDescFree", - BaseSeats = 2, - MaxCollections = 2, - MaxUsers = 2, - - UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to - DisplaySortOrder = -1, - - AllowSeatAutoscale = false, - }, - new Plan - { - Type = PlanType.FamiliesAnnually2019, - Product = ProductType.Families, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Families 2019", - IsAnnual = true, - NameLocalizationKey = "planNameFamilies", - DescriptionLocalizationKey = "planDescFamilies", - BaseSeats = 5, - BaseStorageGb = 1, - MaxUsers = 5, - - HasAdditionalStorageOption = true, - HasPremiumAccessOption = true, - TrialPeriodDays = 7, - - HasSelfHost = true, - HasTotp = true, - - UpgradeSortOrder = 1, - DisplaySortOrder = 1, - LegacyYear = 2020, - - StripePlanId = "personal-org-annually", - StripeStoragePlanId = "storage-gb-annually", - StripePremiumAccessPlanId = "personal-org-premium-access-annually", - BasePrice = 12, - AdditionalStoragePricePerGb = 4, - PremiumAccessOptionPrice = 40, - - AllowSeatAutoscale = false, - }, - new Plan - { - Type = PlanType.TeamsAnnually2019, - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Teams (Annually) 2019", - IsAnnual = true, - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseSeats = 5, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasTotp = true, - - UpgradeSortOrder = 2, - DisplaySortOrder = 2, - LegacyYear = 2020, - - StripePlanId = "teams-org-annually", - StripeSeatPlanId = "teams-org-seat-annually", - StripeStoragePlanId = "storage-gb-annually", - BasePrice = 60, - SeatPrice = 24, - AdditionalStoragePricePerGb = 4, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.TeamsMonthly2019, - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Teams (Monthly) 2019", - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseSeats = 5, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasTotp = true, - - UpgradeSortOrder = 2, - DisplaySortOrder = 2, - LegacyYear = 2020, - - StripePlanId = "teams-org-monthly", - StripeSeatPlanId = "teams-org-seat-monthly", - StripeStoragePlanId = "storage-gb-monthly", - BasePrice = 8, - SeatPrice = 2.5M, - AdditionalStoragePricePerGb = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseAnnually2019, - Name = "Enterprise (Annually) 2019", - IsAnnual = true, - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.PasswordManager, - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasSelfHost = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - LegacyYear = 2020, - - StripePlanId = null, - StripeSeatPlanId = "enterprise-org-seat-annually", - StripeStoragePlanId = "storage-gb-annually", - BasePrice = 0, - SeatPrice = 36, - AdditionalStoragePricePerGb = 4, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseMonthly2019, - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Enterprise (Monthly) 2019", - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSelfHost = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - LegacyYear = 2020, - - StripePlanId = null, - StripeSeatPlanId = "enterprise-org-seat-monthly", - StripeStoragePlanId = "storage-gb-monthly", - BasePrice = 0, - SeatPrice = 4M, - AdditionalStoragePricePerGb = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.FamiliesAnnually, - Product = ProductType.Families, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Families", - IsAnnual = true, - NameLocalizationKey = "planNameFamilies", - DescriptionLocalizationKey = "planDescFamilies", - BaseSeats = 6, - BaseStorageGb = 1, - MaxUsers = 6, - - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasSelfHost = true, - HasTotp = true, - UsersGetPremium = true, - - UpgradeSortOrder = 1, - DisplaySortOrder = 1, - - StripePlanId = "2020-families-org-annually", - StripeStoragePlanId = "storage-gb-annually", - BasePrice = 40, - AdditionalStoragePricePerGb = 4, - - AllowSeatAutoscale = false, - }, - new Plan - { - Type = PlanType.TeamsAnnually, - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Teams (Annually)", - IsAnnual = true, - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseStorageGb = 1, - BaseSeats = 0, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - Has2fa = true, - HasApi = true, - HasDirectory = true, - HasEvents = true, - HasGroups = true, - HasTotp = true, - UsersGetPremium = true, - - UpgradeSortOrder = 2, - DisplaySortOrder = 2, - - StripeSeatPlanId = "2020-teams-org-seat-annually", - StripeStoragePlanId = "storage-gb-annually", - SeatPrice = 36, - AdditionalStoragePricePerGb = 4, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.TeamsMonthly, - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Teams (Monthly)", - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseStorageGb = 1, - BaseSeats = 0, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - Has2fa = true, - HasApi = true, - HasDirectory = true, - HasEvents = true, - HasGroups = true, - HasTotp = true, - UsersGetPremium = true, - - UpgradeSortOrder = 2, - DisplaySortOrder = 2, - - StripeSeatPlanId = "2020-teams-org-seat-monthly", - StripeStoragePlanId = "storage-gb-monthly", - SeatPrice = 4, - AdditionalStoragePricePerGb = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseAnnually, - Name = "Enterprise (Annually)", - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.PasswordManager, - IsAnnual = true, - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasSelfHost = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "2020-enterprise-org-seat-annually", - StripeStoragePlanId = "storage-gb-annually", - BasePrice = 0, - SeatPrice = 60, - AdditionalStoragePricePerGb = 4, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseMonthly, - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.PasswordManager, - Name = "Enterprise (Monthly)", - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseStorageGb = 1, - - HasAdditionalSeatsOption = true, - HasAdditionalStorageOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSelfHost = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "2020-enterprise-seat-monthly", - StripeStoragePlanId = "storage-gb-monthly", - BasePrice = 0, - SeatPrice = 6, - AdditionalStoragePricePerGb = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseMonthly, - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.SecretManager, - Name = "Enterprise (Monthly)", - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseServiceAccount = 200, - - HasAdditionalSeatsOption = true, - HasAdditionalServiceAccountOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSelfHost = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "sm-enterprise-seat-monthly", - StripeStoragePlanId = "service-account-monthly", - BasePrice = 0, - SeatPrice = 12, - AdditionalPricePerServiceAccount = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.EnterpriseAnnually, - Name = "Enterprise (Annually)", - Product = ProductType.Enterprise, - BitwardenProduct = BitwardenProductType.SecretManager, - IsAnnual = true, - NameLocalizationKey = "planNameEnterprise", - DescriptionLocalizationKey = "planDescEnterprise", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseServiceAccount = 200, - - HasAdditionalSeatsOption = true, - HasAdditionalServiceAccountOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasSelfHost = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "sm-enterprise-seat-annually", - StripeStoragePlanId = "service-account-annually", - BasePrice = 0, - SeatPrice = 10, - AdditionalPricePerServiceAccount = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.TeamsMonthly, - Name = "Teams (Monthly)", - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.SecretManager, - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseServiceAccount = 50, - - HasAdditionalSeatsOption = true, - HasAdditionalServiceAccountOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasSelfHost = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "sm-teams-seat-monthly", - StripeStoragePlanId = "service-account-monthly", - BasePrice = 0, - SeatPrice = 6, - AdditionalPricePerServiceAccount = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.TeamsAnnually, - Name = "Teams (Annually)", - Product = ProductType.Teams, - BitwardenProduct = BitwardenProductType.SecretManager, - IsAnnual = true, - NameLocalizationKey = "planNameTeams", - DescriptionLocalizationKey = "planDescTeams", - CanBeUsedByBusiness = true, - BaseSeats = 0, - BaseServiceAccount = 50, - - HasAdditionalSeatsOption = true, - HasAdditionalServiceAccountOption = true, - TrialPeriodDays = 7, - - HasPolicies = true, - HasSelfHost = true, - HasGroups = true, - HasDirectory = true, - HasEvents = true, - HasTotp = true, - Has2fa = true, - HasApi = true, - HasSso = true, - HasKeyConnector = true, - HasScim = true, - HasResetPassword = true, - UsersGetPremium = true, - HasCustomPermissions = true, - - UpgradeSortOrder = 3, - DisplaySortOrder = 3, - - StripeSeatPlanId = "sm-teams-seat-annually", - StripeStoragePlanId = "service-account-annually", - BasePrice = 0, - SeatPrice = 4, - AdditionalPricePerServiceAccount = 0.5M, - - AllowSeatAutoscale = true, - }, - new Plan - { - Type = PlanType.Free, - Product = ProductType.Free, - BitwardenProduct = BitwardenProductType.SecretManager, - Name = "Free", - NameLocalizationKey = "planNameFree", - DescriptionLocalizationKey = "planDescFree", - BaseSeats = 2, - BaseServiceAccount = 3, - MaxCollections = 2, - MaxUsers = 2, - MaxServiceAccount = 3, - - UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to - DisplaySortOrder = -1, - - AllowSeatAutoscale = false, - }, - new Plan - { - Type = PlanType.Custom, - - AllowSeatAutoscale = true, - }, - }; + Plans = passwordManagerPlans.Concat(secretManagerPlans); + #endregion } diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 804f17abc184..32f28911024d 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -12,31 +12,34 @@ public void StaticStore_Initialization_Success() var plans = StaticStore.Plans; Assert.NotNull(plans); Assert.NotEmpty(plans); - Assert.Equal(17, plans.Count()); + Assert.Equal(18, plans.Count()); } - [Fact] - public void StaticStore_GetPlanByPlanType_Success() + [Theory] + [InlineData(PlanType.EnterpriseAnnually)] + public void StaticStore_GetPlanByPlanType_Success(PlanType planType) { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + var plan = StaticStore.GetPlan(planType); Assert.NotNull(plan); - Assert.Equal(PlanType.EnterpriseAnnually, plan.Type); + Assert.Equal(planType, plan.Type); } - [Fact] - public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans() + [Theory] + [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] + public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans(PlanType planType,BitwardenProductType bitwardenProductType) { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually); + var plan = StaticStore.GetPlan(planType); Assert.NotNull(plan); - Assert.Equal(BitwardenProductType.PasswordManager, plan.BitwardenProduct); + Assert.Equal(bitwardenProductType, plan.BitwardenProduct); } - [Fact] - public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans() + [Theory] + [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] + public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans(PlanType planType,BitwardenProductType bitwardenProductType) { - var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually, BitwardenProductType.SecretManager); + var plan = StaticStore.GetPlan(planType, bitwardenProductType); Assert.NotNull(plan); - Assert.Equal(BitwardenProductType.SecretManager, plan.BitwardenProduct); + Assert.Equal(bitwardenProductType, plan.BitwardenProduct); } } From de0d0dd89e9758c40d43ffdc51d5a11d57180ffd Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 08:45:11 +0100 Subject: [PATCH 08/37] Fixing the whitespace --- src/Core/Utilities/PasswordManagerPlanStore.cs | 2 +- src/Core/Utilities/StaticStore.cs | 4 ++-- test/Core.Test/Utilities/StaticStoreTests.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Core/Utilities/PasswordManagerPlanStore.cs b/src/Core/Utilities/PasswordManagerPlanStore.cs index da372e4c3497..1f9400eba61b 100644 --- a/src/Core/Utilities/PasswordManagerPlanStore.cs +++ b/src/Core/Utilities/PasswordManagerPlanStore.cs @@ -7,7 +7,7 @@ public static class PasswordManagerPlanStore { public static IEnumerable CreatePlan() { - return new List + return new List { new Plan { diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 273f660fd260..c63ae4c2b26a 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -105,12 +105,12 @@ static StaticStore() #endregion #region Plans - + var passwordManagerPlans = PasswordManagerPlanStore.CreatePlan(); var secretManagerPlans = SecretsManagerPlanStore.CreatePlan(); Plans = passwordManagerPlans.Concat(secretManagerPlans); - + #endregion } diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 32f28911024d..c4ca7336469a 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -27,7 +27,7 @@ public void StaticStore_GetPlanByPlanType_Success(PlanType planType) [Theory] [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] - public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans(PlanType planType,BitwardenProductType bitwardenProductType) + public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans(PlanType planType, BitwardenProductType bitwardenProductType) { var plan = StaticStore.GetPlan(planType); Assert.NotNull(plan); @@ -36,7 +36,7 @@ public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans(PlanType [Theory] [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] - public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans(PlanType planType,BitwardenProductType bitwardenProductType) + public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans(PlanType planType, BitwardenProductType bitwardenProductType) { var plan = StaticStore.GetPlan(planType, bitwardenProductType); Assert.NotNull(plan); From c5b0e584c05cdb6cc1f1abbc0cc2425e620fc3e1 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 09:03:00 +0100 Subject: [PATCH 09/37] only password manager plans are return for now --- src/Api/Controllers/PlansController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index d738e60cfb20..259aca1d1614 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -1,4 +1,5 @@ using Bit.Api.Models.Response; +using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; @@ -20,7 +21,7 @@ public PlansController(ITaxRateRepository taxRateRepository) [AllowAnonymous] public ListResponseModel Get() { - var data = StaticStore.Plans; + var data = StaticStore.Plans.Where(x=>x.BitwardenProduct == BitwardenProductType.PasswordManager); var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } From 06cfa66c29ca7996e9af372d146fa06e0b06979a Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 09:09:33 +0100 Subject: [PATCH 10/37] format whitespace --- src/Api/Controllers/PlansController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index 259aca1d1614..de6b1ab70f17 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -21,7 +21,7 @@ public PlansController(ITaxRateRepository taxRateRepository) [AllowAnonymous] public ListResponseModel Get() { - var data = StaticStore.Plans.Where(x=>x.BitwardenProduct == BitwardenProductType.PasswordManager); + var data = StaticStore.Plans.Where(x => x.BitwardenProduct == BitwardenProductType.PasswordManager); var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } From 110ee1d71ab40d5e863dd6bc30565a093e6bb582 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 12:59:43 +0100 Subject: [PATCH 11/37] Resolve the test issue --- src/Core/Utilities/SecretsManagerPlanStore.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Core/Utilities/SecretsManagerPlanStore.cs b/src/Core/Utilities/SecretsManagerPlanStore.cs index 857740e6dd17..233cd116fa8d 100644 --- a/src/Core/Utilities/SecretsManagerPlanStore.cs +++ b/src/Core/Utilities/SecretsManagerPlanStore.cs @@ -175,8 +175,7 @@ public static IEnumerable CreatePlan() UpgradeSortOrder = -1, // Always the lowest plan, cannot be upgraded to DisplaySortOrder = -1, AllowSeatAutoscale = false, - }, - new Plan { Type = PlanType.Custom, AllowSeatAutoscale = true, }, + } }; } } From cc695b23156b462cb51f88dbdc4aca014d6213d5 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Fri, 19 May 2023 19:01:15 +0100 Subject: [PATCH 12/37] Fixing the failing test --- test/Core.Test/Utilities/StaticStoreTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index c4ca7336469a..0388ce5dfbde 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -12,7 +12,7 @@ public void StaticStore_Initialization_Success() var plans = StaticStore.Plans; Assert.NotNull(plans); Assert.NotEmpty(plans); - Assert.Equal(18, plans.Count()); + Assert.Equal(17, plans.Count()); } [Theory] From ca38d32288dc5a07e2b60cf6ff1110a741c5bed4 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 20:35:07 +0100 Subject: [PATCH 13/37] Refactoring the Plan separation --- src/Api/Controllers/PlansController.cs | 11 ++++++++++- src/Core/Core.csproj | 9 --------- src/Core/Utilities/StaticStore.cs | 8 +++++--- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index de6b1ab70f17..a3bfebfcb0a8 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -21,7 +21,16 @@ public PlansController(ITaxRateRepository taxRateRepository) [AllowAnonymous] public ListResponseModel Get() { - var data = StaticStore.Plans.Where(x => x.BitwardenProduct == BitwardenProductType.PasswordManager); + var data = StaticStore.PasswordManagerPlans; + var responses = data.Select(plan => new PlanResponseModel(plan)); + return new ListResponseModel(responses); + } + + [HttpGet("all")] + [AllowAnonymous] + public ListResponseModel GetAllPlans() + { + var data = StaticStore.Plans; var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index d2fed4bac748..5a6b771c8fa2 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -17,7 +17,6 @@ - @@ -64,12 +63,4 @@ - - - - - - - - diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index c63ae4c2b26a..0a5f04255b48 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -106,16 +106,18 @@ static StaticStore() #region Plans - var passwordManagerPlans = PasswordManagerPlanStore.CreatePlan(); - var secretManagerPlans = SecretsManagerPlanStore.CreatePlan(); + PasswordManagerPlans = PasswordManagerPlanStore.CreatePlan(); + SecretManagerPlans = SecretsManagerPlanStore.CreatePlan(); - Plans = passwordManagerPlans.Concat(secretManagerPlans); + Plans = PasswordManagerPlans.Concat(SecretManagerPlans); #endregion } public static IDictionary> GlobalDomains { get; set; } + public static IEnumerable PasswordManagerPlans { get; set; } + public static IEnumerable SecretManagerPlans { get; set; } public static IEnumerable Plans { get; set; } public static IEnumerable SponsoredPlans { get; set; } = new[] { From 8a00550e1e7c461c517d9b9fe913223b026e6c36 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 20:53:16 +0100 Subject: [PATCH 14/37] add a unit test for SingleOrDefault --- src/Core/Utilities/StaticStore.cs | 2 +- test/Core.Test/Utilities/StaticStoreTests.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 0a5f04255b48..e1cad89afd92 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -132,7 +132,7 @@ static StaticStore() } }; public static Plan GetPlan(PlanType planType, BitwardenProductType bitwardenProduct = BitwardenProductType.PasswordManager) => - Plans.FirstOrDefault(p => p.Type == planType && p.BitwardenProduct == bitwardenProduct); + Plans.SingleOrDefault(p => p.Type == planType && p.BitwardenProduct == bitwardenProduct); public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); } diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 0388ce5dfbde..7499288ddc5e 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,5 +1,6 @@ using Bit.Core.Enums; using Bit.Core.Utilities; +using Bit.Core.Models.StaticStore; namespace Bit.Core.Test.Utilities; using Xunit; @@ -42,4 +43,17 @@ public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerP Assert.NotNull(plan); Assert.Equal(bitwardenProductType, plan.BitwardenProduct); } + + [Theory] + [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] + public void StaticStore_AddDuplicatePlans_SingleOrDefaultThrowsException(PlanType planType, BitwardenProductType bitwardenProductType) + { + var plansStore = new List + { + new Plan { Type = PlanType.EnterpriseAnnually, BitwardenProduct = BitwardenProductType.PasswordManager }, + new Plan { Type = PlanType.EnterpriseAnnually, BitwardenProduct = BitwardenProductType.PasswordManager } + }; + + Assert.Throws(() => plansStore.SingleOrDefault(p=>p.Type == planType && p.BitwardenProduct ==bitwardenProductType)); + } } From 43bb9d2a465bd0ac3dc054c50fdbbf5421caaab6 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 21:29:02 +0100 Subject: [PATCH 15/37] Fix the whitespace format --- src/Api/Controllers/PlansController.cs | 2 +- test/Core.Test/Utilities/StaticStoreTests.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index a3bfebfcb0a8..dd479c44a8fd 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -25,7 +25,7 @@ public ListResponseModel Get() var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } - + [HttpGet("all")] [AllowAnonymous] public ListResponseModel GetAllPlans() diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 7499288ddc5e..877c2e1962a4 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -43,7 +43,7 @@ public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerP Assert.NotNull(plan); Assert.Equal(bitwardenProductType, plan.BitwardenProduct); } - + [Theory] [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] public void StaticStore_AddDuplicatePlans_SingleOrDefaultThrowsException(PlanType planType, BitwardenProductType bitwardenProductType) @@ -53,7 +53,7 @@ public void StaticStore_AddDuplicatePlans_SingleOrDefaultThrowsException(PlanTyp new Plan { Type = PlanType.EnterpriseAnnually, BitwardenProduct = BitwardenProductType.PasswordManager }, new Plan { Type = PlanType.EnterpriseAnnually, BitwardenProduct = BitwardenProductType.PasswordManager } }; - - Assert.Throws(() => plansStore.SingleOrDefault(p=>p.Type == planType && p.BitwardenProduct ==bitwardenProductType)); + + Assert.Throws(() => plansStore.SingleOrDefault(p => p.Type == planType && p.BitwardenProduct == bitwardenProductType)); } } From 2b5c9a11e31c5a6d39ff1d29dd42e71d217d7f07 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:08:21 +0100 Subject: [PATCH 16/37] Separate the PM and SM plans --- .../Controllers/OrganizationsController.cs | 2 +- src/Api/Controllers/PlansController.cs | 9 +++++ .../OrganizationResponseModel.cs | 2 +- .../ProfileOrganizationResponseModel.cs | 2 +- ...rofileProviderOrganizationResponseModel.cs | 2 +- src/Billing/Controllers/StripeController.cs | 2 +- .../Cloud/CloudSyncSponsorshipsCommand.cs | 2 +- .../Cloud/SetUpSponsorshipCommand.cs | 2 +- .../Cloud/ValidateSponsorshipCommand.cs | 2 +- .../CreateSponsorshipCommand.cs | 2 +- .../Implementations/OrganizationService.cs | 16 ++++----- .../Implementations/StripePaymentService.cs | 2 +- src/Core/Utilities/StaticStore.cs | 14 +++++--- ...OrganizationSponsorshipsControllerTests.cs | 6 ++-- .../Vault/Controllers/SyncControllerTests.cs | 2 +- .../AutoFixture/OrganizationFixtures.cs | 4 +-- .../FamiliesForEnterpriseTestsBase.cs | 8 ++--- .../Services/StripePaymentServiceTests.cs | 20 +++++------ test/Core.Test/Utilities/StaticStoreTests.cs | 36 ++++++++++++------- 19 files changed, 79 insertions(+), 56 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 70084a66b687..5a1569c98ed1 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -487,7 +487,7 @@ public async Task ApiKey(string id, [FromBody] Organization if (model.Type == OrganizationApiKeyType.BillingSync || model.Type == OrganizationApiKeyType.Scim) { // Non-enterprise orgs should not be able to create or view an apikey of billing sync/scim key types - var plan = StaticStore.GetPlan(organization.PlanType); + var plan = StaticStore.GetPasswordManagerPlan(organization.PlanType); if (plan.Product != ProductType.Enterprise) { throw new NotFoundException(); diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index dd479c44a8fd..4edfab10d1d7 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -34,6 +34,15 @@ public ListResponseModel GetAllPlans() var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } + + [HttpGet("sm-plans")] + [AllowAnonymous] + public ListResponseModel GetSecretsManagerPlans() + { + var data = StaticStore.SecretManagerPlans; + var responses = data.Select(plan => new PlanResponseModel(plan)); + return new ListResponseModel(responses); + } [HttpGet("sales-tax-rates")] public async Task> GetTaxRates() diff --git a/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs index 8b9d94d887ed..9f26c9a9e5c9 100644 --- a/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs @@ -26,7 +26,7 @@ public OrganizationResponseModel(Organization organization, string obj = "organi BusinessCountry = organization.BusinessCountry; BusinessTaxNumber = organization.BusinessTaxNumber; BillingEmail = organization.BillingEmail; - Plan = new PlanResponseModel(StaticStore.Plans.FirstOrDefault(plan => plan.Type == organization.PlanType)); + Plan = new PlanResponseModel(StaticStore.PasswordManagerPlans.FirstOrDefault(plan => plan.Type == organization.PlanType)); PlanType = organization.PlanType; Seats = organization.Seats; MaxAutoscaleSeats = organization.MaxAutoscaleSeats; diff --git a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs index 215fc7a23a70..bb57be69d896 100644 --- a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs @@ -54,7 +54,7 @@ public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails orga FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); - PlanProductType = StaticStore.GetPlan(organization.PlanType).Product; + PlanProductType = StaticStore.GetPasswordManagerPlan(organization.PlanType).Product; FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; diff --git a/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs index d08e3b1ea4f6..9dccad8930c5 100644 --- a/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -42,6 +42,6 @@ public ProfileProviderOrganizationResponseModel(ProviderUserOrganizationDetails UserId = organization.UserId?.ToString(); ProviderId = organization.ProviderId?.ToString(); ProviderName = organization.ProviderName; - PlanProductType = StaticStore.GetPlan(organization.PlanType).Product; + PlanProductType = StaticStore.GetPasswordManagerPlan(organization.PlanType).Product; } } diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index fff638bf2045..8562bd0a2438 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -417,7 +417,7 @@ await _transactionRepository.CreateAsync(new Transaction // org if (ids.Item1.HasValue) { - if (subscription.Items.Any(i => StaticStore.Plans.Any(p => p.StripePlanId == i.Plan.Id))) + if (subscription.Items.Any(i => StaticStore.PasswordManagerPlans.Any(p => p.StripePlanId == i.Plan.Id))) { await _organizationService.EnableAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs index d0569278bbaa..5c0f1474abd7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs @@ -54,7 +54,7 @@ await DoSyncAsync(sponsoringOrg, sponsorshipsData) : { var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductType; if (requiredSponsoringProductType == null - || StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) + || StaticStore.GetPasswordManagerPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) { continue; // prevent unsupported sponsorships } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs index 9230e7d13dbd..81a8bac96632 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs @@ -51,7 +51,7 @@ public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductType; if (requiredSponsoredProductType == null || sponsoredOrganization == null || - StaticStore.GetPlan(sponsoredOrganization.PlanType).Product != requiredSponsoredProductType.Value) + StaticStore.GetPasswordManagerPlan(sponsoredOrganization.PlanType).Product != requiredSponsoredProductType.Value) { throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); } diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs index 3f2d7af5ebd5..af2f0af65d41 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs @@ -56,7 +56,7 @@ public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) return false; } - var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); + var sponsoringOrgPlan = Utilities.StaticStore.GetPasswordManagerPlan(sponsoringOrganization.PlanType); if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization) || sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product || existingSponsorship.ToDelete || diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index 69e6c3232c45..bfc33ebe879a 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -32,7 +32,7 @@ public async Task CreateSponsorshipAsync(Organization s var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductType; if (requiredSponsoringProductType == null || sponsoringOrg == null || - StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) + StaticStore.GetPasswordManagerPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) { throw new BadRequestException("Specified Organization cannot sponsor other organizations."); } diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 82d39a9d6de1..94f03c89716c 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -179,13 +179,13 @@ public async Task> UpgradePlanAsync(Guid organizationId, Org throw new BadRequestException("Your account has no payment method available."); } - var existingPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var existingPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); if (existingPlan == null) { throw new BadRequestException("Existing plan not found."); } - var newPlan = StaticStore.Plans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); + var newPlan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == upgrade.Plan && !p.Disabled); if (newPlan == null) { throw new BadRequestException("Plan not found."); @@ -379,7 +379,7 @@ public async Task AdjustStorageAsync(Guid organizationId, short storageA throw new NotFoundException(); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); if (plan == null) { throw new BadRequestException("Existing plan not found."); @@ -437,7 +437,7 @@ private async Task UpdateAutoscalingAsync(Organization organization, int? maxAut throw new BadRequestException($"Cannot set max seat autoscaling below current seat count."); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); if (plan == null) { throw new BadRequestException("Existing plan not found."); @@ -489,7 +489,7 @@ private async Task AdjustSeatsAsync(Organization organization, int seatA throw new BadRequestException("No subscription found."); } - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); if (plan == null) { throw new BadRequestException("Existing plan not found."); @@ -607,7 +607,7 @@ public async Task VerifyBankAsync(Guid organizationId, int amount1, int amount2) public async Task> SignUpAsync(OrganizationSignup signup, bool provider = false) { - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == signup.Plan); + var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == signup.Plan); if (plan is not { LegacyYear: null }) { throw new BadRequestException("Invalid plan selected."); @@ -712,7 +712,7 @@ public async Task> SignUpAsync( } if (license.PlanType != PlanType.Custom && - StaticStore.Plans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null) + StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == license.PlanType && !p.Disabled) == null) { throw new BadRequestException("Plan not found."); } @@ -2519,7 +2519,7 @@ static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(Organ public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted) { - var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType); + var plan = StaticStore.PasswordManagerPlans.FirstOrDefault(p => p.Type == organization.PlanType); if (plan is not { LegacyYear: null }) { throw new BadRequestException("Invalid plan selected."); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index b2f5e87f8436..d7a7901336c2 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -201,7 +201,7 @@ public async Task PurchaseOrganizationAsync(Organization org, PaymentMet private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { - var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); + var existingPlan = Utilities.StaticStore.GetPasswordManagerPlan(org.PlanType); var sponsoredPlan = sponsorship != null ? Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : null; diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index e1cad89afd92..8db7530d83f4 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -116,9 +116,9 @@ static StaticStore() } public static IDictionary> GlobalDomains { get; set; } - public static IEnumerable PasswordManagerPlans { get; set; } - public static IEnumerable SecretManagerPlans { get; set; } public static IEnumerable Plans { get; set; } + public static IEnumerable SecretManagerPlans { get; set; } + public static IEnumerable PasswordManagerPlans { get; set; } public static IEnumerable SponsoredPlans { get; set; } = new[] { new SponsoredPlan @@ -128,11 +128,15 @@ static StaticStore() SponsoringProductType = ProductType.Enterprise, StripePlanId = "2021-family-for-enterprise-annually", UsersCanSponsor = (OrganizationUserOrganizationDetails org) => - GetPlan(org.PlanType).Product == ProductType.Enterprise, + GetPasswordManagerPlan(org.PlanType).Product == ProductType.Enterprise, } }; - public static Plan GetPlan(PlanType planType, BitwardenProductType bitwardenProduct = BitwardenProductType.PasswordManager) => - Plans.SingleOrDefault(p => p.Type == planType && p.BitwardenProduct == bitwardenProduct); + public static Plan GetPasswordManagerPlan(PlanType planType) => + PasswordManagerPlans.SingleOrDefault(p => p.Type == planType); + + public static Plan GetSecretsManagerPlan(PlanType planType) => + SecretManagerPlans.SingleOrDefault(p => p.Type == planType); + public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); } diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index e58add5ef06f..99bc1df636cf 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -20,11 +20,11 @@ namespace Bit.Api.Test.Controllers; public class OrganizationSponsorshipsControllerTests { public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); public static IEnumerable NonConfirmedOrganizationUsersStatuses => Enum.GetValues() diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index d27a41ece670..0e1629192622 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -305,7 +305,7 @@ public async Task Get_ProviderPlanTypeProperlyPopulated(User user, if (matchedProviderUserOrgDetails != null) { - var providerOrgProductType = StaticStore.GetPlan(matchedProviderUserOrgDetails.PlanType).Product; + var providerOrgProductType = StaticStore.GetPasswordManagerPlan(matchedProviderUserOrgDetails.PlanType).Product; Assert.Equal(providerOrgProductType, profProviderOrg.PlanProductType); } } diff --git a/test/Core.Test/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AutoFixture/OrganizationFixtures.cs index 7d0044c36cee..ee0c13dd202f 100644 --- a/test/Core.Test/AutoFixture/OrganizationFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationFixtures.cs @@ -65,7 +65,7 @@ internal class PaidOrganization : ICustomization public PlanType CheckedPlanType { get; set; } public void Customize(IFixture fixture) { - var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList(); + var validUpgradePlans = StaticStore.PasswordManagerPlans.Where(p => p.Type != PlanType.Free && p.LegacyYear == null).OrderBy(p => p.UpgradeSortOrder).Select(p => p.Type).ToList(); var lowestActivePaidPlan = validUpgradePlans.First(); CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType; validUpgradePlans.Remove(lowestActivePaidPlan); @@ -93,7 +93,7 @@ public void Customize(IFixture fixture) .With(o => o.PlanType, PlanType.Free)); var plansToIgnore = new List { PlanType.Free, PlanType.Custom }; - var selectedPlan = StaticStore.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled); + var selectedPlan = StaticStore.PasswordManagerPlans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled); fixture.Customize(composer => composer .With(ou => ou.Plan, selectedPlan.Type) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs index e49b095d76fa..a3fce8b3de2b 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs @@ -6,16 +6,16 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesFo public abstract class FamiliesForEnterpriseTestsBase { public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable FamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product == ProductType.Families).Select(p => new object[] { p }); public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPasswordManagerPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); public static IEnumerable NonConfirmedOrganizationUsersStatuses => Enum.GetValues() diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 82131d874317..c43e99fb9871 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -39,7 +39,7 @@ public async void PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMet [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer @@ -89,7 +89,7 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer @@ -141,7 +141,7 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); paymentToken = "pm_" + paymentToken; var stripeAdapter = sutProvider.GetDependency(); @@ -194,7 +194,7 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer @@ -223,7 +223,7 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); paymentToken = "pm_" + paymentToken; var stripeAdapter = sutProvider.GetDependency(); @@ -256,7 +256,7 @@ public async void PurchaseOrganizationAsync_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer @@ -287,7 +287,7 @@ public async void PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var stripeAdapter = sutProvider.GetDependency(); stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer @@ -346,7 +346,7 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var customerResult = Substitute.For>(); customerResult.IsSuccess().Returns(false); @@ -363,7 +363,7 @@ public async void PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { - var plan = StaticStore.Plans.First(p => p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); paymentToken = "pm_" + paymentToken; var stripeAdapter = sutProvider.GetDependency(); @@ -425,7 +425,7 @@ public async void UpgradeFreeOrganizationAsync_Success(SutProvider p.Type == PlanType.EnterpriseAnnually); + var plan = StaticStore.PasswordManagerPlans.First(p => p.Type == PlanType.EnterpriseAnnually); var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, 0, 0, false, taxInfo); Assert.Null(result); diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 877c2e1962a4..cba0688ad637 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -18,30 +18,40 @@ public void StaticStore_Initialization_Success() [Theory] [InlineData(PlanType.EnterpriseAnnually)] - public void StaticStore_GetPlanByPlanType_Success(PlanType planType) + public void StaticStore_GetPasswordManagerPlanByPlanType_Success(PlanType planType) { - var plan = StaticStore.GetPlan(planType); - + var plan = StaticStore.GetPasswordManagerPlan(planType); + Assert.NotNull(plan); Assert.Equal(planType, plan.Type); } - + [Theory] - [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] - public void StaticStore_GetPlanPlanTypeOnly_ReturnsPasswordManagerPlans(PlanType planType, BitwardenProductType bitwardenProductType) + [InlineData(PlanType.EnterpriseAnnually)] + public void StaticStore_GetSecretsManagerPlanByPlanType_Success(PlanType planType) { - var plan = StaticStore.GetPlan(planType); + var plan = StaticStore.GetSecretsManagerPlan(planType); + Assert.NotNull(plan); - Assert.Equal(bitwardenProductType, plan.BitwardenProduct); + Assert.Equal(planType, plan.Type); } - + [Theory] - [InlineData(PlanType.EnterpriseAnnually, BitwardenProductType.PasswordManager)] - public void StaticStore_GetPlanPlanTypBitwardenProductType_ReturnsSecretManagerPlans(PlanType planType, BitwardenProductType bitwardenProductType) + [InlineData(PlanType.EnterpriseAnnually)] + public void StaticStore_GetPasswordManagerPlan_ReturnsPasswordManagerPlans(PlanType planType) + { + var plan = StaticStore.GetPasswordManagerPlan(planType); + Assert.NotNull(plan); + Assert.Equal(BitwardenProductType.PasswordManager, plan.BitwardenProduct); + } + + [Theory] + [InlineData(PlanType.EnterpriseAnnually)] + public void StaticStore_GetSecretsManagerPlan_ReturnsSecretManagerPlans(PlanType planType) { - var plan = StaticStore.GetPlan(planType, bitwardenProductType); + var plan = StaticStore.GetSecretsManagerPlan(planType); Assert.NotNull(plan); - Assert.Equal(bitwardenProductType, plan.BitwardenProduct); + Assert.Equal(BitwardenProductType.SecretsManager, plan.BitwardenProduct); } [Theory] From 9ae74a14fc331f3ec721752b84b3a4af2e3e78b1 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:09:46 +0100 Subject: [PATCH 17/37] Fixing the whitespace --- src/Api/Controllers/PlansController.cs | 2 +- src/Core/Utilities/StaticStore.cs | 4 ++-- test/Core.Test/Utilities/StaticStoreTests.cs | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index 4edfab10d1d7..69479b0cd328 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -34,7 +34,7 @@ public ListResponseModel GetAllPlans() var responses = data.Select(plan => new PlanResponseModel(plan)); return new ListResponseModel(responses); } - + [HttpGet("sm-plans")] [AllowAnonymous] public ListResponseModel GetSecretsManagerPlans() diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 8db7530d83f4..c5183296a9e6 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -133,10 +133,10 @@ static StaticStore() }; public static Plan GetPasswordManagerPlan(PlanType planType) => PasswordManagerPlans.SingleOrDefault(p => p.Type == planType); - + public static Plan GetSecretsManagerPlan(PlanType planType) => SecretManagerPlans.SingleOrDefault(p => p.Type == planType); - + public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); } diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index cba0688ad637..2f8843173d02 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -21,21 +21,21 @@ public void StaticStore_Initialization_Success() public void StaticStore_GetPasswordManagerPlanByPlanType_Success(PlanType planType) { var plan = StaticStore.GetPasswordManagerPlan(planType); - + Assert.NotNull(plan); Assert.Equal(planType, plan.Type); } - + [Theory] [InlineData(PlanType.EnterpriseAnnually)] public void StaticStore_GetSecretsManagerPlanByPlanType_Success(PlanType planType) { var plan = StaticStore.GetSecretsManagerPlan(planType); - + Assert.NotNull(plan); Assert.Equal(planType, plan.Type); } - + [Theory] [InlineData(PlanType.EnterpriseAnnually)] public void StaticStore_GetPasswordManagerPlan_ReturnsPasswordManagerPlans(PlanType planType) @@ -44,7 +44,7 @@ public void StaticStore_GetPasswordManagerPlan_ReturnsPasswordManagerPlans(PlanT Assert.NotNull(plan); Assert.Equal(BitwardenProductType.PasswordManager, plan.BitwardenProduct); } - + [Theory] [InlineData(PlanType.EnterpriseAnnually)] public void StaticStore_GetSecretsManagerPlan_ReturnsSecretManagerPlans(PlanType planType) From c6e821c8672ce7582bab795ef848648f274fde83 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:14:48 +0100 Subject: [PATCH 18/37] Remove unnecessary directive --- src/Api/Controllers/PlansController.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/Controllers/PlansController.cs b/src/Api/Controllers/PlansController.cs index 69479b0cd328..a7ae7c0b3060 100644 --- a/src/Api/Controllers/PlansController.cs +++ b/src/Api/Controllers/PlansController.cs @@ -1,5 +1,4 @@ using Bit.Api.Models.Response; -using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; From cbfd49c0c7ff0b00645c9d5d4d05682dccb19a99 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:25:45 +0100 Subject: [PATCH 19/37] Fix imports ordering --- test/Core.Test/Utilities/StaticStoreTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index 2f8843173d02..ad49ca026a2b 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,9 +1,10 @@ using Bit.Core.Enums; using Bit.Core.Utilities; using Bit.Core.Models.StaticStore; +using Xunit; namespace Bit.Core.Test.Utilities; -using Xunit; + public class StaticStoreTests { From 10cc83763bff4359df0b8cf1503606f2ee2ecaf8 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:29:51 +0100 Subject: [PATCH 20/37] Fix imports ordering --- test/Core.Test/Utilities/StaticStoreTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index ad49ca026a2b..dbaa6b9bedb4 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,6 +1,6 @@ -using Bit.Core.Enums; -using Bit.Core.Utilities; +using Bit.Core.Utilities; using Bit.Core.Models.StaticStore; +using Bit.Core.Enums; using Xunit; namespace Bit.Core.Test.Utilities; From a607a0514ae7fe205972789c78a4978ca2142b0a Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:43:49 +0100 Subject: [PATCH 21/37] Resolve imports ordering --- test/Core.Test/Utilities/StaticStoreTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index dbaa6b9bedb4..c5cd702fe3f4 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,5 +1,5 @@ -using Bit.Core.Utilities; -using Bit.Core.Models.StaticStore; +using Bit.Core.Models.StaticStore; +using Bit.Core.Utilities; using Bit.Core.Enums; using Xunit; From 1403d28eb05a1e8e0b6c074fe33495f398a81197 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Tue, 23 May 2023 23:51:51 +0100 Subject: [PATCH 22/37] Fixing imports ordering --- test/Core.Test/Utilities/StaticStoreTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index c5cd702fe3f4..d30a0e6c733b 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.StaticStore; +using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; using Bit.Core.Utilities; -using Bit.Core.Enums; using Xunit; namespace Bit.Core.Test.Utilities; From 8413ba957ec6e419f4dcbdfba850287e2de3549f Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 11:13:12 +0100 Subject: [PATCH 23/37] Changes for the purchase organization --- .../Business/SubscriptionCreateOptions.cs | 84 ++++ src/Core/Services/IPaymentService.cs | 3 + .../Implementations/StripePaymentService.cs | 157 +++++++ .../Services/StripePaymentServiceTests.cs | 385 ++++++++++++++++++ 4 files changed, 629 insertions(+) diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 4964a625c8c4..c94252a535f5 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Stripe; namespace Bit.Core.Models.Business; @@ -54,6 +55,80 @@ public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan pl DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; } } + + public OrganizationSubscriptionOptionsBase(Organization org, IEnumerable plans, TaxInfo taxInfo, int additionalSeats + , int additionalStorageGb, bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0) + { + Items = new List(); + Metadata = new Dictionary + { + [org.GatewayIdField()] = org.Id.ToString() + }; + + foreach (var plan in plans) + { + if (plan.StripePlanId != null) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePlanId, + Quantity = 1 + }); + } + + if (additionalSeats > 0 && plan.StripeSeatPlanId != null && plan.BitwardenProduct == BitwardenProductType.PasswordManager) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripeSeatPlanId, + Quantity = additionalSeats + }); + } + + if (additionalStorageGb > 0 && plan.BitwardenProduct == BitwardenProductType.PasswordManager) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripeStoragePlanId, + Quantity = additionalStorageGb + }); + } + + if (additionalSmSeats > 0 && plan.StripePlanId != null && plan.BitwardenProduct == BitwardenProductType.SecretsManager) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePlanId, + Quantity = additionalSmSeats + }); + } + + if (additionalServiceAccount > 0 && plan.StripePlanId != null && plan.BitwardenProduct == BitwardenProductType.SecretsManager) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePlanId, + Quantity = additionalServiceAccount + }); + } + + if (premiumAccessAddon && plan.StripePremiumAccessPlanId != null && plan.BitwardenProduct == BitwardenProductType.PasswordManager) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePremiumAccessPlanId, + Quantity = 1 + }); + } + } + + + if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) + { + DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; + } + + } } public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase @@ -67,6 +142,15 @@ public OrganizationPurchaseSubscriptionOptions( OffSession = true; TrialPeriodDays = plan.TrialPeriodDays; } + + public OrganizationPurchaseSubscriptionOptions(Organization org, IEnumerable plans, TaxInfo taxInfo, int additionalSeats = 0 + , int additionalStorageGb = 0, bool premiumAccessAddon = false + , int additionalSmSeats = 0, int additionalServiceAccount = 0) : + base(org, plans, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon, additionalSmSeats, additionalServiceAccount) + { + OffSession = true; + TrialPeriodDays = plans.FirstOrDefault().TrialPeriodDays; + } } public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 95c6f2590888..79913b6ddd23 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -11,6 +11,9 @@ public interface IPaymentService Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false); + Task PurchaseOrganizationWithProductsAsync(Organization org, PaymentMethodType paymentMethodType, + string paymentToken, IEnumerable plans, short additionalStorageGb, int additionalSeats, + bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0, int additionalServiceAccount = 0); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index ba995615349d..1142696af471 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -199,6 +199,163 @@ public async Task PurchaseOrganizationAsync(Organization org, PaymentMet } } + public async Task PurchaseOrganizationWithProductsAsync(Organization org, PaymentMethodType paymentMethodType, + string paymentToken, IEnumerable plans, short additionalStorageGb, + int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, + int additionalSmSeats = 0, int additionalServiceAccount = 0) + { + Braintree.Customer braintreeCustomer = null; + string stipeCustomerSourceToken = null; + string stipeCustomerPaymentMethodId = null; + var stripeCustomerMetadata = new Dictionary(); + var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card || + paymentMethodType == PaymentMethodType.BankAccount; + + if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken)) + { + if (paymentToken.StartsWith("pm_")) + { + stipeCustomerPaymentMethodId = paymentToken; + } + else + { + stipeCustomerSourceToken = paymentToken; + } + } + else if (paymentMethodType == PaymentMethodType.PayPal) + { + var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false); + var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest + { + PaymentMethodNonce = paymentToken, + Email = org.BillingEmail, + Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix, + CustomFields = new Dictionary + { + [org.BraintreeIdField()] = org.Id.ToString() + } + }); + + if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) + { + throw new GatewayException("Failed to create PayPal customer record."); + } + + braintreeCustomer = customerResult.Target; + stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); + } + else + { + throw new GatewayException("Payment method is not supported at this time."); + } + + if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode)) + { + var taxRateSearch = new TaxRate + { + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode + }; + var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch); + + // should only be one tax rate per country/zip combo + var taxRate = taxRates.FirstOrDefault(); + if (taxRate != null) + { + taxInfo.StripeTaxRateId = taxRate.Id; + } + } + + var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plans, taxInfo, additionalSeats, additionalStorageGb + , premiumAccessAddon, additionalSmSeats, additionalServiceAccount); + + Stripe.Customer customer = null; + Stripe.Subscription subscription; + try + { + customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions + { + Description = org.BusinessName, + Email = org.BillingEmail, + Source = stipeCustomerSourceToken, + PaymentMethod = stipeCustomerPaymentMethodId, + Metadata = stripeCustomerMetadata, + InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions + { + DefaultPaymentMethod = stipeCustomerPaymentMethodId, + CustomFields = new List + { + new Stripe.CustomerInvoiceSettingsCustomFieldOptions() + { + Name = org.SubscriberType(), + Value = GetFirstThirtyCharacters(org.SubscriberName()), + }, + }, + }, + Coupon = provider ? ProviderDiscountId : null, + Address = new Stripe.AddressOptions + { + Country = taxInfo.BillingAddressCountry, + PostalCode = taxInfo.BillingAddressPostalCode, + // Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead. + Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, + Line2 = taxInfo.BillingAddressLine2, + City = taxInfo.BillingAddressCity, + State = taxInfo.BillingAddressState, + }, + TaxIdData = !taxInfo.HasTaxId ? null : new List + { + new Stripe.CustomerTaxIdDataOptions + { + Type = taxInfo.TaxIdType, + Value = taxInfo.TaxIdNumber, + }, + }, + }); + subCreateOptions.AddExpand("latest_invoice.payment_intent"); + subCreateOptions.Customer = customer.Id; + subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions); + if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null) + { + if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method") + { + await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions()); + throw new GatewayException("Payment method was declined."); + } + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating customer, walking back operation."); + if (customer != null) + { + await _stripeAdapter.CustomerDeleteAsync(customer.Id); + } + if (braintreeCustomer != null) + { + await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); + } + throw; + } + + org.Gateway = GatewayType.Stripe; + org.GatewayCustomerId = customer.Id; + org.GatewaySubscriptionId = subscription.Id; + + if (subscription.Status == "incomplete" && + subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") + { + org.Enabled = false; + return subscription.LatestInvoice.PaymentIntent.ClientSecret; + } + else + { + org.Enabled = true; + org.ExpirationDate = subscription.CurrentPeriodEnd; + return null; + } + } + private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { var existingPlan = Utilities.StaticStore.GetPasswordManagerPlan(org.PlanType); diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index c43e99fb9871..cfe9c83cc467 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -36,6 +36,22 @@ public async void PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMet Assert.Equal("Payment method is not supported at this time.", exception.Message); } + [Theory] + [BitAutoData(PaymentMethodType.BitPay)] + [BitAutoData(PaymentMethodType.BitPay)] + [BitAutoData(PaymentMethodType.Credit)] + [BitAutoData(PaymentMethodType.WireTransfer)] + [BitAutoData(PaymentMethodType.AppleInApp)] + [BitAutoData(PaymentMethodType.GoogleInApp)] + [BitAutoData(PaymentMethodType.Check)] + public async void PurchaseOrganizationWithProductsAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PurchaseOrganizationWithProductsAsync(null, paymentMethodType, null, null, 0, 0, false, null)); + + Assert.Equal("Payment method is not supported at this time.", exception.Message); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true) { @@ -86,6 +102,56 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo, provider); + + Assert.Null(result); + Assert.Equal(GatewayType.Stripe, organization.Gateway); + Assert.Equal("C-1", organization.GatewayCustomerId); + Assert.Equal("S-1", organization.GatewaySubscriptionId); + Assert.True(organization.Enabled); + Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); + + await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => + c.Description == organization.BusinessName && + c.Email == organization.BillingEmail && + c.Source == paymentToken && + c.PaymentMethod == null && + c.Coupon == "msp-discount-35" && + !c.Metadata.Any() && + c.InvoiceSettings.DefaultPaymentMethod == null && + c.Address.Country == taxInfo.BillingAddressCountry && + c.Address.PostalCode == taxInfo.BillingAddressPostalCode && + c.Address.Line1 == taxInfo.BillingAddressLine1 && + c.Address.Line2 == taxInfo.BillingAddressLine2 && + c.Address.City == taxInfo.BillingAddressCity && + c.Address.State == taxInfo.BillingAddressState && + c.TaxIdData == null + )); + + await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => + s.Customer == "C-1" && + s.Expand[0] == "latest_invoice.payment_intent" && + s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && + s.Items.Count == 0 + )); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -138,6 +204,58 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo); + + Assert.Null(result); + Assert.Equal(GatewayType.Stripe, organization.Gateway); + Assert.Equal("C-1", organization.GatewayCustomerId); + Assert.Equal("S-1", organization.GatewaySubscriptionId); + Assert.True(organization.Enabled); + Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); + var res = organization.SubscriberName(); + await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => + c.Description == organization.BusinessName && + c.Email == organization.BillingEmail && + c.Source == paymentToken && + c.PaymentMethod == null && + !c.Metadata.Any() && + c.InvoiceSettings.DefaultPaymentMethod == null && + c.InvoiceSettings.CustomFields != null && + c.InvoiceSettings.CustomFields[0].Name == "Organization" && + c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) && + c.Address.Country == taxInfo.BillingAddressCountry && + c.Address.PostalCode == taxInfo.BillingAddressPostalCode && + c.Address.Line1 == taxInfo.BillingAddressLine1 && + c.Address.Line2 == taxInfo.BillingAddressLine2 && + c.Address.City == taxInfo.BillingAddressCity && + c.Address.State == taxInfo.BillingAddressState && + c.TaxIdData == null + )); + + await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => + s.Customer == "C-1" && + s.Expand[0] == "latest_invoice.payment_intent" && + s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && + s.Items.Count == 0 + )); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_PM(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -191,6 +309,59 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + paymentToken = "pm_" + paymentToken; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo); + + Assert.Null(result); + Assert.Equal(GatewayType.Stripe, organization.Gateway); + Assert.Equal("C-1", organization.GatewayCustomerId); + Assert.Equal("S-1", organization.GatewaySubscriptionId); + Assert.True(organization.Enabled); + Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); + + await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => + c.Description == organization.BusinessName && + c.Email == organization.BillingEmail && + c.Source == null && + c.PaymentMethod == paymentToken && + !c.Metadata.Any() && + c.InvoiceSettings.DefaultPaymentMethod == paymentToken && + c.InvoiceSettings.CustomFields != null && + c.InvoiceSettings.CustomFields[0].Name == "Organization" && + c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) && + c.Address.Country == taxInfo.BillingAddressCountry && + c.Address.PostalCode == taxInfo.BillingAddressPostalCode && + c.Address.Line1 == taxInfo.BillingAddressLine1 && + c.Address.Line2 == taxInfo.BillingAddressLine2 && + c.Address.City == taxInfo.BillingAddressCity && + c.Address.State == taxInfo.BillingAddressState && + c.TaxIdData == null + )); + + await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => + s.Customer == "C-1" && + s.Expand[0] == "latest_invoice.payment_intent" && + s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && + s.Items.Count == 0 + )); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_TaxRate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -220,6 +391,35 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + sutProvider.GetDependency().GetByLocationAsync(Arg.Is(t => + t.Country == taxInfo.BillingAddressCountry && t.PostalCode == taxInfo.BillingAddressPostalCode)) + .Returns(new List { new() { Id = "T-1" } }); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo); + + Assert.Null(result); + + await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => + s.DefaultTaxRates.Count == 1 && + s.DefaultTaxRates[0] == "T-1" + )); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -253,6 +453,39 @@ public async void PurchaseOrganizationAsync_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + paymentToken = "pm_" + paymentToken; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + Status = "incomplete", + LatestInvoice = new Stripe.Invoice + { + PaymentIntent = new Stripe.PaymentIntent + { + Status = "requires_payment_method", + }, + }, + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo)); + + Assert.Equal("Payment method was declined.", exception.Message); + + await stripeAdapter.Received(1).CustomerDeleteAsync("C-1"); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -284,6 +517,37 @@ public async void PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + Status = "incomplete", + LatestInvoice = new Stripe.Invoice + { + PaymentIntent = new Stripe.PaymentIntent + { + Status = "requires_action", + ClientSecret = "clientSecret", + }, + }, + }); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.Card, paymentToken, plans, 0, 0, false, taxInfo); + + Assert.Equal("clientSecret", result); + Assert.False(organization.Enabled); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Paypal(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -343,6 +607,66 @@ await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + }); + + var customer = Substitute.For(); + customer.Id.ReturnsForAnyArgs("Braintree-Id"); + customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() }); + var customerResult = Substitute.For>(); + customerResult.IsSuccess().Returns(true); + customerResult.Target.ReturnsForAnyArgs(customer); + + var braintreeGateway = sutProvider.GetDependency(); + braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); + + var result = await sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo); + + Assert.Null(result); + Assert.Equal(GatewayType.Stripe, organization.Gateway); + Assert.Equal("C-1", organization.GatewayCustomerId); + Assert.Equal("S-1", organization.GatewaySubscriptionId); + Assert.True(organization.Enabled); + Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate); + + await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c => + c.Description == organization.BusinessName && + c.Email == organization.BillingEmail && + c.PaymentMethod == null && + c.Metadata.Count == 1 && + c.Metadata["btCustomerId"] == "Braintree-Id" && + c.InvoiceSettings.DefaultPaymentMethod == null && + c.Address.Country == taxInfo.BillingAddressCountry && + c.Address.PostalCode == taxInfo.BillingAddressPostalCode && + c.Address.Line1 == taxInfo.BillingAddressLine1 && + c.Address.Line2 == taxInfo.BillingAddressLine2 && + c.Address.City == taxInfo.BillingAddressCity && + c.Address.State == taxInfo.BillingAddressState && + c.TaxIdData == null + )); + + await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s => + s.Customer == "C-1" && + s.Expand[0] == "latest_invoice.payment_intent" && + s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() && + s.Items.Count == 0 + )); + } + + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -360,6 +684,23 @@ public async void PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + + var customerResult = Substitute.For>(); + customerResult.IsSuccess().Returns(false); + + var braintreeGateway = sutProvider.GetDependency(); + braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo)); + + Assert.Equal("Failed to create PayPal customer record.", exception.Message); + } + [Theory, BitAutoData] public async void PurchaseOrganizationAsync_PayPal_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) { @@ -404,6 +745,50 @@ public async void PurchaseOrganizationAsync_PayPal_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo) + { + var plans = StaticStore.Plans.Where(p => p.Type == PlanType.EnterpriseAnnually); + paymentToken = "pm_" + paymentToken; + + var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer + { + Id = "C-1", + }); + stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription + { + Id = "S-1", + CurrentPeriodEnd = DateTime.Today.AddDays(10), + Status = "incomplete", + LatestInvoice = new Stripe.Invoice + { + PaymentIntent = new Stripe.PaymentIntent + { + Status = "requires_payment_method", + }, + }, + }); + + var customer = Substitute.For(); + customer.Id.ReturnsForAnyArgs("Braintree-Id"); + customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() }); + var customerResult = Substitute.For>(); + customerResult.IsSuccess().Returns(true); + customerResult.Target.ReturnsForAnyArgs(customer); + + var braintreeGateway = sutProvider.GetDependency(); + braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PurchaseOrganizationWithProductsAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo)); + + Assert.Equal("Payment method was declined.", exception.Message); + + await stripeAdapter.Received(1).CustomerDeleteAsync("C-1"); + await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id"); + } + [Theory, BitAutoData] public async void UpgradeFreeOrganizationAsync_Success(SutProvider sutProvider, Organization organization, TaxInfo taxInfo) From 9721826e8b17537de9305ef62b190c2bba2caca7 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 11:57:42 +0100 Subject: [PATCH 24/37] Changes to include sm to signup --- .../Interfaces/IOrganizationSignUpCommand.cs | 0 .../Interfaces/IOrganizationSignUpValidationStrategy.cs | 6 ++++++ .../OrganizationSignUp/OrganizationSignUpCommand.cs | 6 ++++++ .../PasswordManagerSignUpValidationStrategy.cs | 6 ++++++ .../SecretsManagerSignUpValidationStrategy.cs | 6 ++++++ .../Interfaces/IOrganizationSignUpCommand.cs | 0 .../OrganizationSignUp/OrganizationSignUpCommandTests.cs | 6 ++++++ .../PasswordManagerSignUpValidationStrategyTests.cs | 6 ++++++ .../SecretsManagerSignUpValidationStrategyTests.cs | 6 ++++++ 9 files changed, 42 insertions(+) create mode 100644 src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs new file mode 100644 index 000000000000..c0d1f206f0d7 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; + +public interface IOrganizationSignUpValidationStrategy +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs new file mode 100644 index 000000000000..1ef9d1f1e124 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class OrganizationSignUpCommand +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs new file mode 100644 index 000000000000..15199a1d1b52 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class PasswordManagerSignUpValidationStrategy +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs new file mode 100644 index 000000000000..aba82a0c6e6c --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class SecretsManagerSignUpValidationStrategy +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs new file mode 100644 index 000000000000..80366e9bf82a --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +public class OrganizationSignUpCommandTests +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs new file mode 100644 index 000000000000..42ea68a3d91c --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +public class PasswordManagerSignUpValidationStrategyTests +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs new file mode 100644 index 000000000000..8d6e8e1466af --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +public class SecretsManagerSignUpValidationStrategyTests +{ + +} From 0e13348e0ce795aecea7d2c894af7d6afd8e7137 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 11:58:15 +0100 Subject: [PATCH 25/37] changes to add secrets manager on signup --- .../Controllers/OrganizationsController.cs | 14 +- .../OrganizationCreateRequestModel.cs | 9 + src/Core/Constants.cs | 1 + .../Models/Business/OrganizationUpgrade.cs | 3 + ...OrganizationServiceCollectionExtensions.cs | 10 ++ .../Interfaces/IOrganizationSignUpCommand.cs | 10 ++ .../IOrganizationSignUpValidationStrategy.cs | 6 +- .../OrganizationSignUpCommand.cs | 170 +++++++++++++++++- ...PasswordManagerSignUpValidationStrategy.cs | 43 ++++- .../SecretsManagerSignUpValidationStrategy.cs | 44 ++++- src/Core/Services/IOrganizationService.cs | 2 + .../Implementations/OrganizationService.cs | 2 +- .../Tools/Models/Business/ReferenceEvent.cs | 2 + .../Interfaces/IOrganizationSignUpCommand.cs | 0 .../OrganizationSignUpCommandTests.cs | 67 ++++++- ...ordManagerSignUpValidationStrategyTests.cs | 78 +++++++- ...etsManagerSignUpValidationStrategyTests.cs | 88 ++++++++- 17 files changed, 529 insertions(+), 20 deletions(-) delete mode 100644 test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 5a1569c98ed1..f73bdb34b1a8 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -12,12 +12,14 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -50,6 +52,7 @@ public class OrganizationsController : Controller private readonly IFeatureService _featureService; private readonly GlobalSettings _globalSettings; private readonly ILicensingService _licensingService; + private readonly IOrganizationSignUpCommand _organizationSignUpCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -70,7 +73,8 @@ public OrganizationsController( ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery, IFeatureService featureService, GlobalSettings globalSettings, - ILicensingService licensingService) + ILicensingService licensingService, + IOrganizationSignUpCommand organizationSignUpCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -91,6 +95,7 @@ public OrganizationsController( _featureService = featureService; _globalSettings = globalSettings; _licensingService = licensingService; + _organizationSignUpCommand = organizationSignUpCommand; } [HttpGet("{id}")] @@ -241,7 +246,12 @@ public async Task Post([FromBody] OrganizationCreateR } var organizationSignup = model.ToOrganizationSignup(user); - var result = await _organizationService.SignUpAsync(organizationSignup); + + var result = !_featureService.IsEnabled(FeatureFlagKeys.SecretManagerGaBilling, _currentContext) && + !model.UseSecretsManager + ? await _organizationService.SignUpAsync(organizationSignup) + : await _organizationSignUpCommand.SignUpAsync(organizationSignup); + return new OrganizationResponseModel(result.Item1); } diff --git a/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 3e4602179503..a08de38bd65b 100644 --- a/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -40,6 +40,12 @@ public class OrganizationCreateRequestModel : IValidatableObject [StringLength(2)] public string BillingAddressCountry { get; set; } public int? MaxAutoscaleSeats { get; set; } + [Range(0, int.MaxValue)] + public int? AdditionalSmSeats { get; set; } + [Range(0, int.MaxValue)] + public int? AdditionalServiceAccount { get; set; } + [Required] + public bool UseSecretsManager { get; set; } public virtual OrganizationSignup ToOrganizationSignup(User user) { @@ -58,6 +64,9 @@ public virtual OrganizationSignup ToOrganizationSignup(User user) BillingEmail = BillingEmail, BusinessName = BusinessName, CollectionName = CollectionName, + AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(0), + AdditionalServiceAccount = AdditionalServiceAccount.GetValueOrDefault(0), + UseSecretsManager = UseSecretsManager, TaxInfo = new TaxInfo { TaxIdNumber = TaxIdNumber, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 8ff378d00cb7..ff2bf8ac4346 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -36,6 +36,7 @@ public static class FeatureFlagKeys public const string DisplayEuEnvironment = "display-eu-environment"; public const string DisplayLowKdfIterationWarning = "display-kdf-iteration-warning"; public const string TrustedDeviceEncryption = "trusted-device-encryption"; + public const string SecretManagerGaBilling = "sm-ga-billing"; public static List GetAllKeys() { diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index b77a9d012c2e..22bc287e44b3 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -12,4 +12,7 @@ public class OrganizationUpgrade public TaxInfo TaxInfo { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } + public int? AdditionalSmSeats { get; set; } + public int? AdditionalServiceAccount { get; set; } + public bool UseSecretsManager { get; set; } } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 983fa3b3527e..eaa53a479a30 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -11,6 +11,8 @@ using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; @@ -38,6 +40,7 @@ public static void AddOrganizationServices(this IServiceCollection services, IGl services.AddOrganizationGroupCommands(); services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); + services.AddOrganizationSignUpCommands(); } private static void AddOrganizationConnectionCommands(this IServiceCollection services) @@ -117,4 +120,11 @@ private static void AddTokenizers(this IServiceCollection services) serviceProvider.GetRequiredService>>()) ); } + + private static void AddOrganizationSignUpCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } } diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs index e69de29bb2d1..42f145f0efe7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs @@ -0,0 +1,10 @@ +using Bit.Core.Entities; +using Bit.Core.Models.Business; + +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; + +public interface IOrganizationSignUpCommand +{ + Task> SignUpAsync(OrganizationSignup signup, + bool provider = false); +} diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs index c0d1f206f0d7..aa682a5e67b6 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpValidationStrategy.cs @@ -1,6 +1,8 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; +using Bit.Core.Models.Business; + +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; public interface IOrganizationSignUpValidationStrategy { - + void Validate(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); } diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs index 1ef9d1f1e124..d605de9547d0 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs @@ -1,6 +1,170 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Utilities; +using Bit.Core.Exceptions; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Tools.Services; -public class OrganizationSignUpCommand +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class OrganizationSignUpCommand : IOrganizationSignUpCommand { - + private readonly IPaymentService _paymentService; + private readonly ICurrentContext _currentContext; + private readonly IOrganizationService _organizationService; + private readonly IPolicyService _policyService; + private readonly IReferenceEventService _referenceEventService; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationSignUpValidationStrategy _organizationSignUpValidationStrategy; + + public OrganizationSignUpCommand( + IPaymentService paymentService + , ICurrentContext currentContext + , IOrganizationService organizationService + , IPolicyService policyService + , IReferenceEventService referenceEventService + , IOrganizationUserRepository organizationUserRepository + , IOrganizationSignUpValidationStrategy organizationSignUpValidationStrategy) + { + _paymentService = paymentService; + _currentContext = currentContext; + _organizationService = organizationService; + _policyService = policyService; + _referenceEventService = referenceEventService; + _organizationUserRepository = organizationUserRepository; + _organizationSignUpValidationStrategy = organizationSignUpValidationStrategy; + } + + public async Task> SignUpAsync(OrganizationSignup signup, + bool provider = false) + { + var plans = StaticStore.Plans.Where(p => p.Type == signup.Plan).ToList(); + + // if (!_featureService.IsEnabled(FeatureFlagKeys.SecretManagerGaBilling, _currentContext) && + // !signup.UseSecretsManager) + // { + // plans = StaticStore.PasswordManagerPlans.Where(p => p.Type == signup.Plan).ToList(); + // } + // else + // { + // + // } + foreach (var plan in plans) + { + if (plan is not { LegacyYear: null }) + { + throw new BadRequestException("Invalid plan selected."); + } + + if (plan.Disabled) + { + throw new BadRequestException("Plan not found."); + } + + ValidateOrganizationUpgradeParameters(plan, signup, _organizationSignUpValidationStrategy); + } + + if (!provider) + { + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(signup.Owner.Id, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + var passwordManagerPlan = plans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.PasswordManager); + var secretsManagerPlan = plans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.SecretsManager); + + var organization = new Organization + { + // Pre-generate the org id so that we can save it with the Stripe subscription.. + Id = CoreHelpers.GenerateComb(), + Name = signup.Name, + BillingEmail = signup.BillingEmail, + BusinessName = signup.BusinessName, + PlanType = passwordManagerPlan.Type, + Seats = (short)(passwordManagerPlan.BaseSeats + signup.AdditionalSeats), + MaxCollections = passwordManagerPlan.MaxCollections, + MaxStorageGb = !passwordManagerPlan.BaseStorageGb.HasValue ? + (short?)null : (short)(passwordManagerPlan.BaseStorageGb.Value + signup.AdditionalStorageGb), + UsePolicies = passwordManagerPlan.HasPolicies, + UseSso = passwordManagerPlan.HasSso, + UseGroups = passwordManagerPlan.HasGroups, + UseEvents = passwordManagerPlan.HasEvents, + UseDirectory = passwordManagerPlan.HasDirectory, + UseTotp = passwordManagerPlan.HasTotp, + Use2fa = passwordManagerPlan.Has2fa, + UseApi = passwordManagerPlan.HasApi, + UseResetPassword = passwordManagerPlan.HasResetPassword, + SelfHost = passwordManagerPlan.HasSelfHost, + UsersGetPremium = passwordManagerPlan.UsersGetPremium || signup.PremiumAccessAddon, + UseCustomPermissions = passwordManagerPlan.HasCustomPermissions, + UseScim = passwordManagerPlan.HasScim, + Plan = passwordManagerPlan.Name, + Gateway = null, + ReferenceData = signup.Owner.ReferenceData, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + SmSeats = (short)(secretsManagerPlan.BaseSeats + signup.AdditionalSmSeats), + SmServiceAccounts = + (short)(secretsManagerPlan.BaseServiceAccount + signup.AdditionalServiceAccount) + }; + + if (passwordManagerPlan.Type == PlanType.Free && !provider) + { + var adminCount = + await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id); + if (adminCount > 0) + { + throw new BadRequestException("You can only be an admin of one free organization."); + } + } + else if (passwordManagerPlan.Type != PlanType.Free) + { + await _paymentService.PurchaseOrganizationWithProductsAsync(organization, signup.PaymentMethodType.Value, + signup.PaymentToken, plans, signup.AdditionalStorageGb, signup.AdditionalSeats, + signup.PremiumAccessAddon, signup.TaxInfo, provider); + } + + var ownerId = provider ? default : signup.Owner.Id; + var returnValue = await _organizationService.SignUpAsync(organization, ownerId, signup.OwnerKey, signup.CollectionName, true); + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) + { + PlanName = passwordManagerPlan.Name, + PlanType = passwordManagerPlan.Type, + Seats = returnValue.Item1.Seats, + Storage = returnValue.Item1.MaxStorageGb, + SmSeats = returnValue.Item1.SmSeats, + ServiceAccounts = returnValue.Item1.SmServiceAccounts + }); + return returnValue; + } + + private static void ValidateOrganizationUpgradeParameters(Plan plan, OrganizationUpgrade upgrade + , IOrganizationSignUpValidationStrategy strategy) + { + strategy = plan.BitwardenProduct switch + { + BitwardenProductType.PasswordManager => new PasswordManagerSignUpValidationStrategy(), + _ => new SecretsManagerSignUpValidationStrategy() + }; + + strategy.Validate(plan, upgrade); + } + } diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs index 15199a1d1b52..5fb07fedcd89 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs @@ -1,6 +1,43 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.Services; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; -public class PasswordManagerSignUpValidationStrategy +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class PasswordManagerSignUpValidationStrategy : IOrganizationSignUpValidationStrategy { - + public void Validate(Plan plan, OrganizationUpgrade upgrade) + { + if (!plan.HasAdditionalStorageOption && upgrade.AdditionalStorageGb > 0) + { + throw new BadRequestException("Plan does not allow additional storage."); + } + + if (upgrade.AdditionalStorageGb < 0) + { + throw new BadRequestException("You can't subtract storage!"); + } + + if (plan.BaseSeats + upgrade.AdditionalSeats <= 0) + { + throw new BadRequestException("You do not have any seats!"); + } + + if (upgrade.AdditionalSeats < 0) + { + throw new BadRequestException("You can't subtract seats!"); + } + + switch (plan.HasAdditionalSeatsOption) + { + case false when upgrade.AdditionalSeats > 0: + throw new BadRequestException("Plan does not allow additional users."); + case true when plan.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSeats > plan.MaxAdditionalSeats.Value: + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } } diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs index aba82a0c6e6c..86e3efd482f2 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategy.cs @@ -1,6 +1,44 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; -public class SecretsManagerSignUpValidationStrategy +namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; + +public class SecretsManagerSignUpValidationStrategy : IOrganizationSignUpValidationStrategy { - + public void Validate(Plan plan, OrganizationUpgrade upgrade) + { + if (!plan.HasAdditionalServiceAccountOption && upgrade.AdditionalServiceAccount > 0) + { + throw new BadRequestException("Plan does not allow additional service account."); + } + + if (upgrade.AdditionalServiceAccount < 0) + { + throw new BadRequestException("You can't subtract service account!"); + } + + if (plan.BaseSeats + upgrade.AdditionalSmSeats <= 0) + { + throw new BadRequestException("You do not have any seats!"); + } + + if (upgrade.AdditionalSmSeats < 0) + { + throw new BadRequestException("You can't subtract secrets manager seats!"); + } + + if (!plan.HasAdditionalSeatsOption && upgrade.AdditionalSmSeats > 0) + { + throw new BadRequestException("Plan does not allow additional users."); + } + + if (plan.HasAdditionalSeatsOption && plan.MaxAdditionalSeats.HasValue && + upgrade.AdditionalSmSeats > plan.MaxAdditionalSeats.Value) + { + throw new BadRequestException($"Selected plan allows a maximum of " + + $"{plan.MaxAdditionalSeats.GetValueOrDefault(0)} additional users."); + } + } } diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index b912bd92149a..d84c302d5244 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -22,6 +22,8 @@ Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, Payment Task> SignUpAsync(OrganizationSignup organizationSignup, bool provider = false); Task> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); + Task> SignUpAsync(Organization organization, + Guid ownerId, string ownerKey, string collectionName, bool withPayment); Task DeleteAsync(Organization organization); Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index 94f03c89716c..d0d31d028ae2 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -772,7 +772,7 @@ public async Task> SignUpAsync( return result; } - private async Task> SignUpAsync(Organization organization, + public async Task> SignUpAsync(Organization organization, Guid ownerId, string ownerKey, string collectionName, bool withPayment) { try diff --git a/src/Core/Tools/Models/Business/ReferenceEvent.cs b/src/Core/Tools/Models/Business/ReferenceEvent.cs index 62dfa20533bf..b2af984a63bc 100644 --- a/src/Core/Tools/Models/Business/ReferenceEvent.cs +++ b/src/Core/Tools/Models/Business/ReferenceEvent.cs @@ -70,4 +70,6 @@ public ReferenceEvent(ReferenceEventType type, IReferenceable source, ICurrentCo public string ClientId { get; set; } public Version? ClientVersion { get; set; } + public int? SmSeats { get; set; } + public int? ServiceAccounts { get; set; } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/Interfaces/IOrganizationSignUpCommand.cs deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs index 80366e9bf82a..f2cfe180cc8f 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs @@ -1,6 +1,69 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; +using AutoFixture; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +[SutProviderCustomize] public class OrganizationSignUpCommandTests { - + [Theory, BitAutoData] + public async Task SignUpAsync_WhenValidSignup_ReturnsOrganizationAndOrganizationUser( + SutProvider sutProvider, OrganizationSignup signup, + bool provider) + { + var fixture = new Fixture(); + var paymentService = sutProvider.GetDependency(); + var organizationService = sutProvider.GetDependency(); + var policyService = sutProvider.GetDependency(); + var referenceEventService = sutProvider.GetDependency(); + var organizationUserRepository = sutProvider.GetDependency(); + var organizationSignUpValidationStrategy = sutProvider.GetDependency(); + + var plans = fixture.Create>(); + + var passwordManagerPlan = fixture.Create(); + var organization = fixture.Create(); + var organizationUser = fixture.Create(); + + policyService.AnyPoliciesApplicableToUserAsync(signup.Owner.Id, PolicyType.SingleOrg).Returns(false); + organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(signup.Owner.Id).Returns(0); + organizationService.SignUpAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), true) + .Returns(new Tuple(organization, organizationUser)); + + fixture.Inject(plans); + fixture.Inject(passwordManagerPlan); + fixture.Inject(organization); + fixture.Inject(organizationUser); + fixture.Inject(policyService); + fixture.Inject(organizationService); + fixture.Inject(paymentService); + fixture.Inject(referenceEventService); + fixture.Inject(organizationUserRepository); + fixture.Inject(organizationSignUpValidationStrategy); + + signup.AdditionalStorageGb = 0; + signup.AdditionalSeats = 0; + signup.UseSecretsManager = true; + signup.AdditionalServiceAccount = 0; + signup.AdditionalSmSeats = 0; + var result = await sutProvider.Sut.SignUpAsync(signup, provider); + + Assert.NotNull(result); + Assert.Equal(organization, result.Item1); + Assert.Equal(organizationUser, result.Item2); + } + } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs index 42ea68a3d91c..c391c6036867 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs @@ -1,6 +1,80 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; +using AutoFixture.Xunit2; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Core.Exceptions; +using Xunit; +using Bit.Core.Models.StaticStore; +using Bit.Test.Common.AutoFixture; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +[SutProviderCustomize] public class PasswordManagerSignUpValidationStrategyTests { - + [Theory] + [BitAutoData] + public void Validate_WhenPlanDoesNotAllowAdditionalStorageAndUpgradeRequestsAdditionalStorage_ThrowsBadRequestException( + SutProvider sutProvider, OrganizationUpgrade upgrade) + { + var plan = new Plan { HasAdditionalStorageOption = false, BitwardenProduct = BitwardenProductType.PasswordManager }; + upgrade.AdditionalStorageGb = 10; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WhenUpgradeRequestsNegativeAdditionalStorage_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] OrganizationUpgrade upgrade) + { + var plan = new Plan { HasAdditionalStorageOption = true, BitwardenProduct = BitwardenProductType.PasswordManager }; + upgrade.AdditionalStorageGb = -5; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WhenNoSeatsAfterUpgrade_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.BaseSeats = 5; + plan.BitwardenProduct = BitwardenProductType.PasswordManager; + upgrade.AdditionalSeats = -5; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WhenUpgradeRequestsNegativeAdditionalSeats_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.BitwardenProduct = BitwardenProductType.PasswordManager; + upgrade.AdditionalSeats = -3; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WhenPlanDoesNotAllowAdditionalSeatsAndUpgradeRequestsAdditionalSeats_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.HasAdditionalSeatsOption = false; + plan.BitwardenProduct = BitwardenProductType.PasswordManager; + upgrade.AdditionalSeats = 2; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs index 8d6e8e1466af..836f9e33a82c 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/SecretsManagerSignUpValidationStrategyTests.cs @@ -1,6 +1,90 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; +using AutoFixture.Xunit2; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; + +[SutProviderCustomize] public class SecretsManagerSignUpValidationStrategyTests { - + [Theory] + [BitAutoData] + public void Validate_WithInvalidAdditionalServiceAccountOption_ThrowsBadRequestException( + SutProvider sutProvider, [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.HasAdditionalServiceAccountOption = false; + upgrade.AdditionalServiceAccount = 1; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WithNegativeAdditionalServiceAccount_ThrowsBadRequestException( + SutProvider sutProvider, [Frozen] OrganizationUpgrade upgrade) + { + var plan = new Plan { HasAdditionalServiceAccountOption = true, BitwardenProduct = BitwardenProductType.PasswordManager }; + upgrade.AdditionalServiceAccount = -5; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WithZeroSeats_ThrowsBadRequestException( + SutProvider sutProvider + , [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.BaseSeats = 0; + upgrade.AdditionalSmSeats = -10; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WithInvalidAdditionalSmSeatsOption_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.HasAdditionalSeatsOption = false; + upgrade.AdditionalSmSeats = 5; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WithNegativeAdditionalSmSeats_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] OrganizationUpgrade upgrade) + { + var plan = new Plan { HasAdditionalSeatsOption = true, BitwardenProduct = BitwardenProductType.PasswordManager }; + upgrade.AdditionalSmSeats = -5; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } + + [Theory] + [BitAutoData] + public void Validate_WithExceededMaxAdditionalSmSeats_ThrowsBadRequestException( + SutProvider sutProvider, + [Frozen] Plan plan, + [Frozen] OrganizationUpgrade upgrade) + { + plan.HasAdditionalSeatsOption = true; + plan.MaxAdditionalSeats = 5; + upgrade.AdditionalSmSeats = 10; + + Assert.Throws(() => sutProvider.Sut.Validate(plan, upgrade)); + } } From 906b0c50b52dc7e9a967e9d1caa3a20c80465ef3 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:25:14 +0100 Subject: [PATCH 26/37] Add the Upgrade Plan changes --- .../Interface/IOrganizationUpgradePlanCommand.cs | 6 ++++++ .../Interface/IOrganizationUpgradeQuery.cs | 6 ++++++ .../Interface/IValidateUpgradeCommand.cs | 6 ++++++ .../OrganizationUpgradePlanCommand.cs | 6 ++++++ .../OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs | 6 ++++++ .../OrganizationPlanUpgrade/ValidateUpgradeCommand.cs | 6 ++++++ ...ationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs | 6 ++++++ .../OrganizationUpgradePlanCommandTests.cs | 6 ++++++ .../OrganizationUpgradeQueryTests.cs | 6 ++++++ .../OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs | 6 ++++++ 10 files changed, 60 insertions(+) create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs create mode 100644 src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs create mode 100644 src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs create mode 100644 test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs new file mode 100644 index 000000000000..5d1d258aa752 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; + +public interface IOrganizationUpgradePlanCommand +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs new file mode 100644 index 000000000000..bbc4cbb69d09 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; + +public interface IOrganizationUpgradeQuery +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs new file mode 100644 index 000000000000..ca16bbd4b51b --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; + +public interface IValidateUpgradeCommand +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs new file mode 100644 index 000000000000..824908e4fb06 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradePlanCommand +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs new file mode 100644 index 000000000000..ad1db51451d1 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradeQuery +{ + +} diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs new file mode 100644 index 000000000000..7e281a4856da --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class ValidateUpgradeCommand +{ + +} diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs new file mode 100644 index 000000000000..6a79629a546e --- /dev/null +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs @@ -0,0 +1,6 @@ +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs new file mode 100644 index 000000000000..33e593186f75 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradePlanCommandTests +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs new file mode 100644 index 000000000000..27b11a020240 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradeQueryTests +{ + +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs new file mode 100644 index 000000000000..b776c43ee60a --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs @@ -0,0 +1,6 @@ +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +public class ValidateUpgradeCommandTests +{ + +} From 15076a318b6aa42ab3aea619575f0f7e4011b65b Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:25:18 +0100 Subject: [PATCH 27/37] Add the Upgrade Plan changes --- .../Controllers/OrganizationsController.cs | 12 +- .../OrganizationUpgradeRequestModel.cs | 9 + .../IOrganizationUpgradePlanCommand.cs | 6 +- .../Interface/IOrganizationUpgradeQuery.cs | 12 +- .../Interface/IValidateUpgradeCommand.cs | 26 +- .../OrganizationUpgradePlanCommand.cs | 165 ++++++++++++- .../OrganizationUpgradeQuery.cs | 31 ++- .../ValidateUpgradeCommand.cs | 226 +++++++++++++++++- ...OrganizationServiceCollectionExtensions.cs | 10 + .../IOrganizationUserRepository.cs | 2 + .../OrganizationUserRepository.cs | 26 ++ .../OrganizationUserRepository.cs | 17 ++ ...ccupiedSmSeatCountByOrganizationIdQuery.cs | 22 +- .../OrganizationUpgradePlanCommandTests.cs | 154 +++++++++++- .../OrganizationUpgradeQueryTests.cs | 58 ++++- .../ValidateUpgradeCommandTests.cs | 161 ++++++++++++- 16 files changed, 911 insertions(+), 26 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index f73bdb34b1a8..6a7f785ccd57 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -19,6 +19,7 @@ using Bit.Core.Models.Data.Organizations.Policies; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -53,6 +54,7 @@ public class OrganizationsController : Controller private readonly GlobalSettings _globalSettings; private readonly ILicensingService _licensingService; private readonly IOrganizationSignUpCommand _organizationSignUpCommand; + private readonly IOrganizationUpgradePlanCommand _organizationUpgradePlanCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -74,7 +76,8 @@ public OrganizationsController( IFeatureService featureService, GlobalSettings globalSettings, ILicensingService licensingService, - IOrganizationSignUpCommand organizationSignUpCommand) + IOrganizationSignUpCommand organizationSignUpCommand, + IOrganizationUpgradePlanCommand organizationUpgradePlanCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -96,6 +99,7 @@ public OrganizationsController( _globalSettings = globalSettings; _licensingService = licensingService; _organizationSignUpCommand = organizationSignUpCommand; + _organizationUpgradePlanCommand = organizationUpgradePlanCommand; } [HttpGet("{id}")] @@ -316,7 +320,11 @@ public async Task PostUpgrade(string id, [FromBody] Organi throw new NotFoundException(); } - var result = await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); + var result = !_featureService.IsEnabled(FeatureFlagKeys.SecretManagerGaBilling, _currentContext) && + !model.UseSecretsManager + ? await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()) + : await _organizationUpgradePlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); + return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 }; } diff --git a/src/Api/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs index fb2666cc1ed0..1350372d43e1 100644 --- a/src/Api/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -13,7 +13,13 @@ public class OrganizationUpgradeRequestModel public int AdditionalSeats { get; set; } [Range(0, 99)] public short? AdditionalStorageGb { get; set; } + [Range(0, int.MaxValue)] + public int? AdditionalSmSeats { get; set; } + [Range(0, int.MaxValue)] + public int? AdditionalServiceAccount { get; set; } public bool PremiumAccessAddon { get; set; } + [Required] + public bool UseSecretsManager { get; set; } public string BillingAddressCountry { get; set; } public string BillingAddressPostalCode { get; set; } public OrganizationKeysRequestModel Keys { get; set; } @@ -24,6 +30,9 @@ public OrganizationUpgrade ToOrganizationUpgrade() { AdditionalSeats = AdditionalSeats, AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(), + AdditionalServiceAccount = AdditionalServiceAccount.GetValueOrDefault(0), + AdditionalSmSeats = AdditionalSmSeats.GetValueOrDefault(0), + UseSecretsManager = UseSecretsManager, BusinessName = BusinessName, Plan = PlanType, PremiumAccessAddon = PremiumAccessAddon, diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs index 5d1d258aa752..28f88a771085 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradePlanCommand.cs @@ -1,6 +1,8 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.Models.Business; + +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; public interface IOrganizationUpgradePlanCommand { - + Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade); } diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs index bbc4cbb69d09..6c4483fdc666 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IOrganizationUpgradeQuery.cs @@ -1,6 +1,14 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; + +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; public interface IOrganizationUpgradeQuery { - + Plan ExistingPlan(PlanType planType); + + List NewPlans(PlanType planType); + + Task GetOrgById(Guid id); } diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs index ca16bbd4b51b..fab0751b3a2d 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs @@ -1,6 +1,28 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Models.StaticStore; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; public interface IValidateUpgradeCommand { - + void ValidatePlan(Plan newPlan, Plan existingPlan); + Task ValidateSeatsAsync(Organization organization, Plan passwordManagerPlan, OrganizationUpgrade upgrade); + Task ValidateSmSeatsAsync(Organization organization, Plan newPlan, OrganizationUpgrade upgrade); + Task ValidateServiceAccountAsync(Organization organization, Plan newPlan, OrganizationUpgrade upgrade); + Task ValidateCollectionsAsync(Organization organization, Plan newPlan); + Task ValidateGroupsAsync(Organization organization, Plan newPlan); + Task ValidatePoliciesAsync(Organization organization, Plan newPlan); + Task ValidateSsoAsync(Organization organization, Plan newPlan); + Task ValidateKeyConnectorAsync(Organization organization, Plan newPlan); + Task ValidateResetPasswordAsync(Organization organization, Plan newPlan); + Task ValidateScimAsync(Organization organization, Plan newPlan); + Task ValidateCustomPermissionsAsync(Organization organization, Plan newPlan); + } diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs index 824908e4fb06..065b1f759fc0 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs @@ -1,6 +1,165 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSignUp; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Models.StaticStore; -public class OrganizationUpgradePlanCommand +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradePlanCommand : IOrganizationUpgradePlanCommand { - + private readonly IOrganizationUpgradeQuery _organizationUpgradeQuery; + private readonly IValidateUpgradeCommand _validateUpgradeCommand; + private readonly IOrganizationService _organizationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly IPaymentService _paymentService; + private IOrganizationSignUpValidationStrategy _organizationSignUpValidationStrategy; + + public OrganizationUpgradePlanCommand( + IOrganizationUpgradeQuery organizationUpgradeQuery + , IValidateUpgradeCommand validateUpgradeCommand + , IOrganizationService organizationService + , IReferenceEventService referenceEventService + , ICurrentContext currentContext + , IPaymentService paymentService + , IOrganizationSignUpValidationStrategy organizationSignUpValidationStrategy) + { + _organizationUpgradeQuery = organizationUpgradeQuery; + _validateUpgradeCommand = validateUpgradeCommand; + _organizationService = organizationService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + _paymentService = paymentService; + _organizationSignUpValidationStrategy = organizationSignUpValidationStrategy; + } + + public async Task> UpgradePlanAsync(Guid organizationId, OrganizationUpgrade upgrade) + { + var organization = await _organizationUpgradeQuery.GetOrgById(organizationId); + if (organization == null) + { + throw new NotFoundException(); + } + + if (string.IsNullOrWhiteSpace(organization.GatewayCustomerId)) + { + throw new BadRequestException("Your account has no payment method available."); + } + + var existingPlan = _organizationUpgradeQuery.ExistingPlan(organization.PlanType); + + var newPlans = _organizationUpgradeQuery.NewPlans(upgrade.Plan); + var passwordManagerPlan = newPlans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.PasswordManager); + var secretsManagerPlan = newPlans.FirstOrDefault(x => x.BitwardenProduct == BitwardenProductType.SecretsManager); + + _validateUpgradeCommand.ValidatePlan(passwordManagerPlan, existingPlan); + + foreach (var plan in newPlans) + { + _organizationSignUpValidationStrategy = plan.BitwardenProduct switch + { + BitwardenProductType.PasswordManager => new PasswordManagerSignUpValidationStrategy(), + _ => new SecretsManagerSignUpValidationStrategy() + }; + + _organizationSignUpValidationStrategy.Validate(plan, upgrade); + } + + await _validateUpgradeCommand.ValidateSeatsAsync(organization, passwordManagerPlan, upgrade); + await _validateUpgradeCommand.ValidateCollectionsAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateGroupsAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidatePoliciesAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateSsoAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateKeyConnectorAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateResetPasswordAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateScimAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateCustomPermissionsAsync(organization, passwordManagerPlan); + await _validateUpgradeCommand.ValidateSmSeatsAsync(organization, secretsManagerPlan, upgrade); + await _validateUpgradeCommand.ValidateServiceAccountAsync(organization, secretsManagerPlan, upgrade); + + // TODO: Check storage? + + string paymentIntentClientSecret; + var success = true; + if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId)) + { + paymentIntentClientSecret = await _paymentService.UpgradeFreeOrganizationAsync(organization, passwordManagerPlan, + upgrade.AdditionalStorageGb, upgrade.AdditionalSeats, upgrade.PremiumAccessAddon, upgrade.TaxInfo); + success = string.IsNullOrWhiteSpace(paymentIntentClientSecret); + } + else + { + // TODO: Update existing sub + throw new BadRequestException("You can only upgrade from the free plan. Contact support."); + } + + UpdateOrganizationProperties(organization, passwordManagerPlan, upgrade + , success, secretsManagerPlan); + + await _organizationService.ReplaceAndUpdateCacheAsync(organization); + if (success) + { + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.UpgradePlan, organization, _currentContext) + { + PlanName = passwordManagerPlan.Name, + PlanType = passwordManagerPlan.Type, + OldPlanName = existingPlan.Name, + OldPlanType = existingPlan.Type, + Seats = organization.Seats, + Storage = organization.MaxStorageGb, + SmSeats = organization.SmSeats, + ServiceAccounts = organization.SmServiceAccounts + }); + } + + return new Tuple(success, paymentIntentClientSecret); + } + + private static void UpdateOrganizationProperties(Organization organization, Plan passwordManagerPlan, OrganizationUpgrade upgrade + , bool success, Plan secretManagerPlan) + { + organization.BusinessName = upgrade.BusinessName; + organization.PlanType = passwordManagerPlan.Type; + organization.Seats = (short)(passwordManagerPlan.BaseSeats + upgrade.AdditionalSeats); + organization.MaxCollections = passwordManagerPlan.MaxCollections; + organization.UseGroups = passwordManagerPlan.HasGroups; + organization.UseDirectory = passwordManagerPlan.HasDirectory; + organization.UseEvents = passwordManagerPlan.HasEvents; + organization.UseTotp = passwordManagerPlan.HasTotp; + organization.Use2fa = passwordManagerPlan.Has2fa; + organization.UseApi = passwordManagerPlan.HasApi; + organization.SelfHost = passwordManagerPlan.HasSelfHost; + organization.UsePolicies = passwordManagerPlan.HasPolicies; + organization.MaxStorageGb = !passwordManagerPlan.BaseStorageGb.HasValue ? + null : (short)(passwordManagerPlan.BaseStorageGb.Value + upgrade.AdditionalStorageGb); + organization.UseGroups = passwordManagerPlan.HasGroups; + organization.UseDirectory = passwordManagerPlan.HasDirectory; + organization.UseEvents = passwordManagerPlan.HasEvents; + organization.UseTotp = passwordManagerPlan.HasTotp; + organization.Use2fa = passwordManagerPlan.Has2fa; + organization.UseApi = passwordManagerPlan.HasApi; + organization.UseSso = passwordManagerPlan.HasSso; + organization.UseKeyConnector = passwordManagerPlan.HasKeyConnector; + organization.UseScim = passwordManagerPlan.HasScim; + organization.UseResetPassword = passwordManagerPlan.HasResetPassword; + organization.SelfHost = passwordManagerPlan.HasSelfHost; + organization.UsersGetPremium = passwordManagerPlan.UsersGetPremium || upgrade.PremiumAccessAddon; + organization.UseCustomPermissions = passwordManagerPlan.HasCustomPermissions; + organization.Plan = passwordManagerPlan.Name; + organization.Enabled = success; + organization.PublicKey = upgrade.PublicKey; + organization.PrivateKey = upgrade.PrivateKey; + organization.SmSeats = (short)(secretManagerPlan.BaseSeats + upgrade.AdditionalSmSeats); + organization.SmServiceAccounts = (int)(secretManagerPlan.BaseServiceAccount + upgrade.AdditionalServiceAccount); + } } diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs index ad1db51451d1..9886463a54c8 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQuery.cs @@ -1,6 +1,31 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.Repositories; +using Bit.Core.Utilities; -public class OrganizationUpgradeQuery +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class OrganizationUpgradeQuery : IOrganizationUpgradeQuery { - + private readonly IOrganizationRepository _organizationRepository; + public OrganizationUpgradeQuery(IOrganizationRepository organizationRepository) + { + _organizationRepository = organizationRepository; + } + public Plan ExistingPlan(PlanType planType) + { + return StaticStore.Plans.FirstOrDefault(p => p.Type == planType); + } + + public List NewPlans(PlanType planType) + { + return StaticStore.Plans.Where(p => p.Type == planType && !p.Disabled).ToList(); + } + + public async Task GetOrgById(Guid id) + { + return await _organizationRepository.GetByIdAsync(id); + } } diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs index 7e281a4856da..9d3044d6e85d 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs @@ -1,6 +1,226 @@ -namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Models.StaticStore; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.Repositories; -public class ValidateUpgradeCommand +namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; + +public class ValidateUpgradeCommand : IValidateUpgradeCommand { - + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ICollectionRepository _collectionRepository; + private readonly IGroupRepository _groupRepository; + private readonly IPolicyRepository _policyRepository; + private readonly ISsoConfigRepository _ssoConfigRepository; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + + public ValidateUpgradeCommand( + IOrganizationUserRepository organizationUserRepository + , ICollectionRepository collectionRepository + , IGroupRepository groupRepository + , IPolicyRepository policyRepository + , ISsoConfigRepository ssoConfigRepository + , IOrganizationConnectionRepository organizationConnectionRepository) + { + _organizationUserRepository = organizationUserRepository; + _collectionRepository = collectionRepository; + _groupRepository = groupRepository; + _policyRepository = policyRepository; + _ssoConfigRepository = ssoConfigRepository; + _organizationConnectionRepository = organizationConnectionRepository; + } + + public void ValidatePlan(Plan newPlan, Plan existingPlan) + { + if (existingPlan == null) + { + throw new BadRequestException("Existing plan not found."); + } + + if (newPlan == null) + { + throw new BadRequestException("Plan not found."); + } + + if (newPlan.Disabled) + { + throw new BadRequestException("Plan not found."); + } + + if (existingPlan.Type == newPlan.Type) + { + throw new BadRequestException("Organization is already on this plan."); + } + + if (existingPlan.UpgradeSortOrder >= newPlan.UpgradeSortOrder) + { + throw new BadRequestException("You cannot upgrade to this plan."); + } + + if (existingPlan.Type != PlanType.Free) + { + throw new BadRequestException("You can only upgrade from the free plan. Contact support."); + } + } + + public async Task ValidateSeatsAsync(Organization organization, Plan passwordManagerPlan, OrganizationUpgrade upgrade) + { + var newPlanSeats = (short)(passwordManagerPlan.BaseSeats + + (passwordManagerPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSeats : 0)); + if (!organization.Seats.HasValue || organization.Seats.Value > newPlanSeats) + { + var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + if (occupiedSeats > newPlanSeats) + { + throw new BadRequestException($"Your organization currently has {occupiedSeats} password manager seats filled. " + + $"Your new plan only has ({newPlanSeats}) seats. Remove some users."); + } + } + } + + public async Task ValidateSmSeatsAsync(Organization organization, Plan newPlan, OrganizationUpgrade upgrade) + { + var newPlanSeats = (short)(newPlan.BaseSeats + (newPlan.HasAdditionalSeatsOption ? upgrade.AdditionalSmSeats : 0)); + if (!organization.SmSeats.HasValue || organization.SmSeats.Value > newPlanSeats) + { + var occupiedSmSeats = await _organizationUserRepository.GetOccupiedSmSeatCountByOrganizationIdAsync(organization.Id); + if (occupiedSmSeats > newPlanSeats) + { + throw new BadRequestException($"Your organization currently has {occupiedSmSeats} secrets manager seats filled. " + + $"Your new plan only has ({newPlanSeats}) seats. Remove some users."); + } + } + } + + public async Task ValidateServiceAccountAsync(Organization organization, Plan newPlan, OrganizationUpgrade upgrade) + { + if (newPlan.BaseServiceAccount != null) + { + var newPlanSeats = (short)(newPlan.BaseServiceAccount + (newPlan.HasAdditionalServiceAccountOption ? upgrade.AdditionalServiceAccount : 0)); + if (!organization.SmServiceAccounts.HasValue || organization.SmServiceAccounts.Value > newPlanSeats) + { + var occupiedServiceAccount = await _organizationUserRepository.GetOccupiedServiceAccountCountByOrganizationIdAsync(organization.Id); + if (occupiedServiceAccount > newPlanSeats) + { + throw new BadRequestException($"Your organization currently has {occupiedServiceAccount} service account seats filled. " + + $"Your new plan only has ({newPlanSeats}) service accounts. Remove some service accounts."); + } + } + } + } + + public async Task ValidateCollectionsAsync(Organization organization, Plan newPlan) + { + if (newPlan.MaxCollections.HasValue && (!organization.MaxCollections.HasValue || + organization.MaxCollections.Value > newPlan.MaxCollections.Value)) + { + var collectionCount = await _collectionRepository.GetCountByOrganizationIdAsync(organization.Id); + if (collectionCount > newPlan.MaxCollections.Value) + { + throw new BadRequestException($"Your organization currently has {collectionCount} collections. " + + $"Your new plan allows for a maximum of ({newPlan.MaxCollections.Value}) collections. " + + "Remove some collections."); + } + } + } + + public async Task ValidateGroupsAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasGroups && organization.UseGroups) + { + var groups = await _groupRepository.GetManyByOrganizationIdAsync(organization.Id); + if (groups.Any()) + { + throw new BadRequestException($"Your new plan does not allow the groups feature. " + + $"Remove your groups."); + } + } + } + + public async Task ValidatePoliciesAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasPolicies && organization.UsePolicies) + { + var policies = await _policyRepository.GetManyByOrganizationIdAsync(organization.Id); + if (policies.Any(p => p.Enabled)) + { + throw new BadRequestException($"Your new plan does not allow the policies feature. " + + $"Disable your policies."); + } + } + } + + public async Task ValidateSsoAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasSso && organization.UseSso) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); + if (ssoConfig != null && ssoConfig.Enabled) + { + throw new BadRequestException($"Your new plan does not allow the SSO feature. " + + $"Disable your SSO configuration."); + } + } + } + + public async Task ValidateKeyConnectorAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasKeyConnector && organization.UseKeyConnector) + { + var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id); + if (ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.KeyConnector) + { + throw new BadRequestException($"Your new plan does not allow the Key Connector feature. " + + $"Disable your Key Connector configuration."); + } + } + } + + public async Task ValidateResetPasswordAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasResetPassword && organization.UseResetPassword) + { + var resetPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(organization.Id, PolicyType.ResetPassword); + if (resetPasswordPolicy != null && resetPasswordPolicy.Enabled) + { + throw new BadRequestException("Your new plan does not allow the Password Reset feature. " + + "Disable your Password Reset policy."); + } + } + } + + public async Task ValidateScimAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasScim && organization.UseScim) + { + var scimConnections = await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id, + OrganizationConnectionType.Scim); + if (scimConnections != null && scimConnections.Any(c => c.GetConfig()?.Enabled == true)) + { + throw new BadRequestException("Your new plan does not allow the SCIM feature. " + + "Disable your SCIM configuration."); + } + } + } + + public async Task ValidateCustomPermissionsAsync(Organization organization, Plan newPlan) + { + if (!newPlan.HasCustomPermissions && organization.UseCustomPermissions) + { + var organizationCustomUsers = + await _organizationUserRepository.GetManyByOrganizationAsync(organization.Id, + OrganizationUserType.Custom); + if (organizationCustomUsers.Any()) + { + throw new BadRequestException("Your new plan does not allow the Custom Permissions feature. " + + "Disable your Custom Permissions configuration."); + } + } + } } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index eaa53a479a30..7b1ef75bc945 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -11,6 +11,8 @@ using Bit.Core.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; using Bit.Core.OrganizationFeatures.OrganizationSignUp; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; @@ -41,6 +43,7 @@ public static void AddOrganizationServices(this IServiceCollection services, IGl services.AddOrganizationLicenseCommandsQueries(); services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); + services.AddOrganizationUpgradeCommands(); } private static void AddOrganizationConnectionCommands(this IServiceCollection services) @@ -127,4 +130,11 @@ private static void AddOrganizationSignUpCommands(this IServiceCollection servic services.AddScoped(); services.AddScoped(); } + + private static void AddOrganizationUpgradeCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } } diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 16d333f9e9fe..036c6cdebc7e 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -40,4 +40,6 @@ Task GetDetailsByUserAsync(Guid userId, Gui Task RevokeAsync(Guid id); Task RestoreAsync(Guid id, OrganizationUserStatusType status); Task> GetByUserIdWithPolicyDetailsAsync(Guid userId, PolicyType policyType); + Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId); + Task GetOccupiedServiceAccountCountByOrganizationIdAsync(Guid organizationId); } diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs index ca246949089d..7cd6284a010d 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs @@ -98,6 +98,32 @@ public async Task GetOccupiedSeatCountByOrganizationIdAsync(Guid organizati return result; } } + + public async Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.ExecuteScalarAsync( + "[dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } + + public async Task GetOccupiedServiceAccountCountByOrganizationIdAsync(Guid organizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var result = await connection.ExecuteScalarAsync( + "[dbo].[ServiceAccount_ReadCountByOrganizationId]", + new { OrganizationId = organizationId }, + commandType: CommandType.StoredProcedure); + + return result; + } + } public async Task> SelectKnownEmailsAsync(Guid organizationId, IEnumerable emails, bool onlyRegisteredUsers) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs index 282304720fa8..4d6c14ed8943 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs @@ -621,4 +621,21 @@ on p.OrganizationId equals ou.OrganizationId return await query.ToListAsync(); } } + + public async Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId) + { + var query = new OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(organizationId); + return await GetCountFromQuery(query); + } + + public async Task GetOccupiedServiceAccountCountByOrganizationIdAsync(Guid organizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + return await dbContext.ServiceAccount + .Where(ou => ou.OrganizationId == organizationId) + .CountAsync(); + } + } } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs index 6a79629a546e..30a65fe12211 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs @@ -1,6 +1,22 @@ -namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Models; -public class OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery +namespace Bit.Infrastructure.EntityFramework.Repositories.Queries; + +public class OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery : IQuery { - + private readonly Guid _organizationId; + + public OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(Guid organizationId) + { + _organizationId = organizationId; + } + + public IQueryable Run(DatabaseContext dbContext) + { + var query = from ou in dbContext.OrganizationUsers + where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited && ou.AccessSecretsManager == true + select ou; + return query; + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs index 33e593186f75..ede730427b1f 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs @@ -1,6 +1,156 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; +using Bit.Core.Services; +using Bit.Core.Models.StaticStore; +using Bit.Core.Tools.Services; +using Bit.Core.Exceptions; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +[SutProviderCustomize] public class OrganizationUpgradePlanCommandTests { - + + [Theory] + [BitAutoData] + public async Task UpgradePlanAsync_ExistingSubscription_ThrowsBadRequestException(SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + var upgrade = new OrganizationUpgrade + { + Plan = PlanType.EnterpriseAnnually, + }; + var existingPlan = new Plan() + { + Type = PlanType.TeamsAnnually, + BitwardenProduct = BitwardenProductType.PasswordManager, + }; + + var newPlans = new List + { + new() + { + Type = PlanType.EnterpriseAnnually, + BaseSeats = 2, + BitwardenProduct = BitwardenProductType.PasswordManager, + + }, + new() + { + Type = PlanType.EnterpriseAnnually, + BaseSeats = 2, + BaseServiceAccount = 2, + BitwardenProduct = BitwardenProductType.SecretsManager, + + } + }; + + var organization = new Organization + { + Id = organizationId, + GatewayCustomerId = "gateway-customer-id", + GatewaySubscriptionId = "existing-subscription-id", + PlanType = PlanType.Free, + }; + + var organizationUpgradeQuery = sutProvider.GetDependency(); + organizationUpgradeQuery.GetOrgById(organizationId).Returns(organization); + organizationUpgradeQuery.ExistingPlan(PlanType.TeamsAnnually).Returns(existingPlan); + organizationUpgradeQuery.NewPlans(PlanType.EnterpriseAnnually).Returns(newPlans); + + var validateUpgradeCommand = sutProvider.GetDependency(); + var organizationService = sutProvider.GetDependency(); + var referenceEventService = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + var paymentService = sutProvider.GetDependency(); + var organizationSignUpValidationStrategy = sutProvider.GetDependency(); + + var command = new OrganizationUpgradePlanCommand( + organizationUpgradeQuery, + validateUpgradeCommand, + organizationService, + referenceEventService, + currentContext, + paymentService, + organizationSignUpValidationStrategy + ); + + await Assert.ThrowsAsync(() => command.UpgradePlanAsync(organizationId, upgrade)); + } + + [Theory] + [BitAutoData] + public async Task UpgradePlanAsync_NoPaymentMethodAvailable_ThrowsBadRequestException(SutProvider sutProvider) + { + var organizationId = Guid.NewGuid(); + var upgrade = new OrganizationUpgrade + { + Plan = PlanType.EnterpriseAnnually, + }; + + var organization = new Organization + { + Id = organizationId, + GatewayCustomerId = null, + }; + + var existingPlan = new Plan() + { + Type = PlanType.TeamsAnnually, + BitwardenProduct = BitwardenProductType.PasswordManager, + }; + + var newPlans = new List + { + new() + { + Type = PlanType.EnterpriseAnnually, + BaseSeats = 2, + BitwardenProduct = BitwardenProductType.PasswordManager, + + }, + new() + { + Type = PlanType.EnterpriseAnnually, + BaseSeats = 2, + BaseServiceAccount = 2, + BitwardenProduct = BitwardenProductType.SecretsManager, + + } + }; + + var organizationUpgradeQuery = sutProvider.GetDependency(); + organizationUpgradeQuery.GetOrgById(organizationId).Returns(organization); + organizationUpgradeQuery.ExistingPlan(PlanType.TeamsAnnually).Returns(existingPlan); + organizationUpgradeQuery.NewPlans(PlanType.EnterpriseAnnually).Returns(newPlans); + + var validateUpgradeCommand = sutProvider.GetDependency(); + var organizationService = sutProvider.GetDependency(); + var referenceEventService = sutProvider.GetDependency(); + var currentContext = sutProvider.GetDependency(); + var paymentService = sutProvider.GetDependency(); + var organizationSignUpValidationStrategy = sutProvider.GetDependency(); + + var command = new OrganizationUpgradePlanCommand( + organizationUpgradeQuery, + validateUpgradeCommand, + organizationService, + referenceEventService, + currentContext, + paymentService, + organizationSignUpValidationStrategy + ); + + await Assert.ThrowsAsync(() => command.UpgradePlanAsync(organizationId, upgrade)); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs index 27b11a020240..9ffb336958eb 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs @@ -1,6 +1,60 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Repositories; +using Bit.Core.Models.StaticStore; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +[SutProviderCustomize] public class OrganizationUpgradeQueryTests { - + + [Theory] + [BitAutoData] + public void ExistingPlan_ShouldReturnMatchingPlan(SutProvider sutProvider) + { + const PlanType planType = PlanType.EnterpriseAnnually; + var expectedPlan = new Plan { Type = planType }; + var plans = new List { expectedPlan, new Plan { Type = PlanType.EnterpriseAnnually } }; + StaticStore.Plans = plans; + + var result = sutProvider.Sut.ExistingPlan(planType); + + Assert.Equal(expectedPlan, result); + } + + [Theory] + [BitAutoData] + public void NewPlans_ShouldReturnMatchingPlans(SutProvider sutProvider) + { + const PlanType planType = PlanType.EnterpriseAnnually; + var matchingPlan = new Plan { Type = planType, Disabled = false }; + var disabledPlan = new Plan { Type = planType, Disabled = true }; + var plans = new List { matchingPlan, disabledPlan }; + StaticStore.Plans = plans; + + var result = sutProvider.Sut.NewPlans(planType); + + Assert.Contains(matchingPlan, result); + Assert.DoesNotContain(disabledPlan, result); + } + + [Theory] + [BitAutoData] + public async Task GetOrgById_ShouldReturnOrganization(SutProvider sutProvider) + { + var orgId = Guid.NewGuid(); + var expectedOrganization = new Organization { Id = orgId }; + sutProvider.GetDependency().GetByIdAsync(orgId).Returns(expectedOrganization); + + var result = await sutProvider.Sut.GetOrgById(orgId); + + Assert.Equal(expectedOrganization, result); + } } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs index b776c43ee60a..0ccaaa2be3c1 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs @@ -1,6 +1,163 @@ -namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Repositories; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; +namespace Bit.Core.Test.OrganizationFeatures.OrganizationPlanUpgrade; + +[SutProviderCustomize] public class ValidateUpgradeCommandTests { - + [Theory] + [BitAutoData] + public void ValidatePlan_ExistingPlanIsNull_ThrowsBadRequestException(SutProvider sutProvider, Plan newPlan) + { + Assert.Throws(() => sutProvider.Sut.ValidatePlan(newPlan, null)); + } + + [Theory] + [BitAutoData] + public void ValidatePlan_NewPlanIsNull_ThrowsBadRequestException(SutProvider sutProvider, Plan existingPlan) + { + Assert.Throws(() => sutProvider.Sut.ValidatePlan(null, existingPlan)); + } + + [Theory] + [BitAutoData] + public void ValidatePlan_NewPlanIsDisabled_ThrowsBadRequestException(SutProvider sutProvider, Plan existingPlan, Plan newPlan) + { + newPlan = new Plan { Disabled = true }; + + Assert.Throws(() => sutProvider.Sut.ValidatePlan(newPlan, existingPlan)); + } + + [Theory] + [BitAutoData] + public void ValidatePlan_ExistingPlanTypeIsSameAsNewPlanType_ThrowsBadRequestException(SutProvider sutProvider, Plan existingPlan, Plan newPlan) + { + existingPlan = new Plan { Type = PlanType.EnterpriseAnnually }; + newPlan = new Plan { Type = PlanType.EnterpriseAnnually }; + + Assert.Throws(() => sutProvider.Sut.ValidatePlan(newPlan, existingPlan)); + } + + [Theory] + [BitAutoData] + public void + ValidatePlan_ExistingPlanUpgradeSortOrderIsGreaterThanOrEqualToNewPlanUpgradeSortOrder_ThrowsBadRequestException(SutProvider sutProvider, Plan existingPlan, Plan newPlan) + { + existingPlan = new Plan { UpgradeSortOrder = 2 }; + newPlan = new Plan { UpgradeSortOrder = 1 }; + + Assert.Throws(() => sutProvider.Sut.ValidatePlan(newPlan, existingPlan)); + } + + [Theory] + [BitAutoData] + public void ValidatePlan_ExistingPlanTypeIsNotFree_ThrowsBadRequestException(SutProvider sutProvider, Plan existingPlan, Plan newPlan) + { + existingPlan = new Plan { Type = PlanType.TeamsAnnually, UpgradeSortOrder = 2 }; + newPlan = new Plan { Type = PlanType.EnterpriseAnnually, UpgradeSortOrder = 3 }; + + Assert.Throws(() => sutProvider.Sut.ValidatePlan(newPlan, existingPlan)); + } + + [Theory] + [BitAutoData] + public async Task ValidateSeatsAsync_OrganizationSeatsIsGreaterThanNewPlanSeats_OccupiedSeatCountNotChecked( + SutProvider sutProvider, OrganizationUpgrade upgrade) + { + var organization = new Organization { Seats = 5 }; + var passwordManagerPlan = new Plan { BaseSeats = 5 }; + + await sutProvider.Sut.ValidateSeatsAsync(organization, passwordManagerPlan, upgrade); + + await sutProvider.GetDependency().DidNotReceive().GetOccupiedSeatCountByOrganizationIdAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task ValidateSmSeatsAsync_OrganizationSmSeatsIsGreaterThanNewPlanSeats_OccupiedSmSeatCountNotChecked( + SutProvider sutProvider) + { + var organization = new Organization { SmSeats = 5 }; + var newPlan = new Plan { BaseSeats = 5 }; + var upgrade = new OrganizationUpgrade(); + + await sutProvider.Sut.ValidateSmSeatsAsync(organization, newPlan, upgrade); + + await sutProvider.GetDependency().DidNotReceive().GetOccupiedSmSeatCountByOrganizationIdAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task ValidateCollectionsAsync_UpgradePlanAllowsAddingCollections_CollectionCountDoesNotExceedLimit( + SutProvider sutProvider) + { + var organization = new Organization { Id = Guid.NewGuid() }; + var newPlan = new Plan { MaxCollections = 5 }; + sutProvider.GetDependency().GetCountByOrganizationIdAsync(organization.Id).Returns(3); + + await sutProvider.Sut.ValidateCollectionsAsync(organization, newPlan); + + await sutProvider.GetDependency().Received(1).GetCountByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData] + public async Task ValidateGroupsAsync_NewPlanDoesNotAllowGroupsAndOrganizationHasGroups_ThrowsBadRequestException(SutProvider sutProvider) + { + var organization = new Organization { Id = Guid.NewGuid(), UseGroups = true }; + var newPlan = new Plan { HasGroups = false }; + var groups = new List { new Group() }; + + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(groups); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidateGroupsAsync(organization, newPlan)); + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData] + public async Task ValidatePoliciesAsync_NewPlanDoesNotAllowPoliciesAndOrganizationHasEnabledPolicies_ThrowsBadRequestException( + SutProvider sutProvider) + { + var organization = new Organization { Id = Guid.NewGuid(), UsePolicies = true }; + var newPlan = new Plan { HasPolicies = false }; + var policies = new List { new Policy { Enabled = true } }; + + sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(policies); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidatePoliciesAsync(organization, newPlan)); + + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData] + public async Task ValidateSsoAsync_NewPlanDoesNotAllowSsoAndOrganizationHasEnabledSsoConfig_ThrowsBadRequestException(SutProvider sutProvider) + { + var organization = new Organization { Id = Guid.NewGuid(), UseSso = true }; + var newPlan = new Plan { HasSso = false }; + var ssoConfig = new SsoConfig { Enabled = true }; + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id).Returns(ssoConfig); + + await Assert.ThrowsAsync(async () => + await sutProvider.Sut.ValidateSsoAsync(organization, newPlan)); + + await sutProvider.GetDependency().Received(1).GetByOrganizationIdAsync(organization.Id); + } + } From d487a8f214929ea07d0eae48b8a50b8b86aa73ab Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:32:18 +0100 Subject: [PATCH 28/37] Add store procedure changes --- ...eadOccupiedSmSeatCountByOrganizationId.sql | 16 ++++++++++ ...rviceAccount_ReadCountByOrganizationId.sql | 13 ++++++++ ..._OrgUserReadOccupiedSmSeatCountByOrgId.sql | 32 +++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId.sql create mode 100644 src/Sql/dbo/Stored Procedures/ServiceAccount_ReadCountByOrganizationId.sql create mode 100644 util/Migrator/DbScripts/2023-06-08_00_OrgUserReadOccupiedSmSeatCountByOrgId.sql diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId.sql new file mode 100644 index 000000000000..3c3792effe59 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId.sql @@ -0,0 +1,16 @@ +CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(1) + FROM + [dbo].[OrganizationUserView] + WHERE + OrganizationId = @OrganizationId + AND Status >= 0 --Invited + AND AccessSecretsManager = 1 +END +GO diff --git a/src/Sql/dbo/Stored Procedures/ServiceAccount_ReadCountByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/ServiceAccount_ReadCountByOrganizationId.sql new file mode 100644 index 000000000000..e904df1b09a5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/ServiceAccount_ReadCountByOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[ServiceAccount_ReadCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(1) + FROM + [dbo].[ServiceAccount] + WHERE + OrganizationId = @OrganizationId +END \ No newline at end of file diff --git a/util/Migrator/DbScripts/2023-06-08_00_OrgUserReadOccupiedSmSeatCountByOrgId.sql b/util/Migrator/DbScripts/2023-06-08_00_OrgUserReadOccupiedSmSeatCountByOrgId.sql new file mode 100644 index 000000000000..bb31b1e6007b --- /dev/null +++ b/util/Migrator/DbScripts/2023-06-08_00_OrgUserReadOccupiedSmSeatCountByOrgId.sql @@ -0,0 +1,32 @@ +CREATE OR ALTER PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSmSeatCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(1) + FROM + [dbo].[OrganizationUserView] + WHERE + OrganizationId = @OrganizationId + AND Status >= 0 --Invited + AND AccessSecretsManager = 1 +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[ServiceAccount_ReadCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + COUNT(1) + FROM + [dbo].[ServiceAccount] + WHERE + OrganizationId = @OrganizationId +END +GO + From 44a7f824179847db88075e993c3c748ef5959140 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:42:48 +0100 Subject: [PATCH 29/37] format whitespace --- src/Api/Controllers/OrganizationsController.cs | 4 ++-- .../OrganizationServiceCollectionExtensions.cs | 2 +- .../Repositories/OrganizationUserRepository.cs | 4 ++-- .../Repositories/OrganizationUserRepository.cs | 2 +- ...izationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 6a7f785ccd57..f28db9c36fea 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -76,7 +76,7 @@ public OrganizationsController( IFeatureService featureService, GlobalSettings globalSettings, ILicensingService licensingService, - IOrganizationSignUpCommand organizationSignUpCommand, + IOrganizationSignUpCommand organizationSignUpCommand, IOrganizationUpgradePlanCommand organizationUpgradePlanCommand) { _organizationRepository = organizationRepository; @@ -324,7 +324,7 @@ public async Task PostUpgrade(string id, [FromBody] Organi !model.UseSecretsManager ? await _organizationService.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()) : await _organizationUpgradePlanCommand.UpgradePlanAsync(orgIdGuid, model.ToOrganizationUpgrade()); - + return new PaymentResponseModel { Success = result.Item1, PaymentIntentClientSecret = result.Item2 }; } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 7b1ef75bc945..6bf2582f54ff 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -130,7 +130,7 @@ private static void AddOrganizationSignUpCommands(this IServiceCollection servic services.AddScoped(); services.AddScoped(); } - + private static void AddOrganizationUpgradeCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs index 7cd6284a010d..e14cd9e357b0 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs @@ -98,7 +98,7 @@ public async Task GetOccupiedSeatCountByOrganizationIdAsync(Guid organizati return result; } } - + public async Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) @@ -111,7 +111,7 @@ public async Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organiza return result; } } - + public async Task GetOccupiedServiceAccountCountByOrganizationIdAsync(Guid organizationId) { using (var connection = new SqlConnection(ConnectionString)) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs index 4d6c14ed8943..0966b4dd61c7 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs @@ -621,7 +621,7 @@ on p.OrganizationId equals ou.OrganizationId return await query.ToListAsync(); } } - + public async Task GetOccupiedSmSeatCountByOrganizationIdAsync(Guid organizationId) { var query = new OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(organizationId); diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs index 30a65fe12211..0f21a80ba64f 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery.cs @@ -15,8 +15,8 @@ public OrganizationUserReadOccupiedSmSeatCountByOrganizationIdQuery(Guid organiz public IQueryable Run(DatabaseContext dbContext) { var query = from ou in dbContext.OrganizationUsers - where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited && ou.AccessSecretsManager == true - select ou; + where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited && ou.AccessSecretsManager == true + select ou; return query; } } From 7fcbf9bf8b6c7cd98568653c582308bd5397a76f Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:48:24 +0100 Subject: [PATCH 30/37] Fix using directive is unnecessary --- src/Api/Controllers/OrganizationsController.cs | 1 - .../Interface/IValidateUpgradeCommand.cs | 8 +------- .../PasswordManagerSignUpValidationStrategy.cs | 3 +-- .../OrganizationSignUp/OrganizationSignUpCommandTests.cs | 1 - 4 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index f28db9c36fea..fcbe65d2d3ad 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -12,7 +12,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs index fab0751b3a2d..fb10281a4f90 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs @@ -1,12 +1,6 @@ -using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Repositories; -using Bit.Core.Exceptions; -using Bit.Core.Models.StaticStore; +using Bit.Core.Models.StaticStore; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Repositories; namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs index 5fb07fedcd89..275f3aeffe54 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategy.cs @@ -1,5 +1,4 @@ -using Bit.Core.Services; -using Bit.Core.Exceptions; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs index f2cfe180cc8f..088dd7b9d84f 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommandTests.cs @@ -1,5 +1,4 @@ using AutoFixture; -using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; From 848348484a30bfefd999f526099decbf2a424848 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 12:57:05 +0100 Subject: [PATCH 31/37] Fix imports ordering --- .../Interface/IValidateUpgradeCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs index fb10281a4f90..7e2b77c8b754 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/Interface/IValidateUpgradeCommand.cs @@ -1,6 +1,6 @@ -using Bit.Core.Models.StaticStore; -using Bit.Core.Entities; +using Bit.Core.Entities; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; From b8261db6ed4eaee56a4f74c629fb259c42e9de24 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 13:05:46 +0100 Subject: [PATCH 32/37] Fix the failing test --- .../Api.Test/Controllers/OrganizationsControllerTests.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index 069b4bb27c70..86fecb5d145a 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -11,6 +11,8 @@ using Bit.Core.Exceptions; using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; +using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -41,6 +43,8 @@ public class OrganizationsControllerTests : IDisposable private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IFeatureService _featureService; private readonly ILicensingService _licensingService; + private readonly IOrganizationSignUpCommand _organizationSignUpCommand; + private readonly IOrganizationUpgradePlanCommand _organizationUpgradePlanCommand; private readonly OrganizationsController _sut; @@ -65,12 +69,15 @@ public OrganizationsControllerTests() _updateOrganizationLicenseCommand = Substitute.For(); _featureService = Substitute.For(); _licensingService = Substitute.For(); + _organizationSignUpCommand = Substitute.For(); + _organizationUpgradePlanCommand = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _providerRepository, _organizationService, _userService, _paymentService, _currentContext, _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand, _createOrganizationApiKeyCommand, _organizationApiKeyRepository, _updateOrganizationLicenseCommand, - _cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService); + _cloudGetOrganizationLicenseQuery, _featureService, _globalSettings, _licensingService, + _organizationSignUpCommand, _organizationUpgradePlanCommand); } public void Dispose() From c71cb5acb5c382877d9febcf2ab885f681d7c33e Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 13:53:48 +0100 Subject: [PATCH 33/37] Fix imports ordering on upgrade plan --- .../OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs index 065b1f759fc0..c3121993fd01 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommand.cs @@ -1,8 +1,9 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; using Bit.Core.OrganizationFeatures.OrganizationSignUp; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; @@ -10,7 +11,6 @@ using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; -using Bit.Core.Models.StaticStore; namespace Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; From 4f456ac0fb631e51c3844bd25c8b04468a5cfb96 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 14:05:13 +0100 Subject: [PATCH 34/37] Fix imports ordering --- ...ordManagerSignUpValidationStrategyTests.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs index c391c6036867..2eb91959bdf0 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs @@ -1,12 +1,11 @@ -using AutoFixture.Xunit2; -using Bit.Core.Enums; +using Bit.Core.Enums; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSignUp; -using Bit.Test.Common.AutoFixture.Attributes; using Bit.Core.Exceptions; using Xunit; -using Bit.Core.Models.StaticStore; using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; @@ -27,9 +26,9 @@ public void Validate_WhenPlanDoesNotAllowAdditionalStorageAndUpgradeRequestsAddi [Theory] [BitAutoData] public void Validate_WhenUpgradeRequestsNegativeAdditionalStorage_ThrowsBadRequestException( - SutProvider sutProvider, - [Frozen] OrganizationUpgrade upgrade) + SutProvider sutProvider) { + var upgrade = new OrganizationUpgrade(); var plan = new Plan { HasAdditionalStorageOption = true, BitwardenProduct = BitwardenProductType.PasswordManager }; upgrade.AdditionalStorageGb = -5; @@ -39,10 +38,10 @@ public void Validate_WhenUpgradeRequestsNegativeAdditionalStorage_ThrowsBadReque [Theory] [BitAutoData] public void Validate_WhenNoSeatsAfterUpgrade_ThrowsBadRequestException( - SutProvider sutProvider, - [Frozen] Plan plan, - [Frozen] OrganizationUpgrade upgrade) + SutProvider sutProvider) { + var plan = new Plan(); + var upgrade = new OrganizationUpgrade(); plan.BaseSeats = 5; plan.BitwardenProduct = BitwardenProductType.PasswordManager; upgrade.AdditionalSeats = -5; @@ -53,10 +52,10 @@ public void Validate_WhenNoSeatsAfterUpgrade_ThrowsBadRequestException( [Theory] [BitAutoData] public void Validate_WhenUpgradeRequestsNegativeAdditionalSeats_ThrowsBadRequestException( - SutProvider sutProvider, - [Frozen] Plan plan, - [Frozen] OrganizationUpgrade upgrade) + SutProvider sutProvider) { + var plan = new Plan(); + var upgrade = new OrganizationUpgrade(); plan.BitwardenProduct = BitwardenProductType.PasswordManager; upgrade.AdditionalSeats = -3; @@ -66,10 +65,10 @@ public void Validate_WhenUpgradeRequestsNegativeAdditionalSeats_ThrowsBadRequest [Theory] [BitAutoData] public void Validate_WhenPlanDoesNotAllowAdditionalSeatsAndUpgradeRequestsAdditionalSeats_ThrowsBadRequestException( - SutProvider sutProvider, - [Frozen] Plan plan, - [Frozen] OrganizationUpgrade upgrade) + SutProvider sutProvider) { + var plan = new Plan(); + var upgrade = new OrganizationUpgrade(); plan.HasAdditionalSeatsOption = false; plan.BitwardenProduct = BitwardenProductType.PasswordManager; upgrade.AdditionalSeats = 2; From aa6cdb43319d95a98b3e07e9fcd83d73e79d638c Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 14:12:27 +0100 Subject: [PATCH 35/37] Fix imports ordering --- .../PasswordManagerSignUpValidationStrategyTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs index 2eb91959bdf0..f12f1c4e9cce 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs @@ -1,8 +1,8 @@ -using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSignUp; -using Bit.Core.Exceptions; using Xunit; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; From e78e482c0d7d9157ed84267becc5a49417e8918c Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 14:20:59 +0100 Subject: [PATCH 36/37] Fix imports ordering --- .../ValidateUpgradeCommand.cs | 4 +- .../OrganizationSignUpCommand.cs | 10 ++-- .../OrganizationDomainRepository.cs | 4 +- .../SelfHostedOrganizationDetailsTests.cs | 57 ++++++++++++------- .../OrganizationUpgradePlanCommandTests.cs | 4 +- .../OrganizationUpgradeQueryTests.cs | 2 +- .../ValidateUpgradeCommandTests.cs | 4 +- ...ordManagerSignUpValidationStrategyTests.cs | 6 +- util/EfShared/MigrationBuilderExtensions.cs | 2 +- 9 files changed, 56 insertions(+), 37 deletions(-) diff --git a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs index 9d3044d6e85d..918543f72299 100644 --- a/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommand.cs @@ -1,11 +1,11 @@ using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; -using Bit.Core.Exceptions; -using Bit.Core.Models.StaticStore; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; using Bit.Core.Repositories; diff --git a/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs index d605de9547d0..135b4156cd38 100644 --- a/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSignUp/OrganizationSignUpCommand.cs @@ -1,16 +1,16 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Business; -using Bit.Core.Services; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Utilities; using Bit.Core.Exceptions; +using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; +using Bit.Core.Utilities; namespace Bit.Core.OrganizationFeatures.OrganizationSignUp; diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 64438bf70489..08bee361227d 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -105,7 +105,7 @@ from s in sJoin.DefaultIfEmpty() return Mapper.Map(domain); } - public async Task> GetExpiredOrganizationDomainsAsync() + public Task> GetExpiredOrganizationDomainsAsync() { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -117,7 +117,7 @@ from s in sJoin.DefaultIfEmpty() && x.VerifiedDate == null) .ToList(); - return Mapper.Map>(domains); + return Task.FromResult(Mapper.Map>(domains)); } public async Task DeleteExpiredAsync(int expirationPeriod) diff --git a/test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs b/test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs index c0ab7b9c4014..b69b7e51bef0 100644 --- a/test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs +++ b/test/Core.Test/Models/Data/SelfHostedOrganizationDetailsTests.cs @@ -17,7 +17,7 @@ public class SelfHostedOrganizationDetailsTests [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_Success(List orgUsers, + public Task ValidateForOrganization_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -26,12 +26,13 @@ public async Task ValidateForOrganization_Success(List orgUser Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_OccupiedSeatCount_ExceedsLicense_Fail(List orgUsers, + public Task ValidateForOrganization_OccupiedSeatCount_ExceedsLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -41,12 +42,13 @@ public async Task ValidateForOrganization_OccupiedSeatCount_ExceedsLicense_Fail( Assert.False(result); Assert.Contains("Remove some users", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_MaxCollections_ExceedsLicense_Fail(List orgUsers, + public Task ValidateForOrganization_MaxCollections_ExceedsLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -56,12 +58,13 @@ public async Task ValidateForOrganization_MaxCollections_ExceedsLicense_Fail(Lis Assert.False(result); Assert.Contains("Remove some collections", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_Groups_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_Groups_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -71,12 +74,13 @@ public async Task ValidateForOrganization_Groups_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_Policies_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -86,12 +90,13 @@ public async Task ValidateForOrganization_Policies_NotAllowedByLicense_Fail(List Assert.False(result); Assert.Contains("Your new license does not allow for the use of policies", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_DisabledPolicies_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_DisabledPolicies_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -102,12 +107,13 @@ public async Task ValidateForOrganization_DisabledPolicies_NotAllowedByLicense_S Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_Sso_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_Sso_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -117,12 +123,13 @@ public async Task ValidateForOrganization_Sso_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_DisabledSso_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -133,12 +140,13 @@ public async Task ValidateForOrganization_DisabledSso_NotAllowedByLicense_Succes Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_NoSso_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_NoSso_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -149,12 +157,13 @@ public async Task ValidateForOrganization_NoSso_NotAllowedByLicense_Success(List Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_KeyConnector_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_KeyConnector_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -164,12 +173,13 @@ public async Task ValidateForOrganization_KeyConnector_NotAllowedByLicense_Fail( Assert.False(result); Assert.Contains("Your new license does not allow for the use of Key Connector", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_DisabledKeyConnector_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_DisabledKeyConnector_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -180,12 +190,13 @@ public async Task ValidateForOrganization_DisabledKeyConnector_NotAllowedByLicen Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_NoSsoKeyConnector_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_NoSsoKeyConnector_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -196,12 +207,13 @@ public async Task ValidateForOrganization_NoSsoKeyConnector_NotAllowedByLicense_ Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_Scim_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_Scim_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -211,12 +223,13 @@ public async Task ValidateForOrganization_Scim_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_DisabledScim_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -228,12 +241,13 @@ public async Task ValidateForOrganization_DisabledScim_NotAllowedByLicense_Succe Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_NoScimConfig_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_NoScimConfig_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -244,12 +258,13 @@ public async Task ValidateForOrganization_NoScimConfig_NotAllowedByLicense_Succe Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_CustomPermissions_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_CustomPermissions_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -259,12 +274,13 @@ public async Task ValidateForOrganization_CustomPermissions_NotAllowedByLicense_ Assert.False(result); Assert.Contains("Your new plan does not allow the Custom Permissions feature", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_NoCustomPermissions_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_NoCustomPermissions_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -275,12 +291,13 @@ public async Task ValidateForOrganization_NoCustomPermissions_NotAllowedByLicens Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_ResetPassword_NotAllowedByLicense_Fail(List orgUsers, + public Task ValidateForOrganization_ResetPassword_NotAllowedByLicense_Fail(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -290,12 +307,13 @@ public async Task ValidateForOrganization_ResetPassword_NotAllowedByLicense_Fail Assert.False(result); Assert.Contains("Your new license does not allow the Password Reset feature", exception); + return Task.CompletedTask; } [Theory] [BitAutoData] [OrganizationLicenseCustomize] - public async Task ValidateForOrganization_DisabledResetPassword_NotAllowedByLicense_Success(List orgUsers, + public Task ValidateForOrganization_DisabledResetPassword_NotAllowedByLicense_Success(List orgUsers, List policies, SsoConfig ssoConfig, List> scimConnections, OrganizationLicense license) { var (orgDetails, orgLicense) = GetOrganizationAndLicense(orgUsers, policies, ssoConfig, scimConnections, license); @@ -306,6 +324,7 @@ public async Task ValidateForOrganization_DisabledResetPassword_NotAllowedByLice Assert.True(result); Assert.True(string.IsNullOrEmpty(exception)); + return Task.CompletedTask; } private (SelfHostedOrganizationDetails organization, OrganizationLicense license) GetOrganizationAndLicense(List orgUsers, diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs index ede730427b1f..f2411400df52 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradePlanCommandTests.cs @@ -1,14 +1,14 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade.Interface; using Bit.Core.OrganizationFeatures.OrganizationSignUp.Interfaces; using Bit.Core.Services; -using Bit.Core.Models.StaticStore; using Bit.Core.Tools.Services; -using Bit.Core.Exceptions; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs index 9ffb336958eb..914906f19be9 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/OrganizationUpgradeQueryTests.cs @@ -1,8 +1,8 @@ using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; using Bit.Core.Repositories; -using Bit.Core.Models.StaticStore; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs index 0ccaaa2be3c1..2cef5743e880 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationPlanUpgrade/ValidateUpgradeCommandTests.cs @@ -2,11 +2,11 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; -using Bit.Core.Repositories; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; +using Bit.Core.OrganizationFeatures.OrganizationPlanUpgrade; +using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs index f12f1c4e9cce..44ba81ad9f80 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSignUp/PasswordManagerSignUpValidationStrategyTests.cs @@ -1,11 +1,11 @@ -using Bit.Core.Exceptions; -using Bit.Core.Enums; +using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; using Bit.Core.OrganizationFeatures.OrganizationSignUp; -using Xunit; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; namespace Bit.Core.Test.OrganizationFeatures.OrganizationSignUp; diff --git a/util/EfShared/MigrationBuilderExtensions.cs b/util/EfShared/MigrationBuilderExtensions.cs index b59ad207fb6e..0ad02f74c479 100644 --- a/util/EfShared/MigrationBuilderExtensions.cs +++ b/util/EfShared/MigrationBuilderExtensions.cs @@ -19,7 +19,7 @@ public static class MigrationBuilderExtensions /// The MigrationBuilder instance the sql should be applied to /// The file name portion of the resource name, it is assumed to be in a Scripts folder /// The direction of the migration taking place - public static void SqlResource(this MigrationBuilder migrationBuilder, string resourceName, [CallerMemberName] string dir = null) + public static void SqlResource(this MigrationBuilder migrationBuilder, string resourceName, [CallerMemberName] string? dir = null) { var formattedResourceName = string.IsNullOrEmpty(dir) ? resourceName : string.Format(resourceName, dir); From cdc2e9487868bbf12201a819aae4dbc26703b686 Mon Sep 17 00:00:00 2001 From: cyprain-okeke Date: Wed, 14 Jun 2023 14:41:43 +0100 Subject: [PATCH 37/37] Fix the failed test after dotnet format --- .../Repositories/OrganizationDomainRepository.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs index 08bee361227d..64438bf70489 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationDomainRepository.cs @@ -105,7 +105,7 @@ from s in sJoin.DefaultIfEmpty() return Mapper.Map(domain); } - public Task> GetExpiredOrganizationDomainsAsync() + public async Task> GetExpiredOrganizationDomainsAsync() { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -117,7 +117,7 @@ from s in sJoin.DefaultIfEmpty() && x.VerifiedDate == null) .ToList(); - return Task.FromResult(Mapper.Map>(domains)); + return Mapper.Map>(domains); } public async Task DeleteExpiredAsync(int expirationPeriod)