diff --git a/shared/django_apps/codecov_auth/models.py b/shared/django_apps/codecov_auth/models.py index 393fc6238..f32afbb1f 100644 --- a/shared/django_apps/codecov_auth/models.py +++ b/shared/django_apps/codecov_auth/models.py @@ -2,7 +2,6 @@ import logging import os import uuid -from dataclasses import asdict from datetime import datetime from hashlib import md5 from typing import Optional, Self @@ -30,7 +29,7 @@ from shared.django_apps.codecov_auth.managers import OwnerManager from shared.django_apps.core.managers import RepositoryManager from shared.django_apps.core.models import DateTimeWithoutTZField, Repository -from shared.plan.constants import USER_PLAN_REPRESENTATIONS, PlanName +from shared.plan.constants import PlanName, TrialDaysAmount # Added to avoid 'doesn't declare an explicit app_label and isn't in an application in INSTALLED_APPS' error\ # Needs to be called the same as the API app @@ -221,10 +220,22 @@ def pretty_plan(self) -> dict | None: This is how we represent the details of a plan to a user, see plan.constants.py We inject quantity to make plan management easier on api, see PlanSerializer """ - if self.plan in USER_PLAN_REPRESENTATIONS: - plan_details = asdict(USER_PLAN_REPRESENTATIONS[self.plan]) - plan_details.update({"quantity": self.plan_seat_count}) - return plan_details + plan_details = Plan.objects.select_related("tier").get(name=self.plan) + if plan_details: + return { + "marketing_name": plan_details.marketing_name, + "value": plan_details.name, + "billing_rate": plan_details.billing_rate, + "base_unit_price": plan_details.base_unit_price, + "benefits": plan_details.benefits, + "tier_name": plan_details.tier.tier_name, + "monthly_uploads_limit": plan_details.monthly_uploads_limit, + "trial_days": TrialDaysAmount.CODECOV_SENTRY.value + if plan_details.name == PlanName.TRIAL_PLAN_NAME.value + else None, + "quantity": self.plan_seat_count, + } + return None def can_activate_user(self, user: User | None = None) -> bool: """ @@ -593,16 +604,23 @@ def avatar_url(self, size=DEFAULT_AVATAR_SIZE): def pretty_plan(self): if self.account: return self.account.pretty_plan - if self.plan in USER_PLAN_REPRESENTATIONS: - plan_details = asdict(USER_PLAN_REPRESENTATIONS[self.plan]) - # update with quantity they've purchased - # allows api users to update the quantity - # by modifying the "plan", sidestepping - # some iffy data modeling - - plan_details.update({"quantity": self.plan_user_count}) - return plan_details + plan_details = Plan.objects.select_related("tier").get(name=self.plan) + if plan_details: + return { + "marketing_name": plan_details.marketing_name, + "value": plan_details.name, + "billing_rate": plan_details.billing_rate, + "base_unit_price": plan_details.base_unit_price, + "benefits": plan_details.benefits, + "tier_name": plan_details.tier.tier_name, + "monthly_uploads_limit": plan_details.monthly_uploads_limit, + "trial_days": TrialDaysAmount.CODECOV_SENTRY.value + if plan_details.name == PlanName.TRIAL_PLAN_NAME.value + else None, + "quantity": self.plan_user_count, + } + return None def can_activate_user(self, owner_user: Self) -> bool: owner_org = self diff --git a/shared/django_apps/codecov_auth/services/org_level_token_service.py b/shared/django_apps/codecov_auth/services/org_level_token_service.py index 508b3de81..a72617e22 100644 --- a/shared/django_apps/codecov_auth/services/org_level_token_service.py +++ b/shared/django_apps/codecov_auth/services/org_level_token_service.py @@ -5,8 +5,7 @@ from django.dispatch import receiver from django.forms import ValidationError -from shared.django_apps.codecov_auth.models import OrganizationLevelToken, Owner -from shared.plan.constants import USER_PLAN_REPRESENTATIONS +from shared.django_apps.codecov_auth.models import OrganizationLevelToken, Owner, Plan log = logging.getLogger(__name__) @@ -18,9 +17,10 @@ class OrgLevelTokenService(object): -- only 1 token per Owner """ + # MIGHT BE ABLE TO REMOVE THIS AND SUBSEQUENT DOWNSTREAM STUFF @classmethod def org_can_have_upload_token(cls, org: Owner): - return org.plan in USER_PLAN_REPRESENTATIONS + return Plan.objects.filter(name=org.plan, is_active=True).exists() @classmethod def get_or_create_org_token(cls, org: Owner): diff --git a/shared/django_apps/codecov_auth/tests/factories.py b/shared/django_apps/codecov_auth/tests/factories.py index 65c2a5bda..c3fe02b6b 100644 --- a/shared/django_apps/codecov_auth/tests/factories.py +++ b/shared/django_apps/codecov_auth/tests/factories.py @@ -23,7 +23,7 @@ UserToken, ) from shared.encryption.oauth import get_encryptor_from_configuration -from shared.plan.constants import TrialStatus +from shared.plan.constants import PlanName, TierName, TrialStatus encryptor = get_encryptor_from_configuration() @@ -177,20 +177,26 @@ class TierFactory(DjangoModelFactory): class Meta: model = Tier - tier_name = factory.Faker("name") + tier_name = TierName.BASIC.value + bundle_analysis = False + test_analytics = False + flaky_test_detection = False + project_coverage = False + private_repo_support = False class PlanFactory(DjangoModelFactory): class Meta: model = Plan - base_unit_price = factory.Faker("pyint") - benefits = [] + tier = factory.SubFactory(TierFactory) + base_unit_price = 0 + benefits = factory.LazyFunction(lambda: ["Benefit 1", "Benefit 2", "Benefit 3"]) billing_rate = None is_active = True - marketing_name = factory.Faker("name") - max_seats = None + marketing_name = factory.Faker("catch_phrase") + max_seats = 1 monthly_uploads_limit = None - paid_plan = True - name = factory.Faker("name") - tier = factory.SubFactory(TierFactory) + name = PlanName.BASIC_PLAN_NAME.value + paid_plan = False + stripe_id = None diff --git a/shared/plan/constants.py b/shared/plan/constants.py index 6823ef850..30583dbde 100644 --- a/shared/plan/constants.py +++ b/shared/plan/constants.py @@ -73,6 +73,26 @@ class TierName(enum.Enum): TEAM = "team" PRO = "pro" ENTERPRISE = "enterprise" + SENTRY = "sentry" + TRIAL = "trial" + + +def convert_to_DTO(plan) -> dict: + return { + "marketing_name": plan.marketing_name, + "value": plan.name, + "billing_rate": plan.billing_rate, + "base_unit_price": plan.base_unit_price, + "benefits": plan.benefits, + "tier_name": plan.tier.tier_name, + "monthly_uploads_limit": plan.monthly_uploads_limit, + "is_free_plan": not plan.paid_plan, + "is_pro_plan": plan.tier.tier_name == TierName.PRO.value, + "is_team_plan": plan.tier.tier_name == TierName.TEAM.value, + "is_enterprise_plan": plan.tier.tier_name == TierName.ENTERPRISE.value, + "is_trial_plan": plan.tier.tier_name == TierName.TRIAL.value, + "is_sentry_plan": plan.tier.tier_name == TierName.SENTRY.value, + } @dataclass(repr=False) @@ -99,7 +119,6 @@ def convert_to_DTO(self) -> dict: "benefits": self.benefits, "tier_name": self.tier_name, "monthly_uploads_limit": self.monthly_uploads_limit, - "trial_days": self.trial_days, "is_free_plan": self.tier_name == TierName.BASIC.value, "is_pro_plan": self.tier_name == TierName.PRO.value, "is_team_plan": self.tier_name == TierName.TEAM.value, @@ -189,7 +208,7 @@ def convert_to_DTO(self) -> dict: "Unlimited private repositories", "Priority Support", ], - tier_name=TierName.PRO.value, + tier_name=TierName.SENTRY.value, trial_days=TrialDaysAmount.CODECOV_SENTRY.value, monthly_uploads_limit=None, ), @@ -205,7 +224,7 @@ def convert_to_DTO(self) -> dict: "Unlimited private repositories", "Priority Support", ], - tier_name=TierName.PRO.value, + tier_name=TierName.SENTRY.value, trial_days=TrialDaysAmount.CODECOV_SENTRY.value, monthly_uploads_limit=None, ), @@ -342,7 +361,7 @@ def convert_to_DTO(self) -> dict: "Unlimited private repositories", "Priority Support", ], - tier_name=TierName.PRO.value, + tier_name=TierName.TRIAL.value, trial_days=None, monthly_uploads_limit=None, ), diff --git a/shared/plan/service.py b/shared/plan/service.py index cf0b8b23b..d0f412501 100644 --- a/shared/plan/service.py +++ b/shared/plan/service.py @@ -1,28 +1,21 @@ import logging from datetime import datetime, timedelta +from functools import cached_property from typing import List, Optional from shared.billing import is_pr_billing_plan from shared.config import get_config from shared.django_apps.codecov.commands.exceptions import ValidationError -from shared.django_apps.codecov_auth.models import Owner, Service +from shared.django_apps.codecov_auth.models import Owner, Plan, Service from shared.plan.constants import ( - BASIC_PLAN, - ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS, - FREE_PLAN, - FREE_PLAN_REPRESENTATIONS, - PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS, - SENTRY_PAID_USER_PLAN_REPRESENTATIONS, TEAM_PLAN_MAX_USERS, - TEAM_PLAN_REPRESENTATIONS, - TRIAL_PLAN_REPRESENTATION, TRIAL_PLAN_SEATS, - USER_PLAN_REPRESENTATIONS, PlanBillingRate, - PlanData, PlanName, + TierName, TrialDaysAmount, TrialStatus, + convert_to_DTO, ) from shared.self_hosted.service import enterprise_has_seats_left, license_seats @@ -55,19 +48,20 @@ def __init__(self, current_org: Owner): self.current_org = current_org.root_organization else: self.current_org = current_org - if self.current_org.plan not in USER_PLAN_REPRESENTATIONS: + + if not Plan.objects.filter(name=self.current_org.plan).exists(): raise ValueError("Unsupported plan") self._plan_data = None def update_plan(self, name: str, user_count: Optional[int]) -> None: """Updates the organization's plan and user count.""" - if name not in USER_PLAN_REPRESENTATIONS: + if not Plan.objects.filter(name=name).exists(): raise ValueError("Unsupported plan") if not user_count: raise ValueError("Quantity Needed") self.current_org.plan = name self.current_org.plan_user_count = user_count - self._plan_data = USER_PLAN_REPRESENTATIONS[self.current_org.plan] + self._plan_data = Plan.objects.select_related("tier").get(name=name) self.current_org.delinquent = False self.current_org.save() @@ -88,26 +82,21 @@ def has_account(self) -> bool: """Returns whether the organization has an associated account.""" return self.current_org.account is not None - @property - def plan_data(self) -> PlanData: + @cached_property + def plan_data(self) -> Plan: """Returns the plan data for the organization, either from account or default.""" if self._plan_data is None: - self._plan_data = USER_PLAN_REPRESENTATIONS.get( - self.current_org.account.plan + self._plan_data = Plan.objects.select_related("tier").get( + name=self.current_org.account.plan if self.has_account else self.current_org.plan ) return self._plan_data - @plan_data.setter - def plan_data(self, plan_data: Optional[PlanData]) -> None: - """Sets the plan data directly.""" - self._plan_data = plan_data - @property def plan_name(self) -> str: """Returns the name of the organization's current plan.""" - return self.plan_data.value + return self.plan_data.name @property def plan_user_count(self) -> int: @@ -159,29 +148,39 @@ def monthly_uploads_limit(self) -> Optional[int]: return self.plan_data.monthly_uploads_limit @property - def tier_name(self) -> str: + def tier_name(self) -> TierName: """Returns the tier name of the plan.""" - return self.plan_data.tier_name + return self.plan_data.tier.tier_name - def available_plans(self, owner: Owner) -> List[PlanData]: + def available_plans(self, owner: Owner) -> List[Plan]: """Returns the available plans for the owner and organization.""" - available_plans = [BASIC_PLAN] + available_plans = { + Plan.objects.select_related("tier").get(name=PlanName.BASIC_PLAN_NAME.value) + } - if self.plan_name == FREE_PLAN.value: - available_plans.append(FREE_PLAN) + curr_plan = self.plan_data + if not curr_plan.paid_plan: + available_plans.add(curr_plan) - available_plans += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() + # Build list of available tiers based on conditions + available_tiers = [TierName.PRO.value] if is_sentry_user(owner): - available_plans += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() + available_tiers.append(TierName.SENTRY.value) if ( - self.plan_activated_users is None + not self.plan_activated_users or len(self.plan_activated_users) <= TEAM_PLAN_MAX_USERS ): - available_plans += TEAM_PLAN_REPRESENTATIONS.values() + available_tiers.append(TierName.TEAM.value) + + available_plans.update( + Plan.objects.select_related("tier").filter( + tier__tier_name__in=available_tiers, is_active=True + ) + ) - return [plan.convert_to_DTO() for plan in available_plans] + return [convert_to_DTO(plan) for plan in available_plans] def _start_trial_helper( self, @@ -222,7 +221,7 @@ def start_trial(self, current_owner: Owner) -> None: """ if self.trial_status != TrialStatus.NOT_STARTED.value: raise ValidationError("Cannot start an existing trial") - if self.plan_name not in FREE_PLAN_REPRESENTATIONS: + if not Plan.objects.filter(name=self.plan_name, paid_plan=False).exists(): raise ValidationError("Cannot trial from a paid plan") self._start_trial_helper(current_owner) @@ -236,12 +235,12 @@ def start_trial_manually(self, current_owner: Owner, end_date: datetime) -> None No value """ # Start a new trial plan for free users currently not on trial - if self.plan_name in FREE_PLAN_REPRESENTATIONS: + + if self.plan_data.tier.tier_name == TierName.TRIAL.value: + self._start_trial_helper(current_owner, end_date, is_extension=True) + elif self.plan_data.paid_plan is False: self._start_trial_helper(current_owner, end_date, is_extension=False) # Extend an existing trial plan for users currently on trial - elif self.plan_name in TRIAL_PLAN_REPRESENTATION: - self._start_trial_helper(current_owner, end_date, is_extension=True) - # Paying users cannot start a trial else: raise ValidationError("Cannot trial from a paid plan") @@ -294,7 +293,7 @@ def trial_end_date(self) -> Optional[datetime]: @property def trial_total_days(self) -> Optional[TrialDaysAmount]: """Returns the total number of trial days.""" - return self.plan_data.trial_days + return TrialDaysAmount.CODECOV_SENTRY.value @property def is_org_trialing(self) -> bool: @@ -324,30 +323,30 @@ def has_seats_left(self) -> bool: @property def is_enterprise_plan(self) -> bool: - return self.plan_name in ENTERPRISE_CLOUD_USER_PLAN_REPRESENTATIONS + return self.plan_data.tier.tier_name == TierName.ENTERPRISE.value @property def is_free_plan(self) -> bool: - return self.plan_name in FREE_PLAN_REPRESENTATIONS + return self.plan_data.paid_plan is False and not self.is_org_trialing @property def is_pro_plan(self) -> bool: return ( - self.plan_name in PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS - or self.plan_name in SENTRY_PAID_USER_PLAN_REPRESENTATIONS + self.plan_data.tier.tier_name == TierName.PRO.value + or self.plan_data.tier.tier_name == TierName.SENTRY.value ) @property def is_sentry_plan(self) -> bool: - return self.plan_name in SENTRY_PAID_USER_PLAN_REPRESENTATIONS + return self.plan_data.tier.tier_name == TierName.SENTRY.value @property def is_team_plan(self) -> bool: - return self.plan_name in TEAM_PLAN_REPRESENTATIONS + return self.plan_data.tier.tier_name == TierName.TEAM.value @property def is_trial_plan(self) -> bool: - return self.plan_name in TRIAL_PLAN_REPRESENTATION + return self.plan_data.tier.tier_name == TierName.TRIAL.value @property def is_pr_billing_plan(self) -> bool: diff --git a/tests/helper.py b/tests/helper.py index a39fb359c..a065ed883 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,5 +1,13 @@ from json import dumps +from shared.django_apps.codecov_auth.models import BillingRate +from shared.django_apps.codecov_auth.tests.factories import PlanFactory, TierFactory +from shared.plan.constants import ( + PlanName, + PlanPrice, + TierName, +) + def v2_to_v3(report): def _sessions(sessions): @@ -46,3 +54,166 @@ def _sessions(sessions): "totals": report.get("totals", {}), "chunks": chunks, } + + +def mock_all_plans_and_tiers(): + trial_tier = TierFactory(tier_name=TierName.TRIAL.value) + PlanFactory( + tier=trial_tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + marketing_name="Developer", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + basic_tier = TierFactory(tier_name=TierName.BASIC.value) + PlanFactory( + name=PlanName.BASIC_PLAN_NAME.value, + tier=basic_tier, + marketing_name="Developer", + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + monthly_uploads_limit=250, + ) + PlanFactory( + name=PlanName.FREE_PLAN_NAME.value, + tier=basic_tier, + marketing_name="Developer", + benefits=[ + "Up to 1 user", + "Unlimited public repositories", + "Unlimited private repositories", + ], + ) + + pro_tier = TierFactory(tier_name=TierName.PRO.value) + PlanFactory( + name=PlanName.CODECOV_PRO_MONTHLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.CODECOV_PRO_YEARLY.value, + tier=pro_tier, + marketing_name="Pro", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + ) + + team_tier = TierFactory(tier_name=TierName.TEAM.value) + PlanFactory( + name=PlanName.TEAM_MONTHLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.TEAM_MONTHLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + PlanFactory( + name=PlanName.TEAM_YEARLY.value, + tier=team_tier, + marketing_name="Team", + benefits=[ + "Up to 10 users", + "Unlimited repositories", + "2500 private repo uploads", + "Patch coverage analysis", + ], + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.TEAM_YEARLY.value, + monthly_uploads_limit=2500, + paid_plan=True, + ) + + sentry_tier = TierFactory(tier_name=TierName.SENTRY.value) + PlanFactory( + name=PlanName.SENTRY_MONTHLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$12 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + PlanFactory( + name=PlanName.SENTRY_YEARLY.value, + tier=sentry_tier, + marketing_name="Sentry Pro", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Includes 5 seats", + "$10 per additional seat", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) + + enterprise_tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_MONTHLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + billing_rate=BillingRate.MONTHLY.value, + base_unit_price=PlanPrice.MONTHLY.value, + paid_plan=True, + ) + PlanFactory( + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + tier=enterprise_tier, + marketing_name="Enterprise Cloud", + billing_rate=BillingRate.ANNUALLY.value, + base_unit_price=PlanPrice.YEARLY.value, + paid_plan=True, + benefits=[ + "Configurable # of users", + "Unlimited public repositories", + "Unlimited private repositories", + "Priority Support", + ], + ) diff --git a/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py b/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py index b4f822c44..57ffd20ac 100644 --- a/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py +++ b/tests/unit/django_apps/codecov_auth/test_codecov_auth_models.py @@ -5,7 +5,7 @@ import pytest from django.db import IntegrityError from django.forms import ValidationError -from django.test import TransactionTestCase +from django.test import TestCase, TransactionTestCase from pytest import LogCaptureFixture from shared.django_apps.codecov_auth.models import ( @@ -41,9 +41,10 @@ PlanName, ) from shared.utils.test_utils import mock_config_helper +from tests.helper import mock_all_plans_and_tiers -class TestOwnerModel(TransactionTestCase): +class TestOwnerModel(TestCase): def setUp(self): self.owner = OwnerFactory(username="codecov_name", email="name@codecov.io") @@ -377,6 +378,7 @@ def test_can_activate_user_cannot_activate_account(self): assert not self.owner.can_activate_user(self.owner) def test_fields_that_account_overrides(self): + mock_all_plans_and_tiers() to_activate = OwnerFactory() self.owner.plan = PlanName.BASIC_PLAN_NAME.value self.owner.plan_user_count = 1 @@ -523,7 +525,7 @@ def test_has_yaml(self): assert org.has_yaml is True -class TestOrganizationLevelTokenModel(TransactionTestCase): +class TestOrganizationLevelTokenModel(TestCase): def test_can_save_org_token_for_org_basic_plan(self): owner = OwnerFactory(plan="users-basic") owner.save() @@ -548,7 +550,7 @@ def test_token_is_deleted_when_changing_user_plan( assert OrganizationLevelToken.objects.filter(owner=owner).count() == 0 -class TestGithubAppInstallationModel(TransactionTestCase): +class TestGithubAppInstallationModel(TestCase): DEFAULT_APP_ID = 12345 @pytest.fixture(autouse=True) @@ -673,7 +675,7 @@ def test_is_configured(self): ) -class TestGitHubAppInstallationNoDefaultAppIdConfig(TransactionTestCase): +class TestGitHubAppInstallationNoDefaultAppIdConfig(TestCase): @pytest.fixture(autouse=True) def mock_no_default_app_id(self, mocker): mock_config_helper(mocker, configs={"github.integration.id": None}) @@ -717,6 +719,7 @@ def test_account_with_billing_details(self): self.assertTrue(stripe.is_active) def test_account_with_users(self): + mock_all_plans_and_tiers() user_1 = UserFactory() OwnerFactory(user=user_1) user_2 = UserFactory() @@ -761,6 +764,7 @@ def test_account_with_users(self): self.assertEqual(account.pretty_plan, pretty_plan) def test_create_account_for_enterprise_experience(self): + mock_all_plans_and_tiers() # 2 separate OwnerOrgs that wish to Enterprise stripe_customer_id = "abc123" stripe_subscription_id = "defg456" diff --git a/tests/unit/plan/test_plan.py b/tests/unit/plan/test_plan.py index c1258f4eb..4729ac109 100644 --- a/tests/unit/plan/test_plan.py +++ b/tests/unit/plan/test_plan.py @@ -6,25 +6,31 @@ from shared.django_apps.codecov.commands.exceptions import ValidationError from shared.django_apps.codecov_auth.models import Service -from shared.django_apps.codecov_auth.tests.factories import OwnerFactory +from shared.django_apps.codecov_auth.tests.factories import ( + OwnerFactory, + PlanFactory, + TierFactory, +) from shared.plan.constants import ( - BASIC_PLAN, - FREE_PLAN, FREE_PLAN_REPRESENTATIONS, - PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS, - SENTRY_PAID_USER_PLAN_REPRESENTATIONS, - TEAM_PLAN_REPRESENTATIONS, TRIAL_PLAN_REPRESENTATION, TRIAL_PLAN_SEATS, PlanName, + TierName, TrialDaysAmount, TrialStatus, ) from shared.plan.service import PlanService +from tests.helper import mock_all_plans_and_tiers @freeze_time("2023-06-19") class PlanServiceTests(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def test_plan_service_trial_status_not_started(self): current_org = OwnerFactory(plan=PlanName.BASIC_PLAN_NAME.value) plan_service = PlanService(current_org=current_org) @@ -262,7 +268,6 @@ def test_plan_service_returns_plan_data_for_non_trial_basic_plan(self): assert ( plan_service.monthly_uploads_limit == 250 ) # should be 250 since not trialing - assert plan_service.trial_total_days == basic_plan.trial_days def test_plan_service_returns_plan_data_for_trialing_user_trial_plan(self): trial_start_date = datetime.utcnow() @@ -286,7 +291,6 @@ def test_plan_service_returns_plan_data_for_trialing_user_trial_plan(self): assert plan_service.base_unit_price == trial_plan.base_unit_price assert plan_service.benefits == trial_plan.benefits assert plan_service.monthly_uploads_limit is None # Not 250 since it's trialing - assert plan_service.trial_total_days == trial_plan.trial_days def test_plan_service_sets_default_plan_data_values_correctly(self): current_org = OwnerFactory( @@ -358,6 +362,11 @@ class AvailablePlansBeforeTrial(TestCase): - sentry customer, users-sentrym/y, no trial -> users-pr-inappm/y, users-sentrym/y, users-basic """ + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def setUp(self): self.current_org = OwnerFactory( trial_start_date=None, @@ -374,13 +383,20 @@ def test_available_plans_for_basic_plan_non_trial( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) def test_available_plans_for_free_plan_non_trial( self, @@ -390,14 +406,21 @@ def test_available_plans_for_free_plan_non_trial( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result.append(FREE_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.FREE_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) def test_available_plans_for_team_plan_non_trial( self, @@ -407,13 +430,20 @@ def test_available_plans_for_team_plan_non_trial( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) def test_available_plans_for_pro_plan_non_trial(self): self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value @@ -421,13 +451,20 @@ def test_available_plans_for_pro_plan_non_trial(self): plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_customer_basic_plan_non_trial( @@ -439,14 +476,22 @@ def test_available_plans_for_sentry_customer_basic_plan_non_trial( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_customer_team_plan_non_trial( @@ -458,14 +503,22 @@ def test_available_plans_for_sentry_customer_team_plan_non_trial( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_plan_non_trial(self, is_sentry_user): @@ -475,14 +528,22 @@ def test_available_plans_for_sentry_plan_non_trial(self, is_sentry_user): plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @freeze_time("2023-06-19") @@ -496,6 +557,11 @@ class AvailablePlansExpiredTrialLessThanTenUsers(TestCase): - sentry customer, users-sentrym/y, has trialed, less than 10 users -> users-pr-inappm/y, users-sentrym/y, users-basic, users-teamm/y """ + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def setUp(self): self.current_org = OwnerFactory( trial_start_date=datetime.utcnow() + timedelta(days=-10), @@ -513,13 +579,20 @@ def test_available_plans_for_basic_plan_expired_trial_less_than_10_users( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) def test_available_plans_for_team_plan_expired_trial_less_than_10_users( self, @@ -529,13 +602,20 @@ def test_available_plans_for_team_plan_expired_trial_less_than_10_users( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) def test_available_plans_for_pro_plan_expired_trial_less_than_10_users(self): self.current_org.plan = PlanName.CODECOV_PRO_MONTHLY.value @@ -543,13 +623,20 @@ def test_available_plans_for_pro_plan_expired_trial_less_than_10_users(self): plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_customer_basic_plan_expired_trial_less_than_10_users( @@ -561,14 +648,22 @@ def test_available_plans_for_sentry_customer_basic_plan_expired_trial_less_than_ plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_customer_team_plan_expired_trial_less_than_10_users( @@ -580,14 +675,22 @@ def test_available_plans_for_sentry_customer_team_plan_expired_trial_less_than_1 plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_plan_expired_trial_less_than_10_users( @@ -599,14 +702,22 @@ def test_available_plans_for_sentry_plan_expired_trial_less_than_10_users( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @freeze_time("2023-06-19") @@ -617,6 +728,11 @@ class AvailablePlansExpiredTrialMoreThanTenActivatedUsers(TestCase): - sentry customer, users-sentrym/y, has trialed, more than 10 activated users -> users-pr-inappm/y, users-sentrym/y, users-basic """ + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def setUp(self): self.current_org = OwnerFactory( trial_start_date=datetime.utcnow() + timedelta(days=-10), @@ -633,12 +749,18 @@ def test_available_plans_for_pro_plan_expired_trial_more_than_10_users(self): plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_customer_basic_plan_expired_trial_more_than_10_users( @@ -650,13 +772,20 @@ def test_available_plans_for_sentry_customer_basic_plan_expired_trial_more_than_ plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_available_plans_for_sentry_plan_expired_trial_more_than_10_users( @@ -668,13 +797,20 @@ def test_available_plans_for_sentry_plan_expired_trial_more_than_10_users( plan_service = PlanService(current_org=self.current_org) - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } - assert plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] for plan in plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @freeze_time("2023-06-19") @@ -683,14 +819,19 @@ class AvailablePlansExpiredTrialMoreThanTenSeatsLessThanTenActivatedUsers(TestCa Tests that what matters for Team plan is activated users not the total seat count """ + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def setUp(self): - self.expected_result = [] - self.expected_result.append(BASIC_PLAN) - self.expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - self.expected_result += TEAM_PLAN_REPRESENTATIONS.values() - self.expected_result = [ - result.convert_to_DTO() for result in self.expected_result - ] + self.expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } def test_currently_team_plan(self): self.current_org = OwnerFactory( @@ -702,7 +843,11 @@ def test_currently_team_plan(self): self.plan_service = PlanService(current_org=self.current_org) assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result ) def test_trial_expired(self): @@ -717,7 +862,11 @@ def test_trial_expired(self): self.plan_service = PlanService(current_org=self.current_org) assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result ) def test_trial_ongoing(self): @@ -732,7 +881,11 @@ def test_trial_ongoing(self): self.plan_service = PlanService(current_org=self.current_org) assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result ) def test_trial_not_started(self): @@ -744,16 +897,20 @@ def test_trial_not_started(self): self.owner = OwnerFactory() self.plan_service = PlanService(current_org=self.current_org) - self.expected_result = [] - self.expected_result.append(BASIC_PLAN) - self.expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - self.expected_result += TEAM_PLAN_REPRESENTATIONS.values() - self.expected_result = [ - result.convert_to_DTO() for result in self.expected_result - ] + self.expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } assert ( - self.plan_service.available_plans(owner=self.owner) == self.expected_result + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == self.expected_result ) @@ -768,9 +925,14 @@ class AvailablePlansOngoingTrial(TestCase): when > 10 activated seats -> users-pr-inappm/y, users-sentrym/y, users-basic """ + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def setUp(self): self.current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, + plan=PlanName.BASIC_PLAN_NAME.value, trial_start_date=datetime.utcnow(), trial_end_date=datetime.utcnow() + timedelta(days=14), trial_status=TrialStatus.ONGOING.value, @@ -782,73 +944,125 @@ def setUp(self): def test_non_sentry_user(self): # [Basic, Pro Monthly, Pro Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } # Can do Team plan when plan_activated_users is null - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) self.current_org.plan_activated_users = [i for i in range(10)] self.current_org.save() # Can do Team plan when at 10 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) self.current_org.plan_activated_users = [i for i in range(11)] self.current_org.save() - # [Basic, Pro Monthly, Pro Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + # [Basic, Pro Monthly, Pro Yearly] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + } # Can not do Team plan when at 11 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @patch("shared.plan.service.is_sentry_user") def test_sentry_user(self, is_sentry_user): + self.current_org.plan = PlanName.SENTRY_MONTHLY.value + self.current_org.save() + is_sentry_user.return_value = True # [Basic, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly, Team Monthly, Team Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += TEAM_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] - + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + PlanName.TEAM_MONTHLY.value, + PlanName.TEAM_YEARLY.value, + } # Can do Team plan when plan_activated_users is null - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) self.current_org.plan_activated_users = [i for i in range(10)] self.current_org.save() # Can do Team plan when at 10 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) self.current_org.plan_activated_users = [i for i in range(11)] self.current_org.save() # [Basic, Pro Monthly, Pro Yearly, Sentry Monthly, Sentry Yearly] - expected_result = [] - expected_result.append(BASIC_PLAN) - expected_result += PR_AUTHOR_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result += SENTRY_PAID_USER_PLAN_REPRESENTATIONS.values() - expected_result = [result.convert_to_DTO() for result in expected_result] + expected_result = { + PlanName.BASIC_PLAN_NAME.value, + PlanName.CODECOV_PRO_YEARLY.value, + PlanName.CODECOV_PRO_MONTHLY.value, + PlanName.SENTRY_MONTHLY.value, + PlanName.SENTRY_YEARLY.value, + } # Can not do Team plan when at 11 activated users - assert self.plan_service.available_plans(owner=self.owner) == expected_result + assert ( + set( + plan["value"] + for plan in self.plan_service.available_plans(owner=self.owner) + ) + == expected_result + ) @override_settings(IS_ENTERPRISE=False) class PlanServiceIs___PlanTests(TestCase): def test_is_trial_plan(self): + tier = TierFactory(tier_name=TierName.TRIAL.value) + plan = PlanFactory( + tier=tier, + name=PlanName.TRIAL_PLAN_NAME.value, + paid_plan=False, + ) self.current_org = OwnerFactory( - plan=PlanName.TRIAL_PLAN_NAME.value, + plan=plan.name, trial_start_date=datetime.utcnow(), trial_end_date=datetime.utcnow() + timedelta(days=14), trial_status=TrialStatus.ONGOING.value, @@ -867,8 +1081,14 @@ def test_is_trial_plan(self): assert self.plan_service.is_pr_billing_plan == True def test_is_team_plan(self): + tier = TierFactory(tier_name=TierName.TEAM.value) + plan = PlanFactory( + tier=tier, + name=PlanName.TEAM_MONTHLY.value, + paid_plan=True, + ) self.current_org = OwnerFactory( - plan=PlanName.TEAM_MONTHLY.value, + plan=plan.name, trial_status=TrialStatus.EXPIRED.value, ) self.owner = OwnerFactory() @@ -883,8 +1103,14 @@ def test_is_team_plan(self): assert self.plan_service.is_pr_billing_plan == True def test_is_sentry_plan(self): + tier = TierFactory(tier_name=TierName.SENTRY.value) + plan = PlanFactory( + tier=tier, + name=PlanName.SENTRY_MONTHLY.value, + paid_plan=True, + ) self.current_org = OwnerFactory( - plan=PlanName.SENTRY_MONTHLY.value, + plan=plan.name, trial_status=TrialStatus.EXPIRED.value, ) self.owner = OwnerFactory() @@ -899,8 +1125,14 @@ def test_is_sentry_plan(self): assert self.plan_service.is_pr_billing_plan == True def test_is_free_plan(self): + tier = TierFactory(tier_name=TierName.BASIC.value) + plan = PlanFactory( + tier=tier, + name=PlanName.FREE_PLAN_NAME.value, + paid_plan=False, + ) self.current_org = OwnerFactory( - plan=PlanName.FREE_PLAN_NAME.value, + plan=plan.name, ) self.owner = OwnerFactory() self.plan_service = PlanService(current_org=self.current_org) @@ -914,8 +1146,15 @@ def test_is_free_plan(self): assert self.plan_service.is_pr_billing_plan == True def test_is_pro_plan(self): + tier = TierFactory(tier_name=TierName.PRO.value) + plan = PlanFactory( + tier=tier, + name=PlanName.CODECOV_PRO_MONTHLY.value, + paid_plan=True, + ) + self.current_org = OwnerFactory( - plan=PlanName.CODECOV_PRO_MONTHLY.value, + plan=plan.name, ) self.owner = OwnerFactory() self.plan_service = PlanService(current_org=self.current_org) @@ -929,8 +1168,14 @@ def test_is_pro_plan(self): assert self.plan_service.is_pr_billing_plan == True def test_is_enterprise_plan(self): + tier = TierFactory(tier_name=TierName.ENTERPRISE.value) + plan = PlanFactory( + tier=tier, + name=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + paid_plan=True, + ) self.current_org = OwnerFactory( - plan=PlanName.ENTERPRISE_CLOUD_YEARLY.value, + plan=plan.name, ) self.owner = OwnerFactory() self.plan_service = PlanService(current_org=self.current_org) diff --git a/tests/unit/upload/test_utils.py b/tests/unit/upload/test_utils.py index 0dcffd79e..b5f97ccb4 100644 --- a/tests/unit/upload/test_utils.py +++ b/tests/unit/upload/test_utils.py @@ -18,9 +18,15 @@ insert_coverage_measurement, query_monthly_coverage_measurements, ) +from tests.helper import mock_all_plans_and_tiers class CoverageMeasurement(TestCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + mock_all_plans_and_tiers() + def add_upload_measurements_records( self, owner: Owner,