feat: add staged subscription rollout mode
This commit is contained in:
+142
-9
@@ -2,10 +2,11 @@ from datetime import timedelta
|
||||
from decimal import Decimal
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from django.contrib import admin
|
||||
from django.db import close_old_connections
|
||||
from django.test import TestCase, TransactionTestCase, override_settings
|
||||
from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
from rest_framework.test import APIClient
|
||||
@@ -29,6 +30,7 @@ from apps.licensing.services import (
|
||||
LicensingError,
|
||||
SoftwareOrderAmountMismatchError,
|
||||
SoftwareOrderTransactionMismatchError,
|
||||
SubscriptionAuthorizationDecision,
|
||||
apply_software_payment,
|
||||
assign_license_seat,
|
||||
confirm_migration_request,
|
||||
@@ -36,16 +38,139 @@ from apps.licensing.services import (
|
||||
create_migration_request,
|
||||
create_software_order,
|
||||
evaluate_device_authorization,
|
||||
evaluate_subscription_access,
|
||||
get_subscription_mode,
|
||||
grant_software_entitlement,
|
||||
record_device_heartbeat,
|
||||
release_license_seat,
|
||||
renew_software_entitlement,
|
||||
revoke_software_entitlement,
|
||||
register_device,
|
||||
software_subscription_status,
|
||||
)
|
||||
from apps.users.models import ApiKey, User, UserWallet
|
||||
|
||||
|
||||
class SubscriptionModeUnitTests(SimpleTestCase):
|
||||
@override_settings(
|
||||
CMSHOPEE_SUBSCRIPTION_MODE="",
|
||||
CMSHOPEE_SUBSCRIPTION_ENFORCEMENT=True,
|
||||
)
|
||||
def test_legacy_enforcement_setting_maps_to_enforce(self):
|
||||
self.assertEqual(get_subscription_mode(), "enforce")
|
||||
|
||||
@override_settings(CMSHOPEE_SUBSCRIPTION_MODE="shadow")
|
||||
@patch("apps.licensing.services.evaluate_account_authorization")
|
||||
def test_shadow_access_keeps_real_rejection_for_observation(self, evaluate_mock):
|
||||
evaluate_mock.return_value = SubscriptionAuthorizationDecision(
|
||||
product_code="cmshopee",
|
||||
allowed=False,
|
||||
code="subscription_required",
|
||||
)
|
||||
|
||||
access = evaluate_subscription_access(
|
||||
user=SimpleNamespace(),
|
||||
product_code="cmshopee",
|
||||
)
|
||||
|
||||
self.assertTrue(access.allowed)
|
||||
self.assertEqual(access.access_source, "shadow_fallback")
|
||||
self.assertEqual(access.entitlement_status, "required")
|
||||
self.assertEqual(access.entitlement_code, "subscription_required")
|
||||
|
||||
@patch("apps.licensing.services.evaluate_account_authorization")
|
||||
def test_open_status_has_client_contract_without_database_entitlement(self, evaluate_mock):
|
||||
evaluate_mock.return_value = SubscriptionAuthorizationDecision(
|
||||
product_code="cmshopee",
|
||||
allowed=False,
|
||||
code="subscription_required",
|
||||
)
|
||||
user = SimpleNamespace(
|
||||
get_username=lambda: "desktop-user",
|
||||
get_full_name=lambda: "",
|
||||
)
|
||||
|
||||
data = software_subscription_status(
|
||||
user=user,
|
||||
product_code="cmshopee",
|
||||
mode="open",
|
||||
)
|
||||
|
||||
self.assertEqual(data["status"], "active")
|
||||
self.assertTrue(data["allowed"])
|
||||
self.assertEqual(data["account"]["display_name"], "desktop-user")
|
||||
self.assertEqual(data["plan"]["code"], "development-open")
|
||||
self.assertEqual(data["expires_at"], data["plan"]["expires_at"])
|
||||
self.assertEqual(data["access_source"], "open_mode")
|
||||
self.assertEqual(data["entitlement_status"], "required")
|
||||
|
||||
def test_admin_index_only_exposes_membership_models_without_deleting_registration(self):
|
||||
hidden_models = (
|
||||
ClientDevice,
|
||||
DeviceSession,
|
||||
DeviceBindingAudit,
|
||||
LicenseSeat,
|
||||
LegacyMigrationGrant,
|
||||
MigrationRequest,
|
||||
DeviceCredential,
|
||||
SoftwareOrder,
|
||||
LicenseEvent,
|
||||
)
|
||||
user = SimpleNamespace(
|
||||
is_active=True,
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
has_perm=lambda *_args, **_kwargs: True,
|
||||
has_module_perms=lambda *_args, **_kwargs: True,
|
||||
)
|
||||
request = SimpleNamespace(user=user)
|
||||
|
||||
for model in hidden_models:
|
||||
self.assertIn(model, admin.site._registry)
|
||||
self.assertEqual(admin.site._registry[model].get_model_perms(request), {})
|
||||
self.assertTrue(admin.site._registry[SoftwarePlan].get_model_perms(request))
|
||||
self.assertTrue(admin.site._registry[SoftwareEntitlement].get_model_perms(request))
|
||||
self.assertEqual(SoftwarePlan._meta.verbose_name, "会员套餐")
|
||||
self.assertEqual(SoftwareEntitlement._meta.verbose_name, "用户会员")
|
||||
|
||||
@patch("apps.licensing.services.evaluate_account_authorization")
|
||||
def test_enforce_status_reports_real_grace_period_at_top_level(self, evaluate_mock):
|
||||
now = timezone.now()
|
||||
entitlement = SimpleNamespace(
|
||||
id=7,
|
||||
source_plan_id=3,
|
||||
plan_name="专业版",
|
||||
expires_at=now - timedelta(days=1),
|
||||
grace_expires_at=now + timedelta(days=2),
|
||||
)
|
||||
evaluate_mock.return_value = SubscriptionAuthorizationDecision(
|
||||
product_code="cmshopee",
|
||||
allowed=True,
|
||||
code="",
|
||||
entitlement=entitlement,
|
||||
)
|
||||
user = SimpleNamespace(
|
||||
get_username=lambda: "member-user",
|
||||
get_full_name=lambda: "会员用户",
|
||||
)
|
||||
|
||||
data = software_subscription_status(
|
||||
user=user,
|
||||
product_code="cmshopee",
|
||||
now=now,
|
||||
mode="enforce",
|
||||
)
|
||||
|
||||
self.assertEqual(data["status"], "grace")
|
||||
self.assertEqual(data["entitlement_status"], "grace")
|
||||
self.assertEqual(data["plan"]["code"], "plan-3")
|
||||
self.assertEqual(data["expires_at"], entitlement.expires_at.isoformat())
|
||||
self.assertEqual(
|
||||
data["grace_expires_at"],
|
||||
entitlement.grace_expires_at.isoformat(),
|
||||
)
|
||||
|
||||
|
||||
class DeviceRegistrationApiTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
@@ -550,22 +675,22 @@ class SoftwareEntitlementAdminTests(TestCase):
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_admin_hides_legacy_license_models_but_keeps_current_models_visible(self):
|
||||
def test_admin_index_only_exposes_membership_models(self):
|
||||
hidden_models = (
|
||||
ClientDevice,
|
||||
DeviceSession,
|
||||
DeviceBindingAudit,
|
||||
LicenseSeat,
|
||||
LegacyMigrationGrant,
|
||||
MigrationRequest,
|
||||
DeviceCredential,
|
||||
)
|
||||
visible_models = (
|
||||
ClientDevice,
|
||||
DeviceSession,
|
||||
DeviceBindingAudit,
|
||||
SoftwarePlan,
|
||||
SoftwareEntitlement,
|
||||
SoftwareOrder,
|
||||
LicenseEvent,
|
||||
)
|
||||
visible_models = (
|
||||
SoftwarePlan,
|
||||
SoftwareEntitlement,
|
||||
)
|
||||
request = SimpleNamespace(user=self.operator)
|
||||
|
||||
for model in hidden_models:
|
||||
@@ -576,6 +701,14 @@ class SoftwareEntitlementAdminTests(TestCase):
|
||||
self.assertTrue(LicenseSeat.objects.model._meta.db_table)
|
||||
self.assertTrue(DeviceCredential.objects.model._meta.db_table)
|
||||
|
||||
self.client.force_login(self.operator)
|
||||
self.assertEqual(
|
||||
self.client.get(reverse("admin:licensing_softwareorder_changelist")).status_code,
|
||||
200,
|
||||
)
|
||||
self.assertEqual(SoftwarePlan._meta.verbose_name, "会员套餐")
|
||||
self.assertEqual(SoftwareEntitlement._meta.verbose_name, "用户会员")
|
||||
|
||||
|
||||
class LicenseSeatConcurrencyTests(TransactionTestCase):
|
||||
def setUp(self):
|
||||
|
||||
Reference in New Issue
Block a user