feat: add admin client policy control
This commit is contained in:
@@ -7,6 +7,8 @@ from django.template.response import TemplateResponse
|
||||
from django.urls import path, reverse
|
||||
|
||||
from .models import (
|
||||
ClientSubscriptionPolicy,
|
||||
ClientSubscriptionPolicyAudit,
|
||||
ClientDevice,
|
||||
DeviceCredential,
|
||||
DeviceBindingAudit,
|
||||
@@ -23,8 +25,10 @@ from .services import (
|
||||
LicensingError,
|
||||
grant_software_entitlement,
|
||||
create_legacy_migration_grant,
|
||||
get_subscription_mode,
|
||||
renew_software_entitlement,
|
||||
revoke_software_entitlement,
|
||||
set_client_subscription_policy,
|
||||
)
|
||||
|
||||
|
||||
@@ -35,6 +39,95 @@ class HiddenFromAdminIndexMixin:
|
||||
return {}
|
||||
|
||||
|
||||
class ClientSubscriptionPolicyAdminForm(forms.ModelForm):
|
||||
reason = forms.CharField(label="变更原因", widget=forms.Textarea(attrs={"rows": 3}))
|
||||
|
||||
class Meta:
|
||||
model = ClientSubscriptionPolicy
|
||||
fields = ("mode",)
|
||||
|
||||
def clean_mode(self):
|
||||
mode = self.cleaned_data["mode"]
|
||||
if not self.instance._state.adding and self.instance.mode == mode:
|
||||
raise forms.ValidationError("请选择不同的客户端订阅策略。")
|
||||
return mode
|
||||
|
||||
def clean_reason(self):
|
||||
reason = self.cleaned_data["reason"].strip()
|
||||
if not reason:
|
||||
raise forms.ValidationError("必须填写操作原因。")
|
||||
return reason
|
||||
|
||||
|
||||
class ClientSubscriptionPolicyAuditInline(admin.TabularInline):
|
||||
model = ClientSubscriptionPolicyAudit
|
||||
extra = 0
|
||||
can_delete = False
|
||||
max_num = 0
|
||||
ordering = ("-changed_at", "-id")
|
||||
fields = ("previous_mode", "mode", "reason", "changed_by", "changed_at")
|
||||
readonly_fields = fields
|
||||
verbose_name = "策略变更审计"
|
||||
verbose_name_plural = "策略变更审计"
|
||||
|
||||
def has_add_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(ClientSubscriptionPolicy)
|
||||
class ClientSubscriptionPolicyAdmin(admin.ModelAdmin):
|
||||
form = ClientSubscriptionPolicyAdminForm
|
||||
inlines = (ClientSubscriptionPolicyAuditInline,)
|
||||
fields = (
|
||||
"mode",
|
||||
"reason",
|
||||
"server_subscription_mode",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
readonly_fields = (
|
||||
"server_subscription_mode",
|
||||
"updated_by",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
)
|
||||
|
||||
def has_module_permission(self, request):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_view_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return request.user.is_superuser and not ClientSubscriptionPolicy.objects.exists()
|
||||
|
||||
def has_change_permission(self, request, obj=None):
|
||||
return request.user.is_superuser
|
||||
|
||||
def has_delete_permission(self, request, obj=None):
|
||||
return False
|
||||
|
||||
@admin.display(description="服务端订阅授权模式")
|
||||
def server_subscription_mode(self, obj):
|
||||
return get_subscription_mode()
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
policy = set_client_subscription_policy(
|
||||
mode=form.cleaned_data["mode"],
|
||||
reason=form.cleaned_data["reason"],
|
||||
actor=request.user,
|
||||
)
|
||||
obj.singleton_id = policy.singleton_id
|
||||
obj.mode = policy.mode
|
||||
obj.updated_by = policy.updated_by
|
||||
obj.created_at = policy.created_at
|
||||
obj.updated_at = policy.updated_at
|
||||
|
||||
|
||||
def _masked_fingerprint(value: str) -> str:
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# Generated by Django 5.2.15 on 2026-07-28 07:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('licensing', '0005_alter_softwareentitlement_options_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ClientSubscriptionPolicy',
|
||||
fields=[
|
||||
('singleton_id', models.PositiveSmallIntegerField(default=1, editable=False, primary_key=True, serialize=False, verbose_name='固定配置标识')),
|
||||
('mode', models.CharField(choices=[('off', '关闭'), ('observe', '仅检测'), ('enforce', '检测并门禁')], max_length=16, verbose_name='客户端策略')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('updated_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='client_subscription_policy_updates', to=settings.AUTH_USER_MODEL, verbose_name='最后操作人')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '客户端订阅策略',
|
||||
'verbose_name_plural': '客户端订阅策略',
|
||||
'db_table': 'client_subscription_policy',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ClientSubscriptionPolicyAudit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('previous_mode', models.CharField(blank=True, max_length=16, verbose_name='变更前策略')),
|
||||
('mode', models.CharField(choices=[('off', '关闭'), ('observe', '仅检测'), ('enforce', '检测并门禁')], max_length=16, verbose_name='变更后策略')),
|
||||
('reason', models.CharField(max_length=255, verbose_name='变更原因')),
|
||||
('changed_at', models.DateTimeField(auto_now_add=True, verbose_name='变更时间')),
|
||||
('changed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='client_subscription_policy_audits', to=settings.AUTH_USER_MODEL, verbose_name='操作人')),
|
||||
('policy', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='audits', to='licensing.clientsubscriptionpolicy', verbose_name='客户端订阅策略')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '客户端订阅策略审计',
|
||||
'verbose_name_plural': '客户端订阅策略审计',
|
||||
'db_table': 'client_subscription_policy_audit',
|
||||
'ordering': ('-changed_at', '-id'),
|
||||
},
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='clientsubscriptionpolicy',
|
||||
constraint=models.CheckConstraint(condition=models.Q(('singleton_id', 1)), name='client_subscription_policy_singleton_only'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='clientsubscriptionpolicyaudit',
|
||||
index=models.Index(fields=['policy', 'changed_at'], name='client_subs_policy__05d186_idx'),
|
||||
),
|
||||
]
|
||||
@@ -12,6 +12,84 @@ from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
|
||||
|
||||
class ClientSubscriptionPolicy(models.Model):
|
||||
"""The one global policy published to supported cmshopee clients."""
|
||||
|
||||
class Mode(models.TextChoices):
|
||||
OFF = "off", "关闭"
|
||||
OBSERVE = "observe", "仅检测"
|
||||
ENFORCE = "enforce", "检测并门禁"
|
||||
|
||||
singleton_id = models.PositiveSmallIntegerField(
|
||||
"固定配置标识",
|
||||
primary_key=True,
|
||||
default=1,
|
||||
editable=False,
|
||||
)
|
||||
mode = models.CharField("客户端策略", max_length=16, choices=Mode.choices)
|
||||
updated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="最后操作人",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="client_subscription_policy_updates",
|
||||
)
|
||||
created_at = models.DateTimeField("创建时间", auto_now_add=True)
|
||||
updated_at = models.DateTimeField("更新时间", auto_now=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_subscription_policy"
|
||||
verbose_name = "客户端订阅策略"
|
||||
verbose_name_plural = "客户端订阅策略"
|
||||
constraints = [
|
||||
models.CheckConstraint(
|
||||
condition=Q(singleton_id=1),
|
||||
name="client_subscription_policy_singleton_only",
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"客户端订阅策略:{self.get_mode_display()}"
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.singleton_id = 1
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class ClientSubscriptionPolicyAudit(models.Model):
|
||||
policy = models.ForeignKey(
|
||||
ClientSubscriptionPolicy,
|
||||
verbose_name="客户端订阅策略",
|
||||
on_delete=models.PROTECT,
|
||||
related_name="audits",
|
||||
)
|
||||
previous_mode = models.CharField("变更前策略", max_length=16, blank=True)
|
||||
mode = models.CharField("变更后策略", max_length=16, choices=ClientSubscriptionPolicy.Mode.choices)
|
||||
reason = models.CharField("变更原因", max_length=255)
|
||||
changed_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
verbose_name="操作人",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="client_subscription_policy_audits",
|
||||
)
|
||||
changed_at = models.DateTimeField("变更时间", auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
db_table = "client_subscription_policy_audit"
|
||||
verbose_name = "客户端订阅策略审计"
|
||||
verbose_name_plural = "客户端订阅策略审计"
|
||||
ordering = ("-changed_at", "-id")
|
||||
indexes = [
|
||||
models.Index(fields=("policy", "changed_at")),
|
||||
]
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.policy} {self.previous_mode or '未配置'} -> {self.mode}"
|
||||
|
||||
|
||||
class ClientDevice(models.Model):
|
||||
class ProductCode(models.TextChoices):
|
||||
CMSHOPEE = "cmshopee", "虾皮圈优化助手"
|
||||
|
||||
@@ -11,7 +11,13 @@ from django.contrib.auth import get_user_model
|
||||
from django.db import IntegrityError, transaction
|
||||
from django.utils import timezone
|
||||
|
||||
from config.client_subscription_policy import (
|
||||
build_client_subscription_policy,
|
||||
get_client_subscription_policy,
|
||||
)
|
||||
from apps.licensing.models import (
|
||||
ClientSubscriptionPolicy,
|
||||
ClientSubscriptionPolicyAudit,
|
||||
ClientDevice,
|
||||
DeviceCredential,
|
||||
DeviceBindingAudit,
|
||||
@@ -289,6 +295,57 @@ def _required_reason(reason: str) -> str:
|
||||
return normalized_reason
|
||||
|
||||
|
||||
def get_published_client_subscription_policy() -> dict:
|
||||
"""Return the admin override when present, otherwise the T-635 env fallback."""
|
||||
|
||||
policy = ClientSubscriptionPolicy.objects.filter(pk=1).only("mode", "updated_at").first()
|
||||
if policy is None:
|
||||
return get_client_subscription_policy()
|
||||
return build_client_subscription_policy(mode=policy.mode, updated_at=policy.updated_at)
|
||||
|
||||
|
||||
@transaction.atomic
|
||||
def set_client_subscription_policy(*, mode: str, reason: str, actor) -> ClientSubscriptionPolicy:
|
||||
"""Persist one audited policy transition without allowing a second singleton."""
|
||||
|
||||
normalized_mode = str(mode or "").strip().lower()
|
||||
if normalized_mode not in ClientSubscriptionPolicy.Mode.values:
|
||||
raise LicensingError("bad_request", "客户端订阅策略不合法")
|
||||
normalized_reason = _required_reason(reason)
|
||||
|
||||
policy = ClientSubscriptionPolicy.objects.select_for_update().filter(pk=1).first()
|
||||
created = policy is None
|
||||
if policy is None:
|
||||
try:
|
||||
with transaction.atomic():
|
||||
policy = ClientSubscriptionPolicy.objects.create(
|
||||
singleton_id=1,
|
||||
mode=normalized_mode,
|
||||
updated_by=actor,
|
||||
)
|
||||
except IntegrityError:
|
||||
policy = ClientSubscriptionPolicy.objects.select_for_update().get(pk=1)
|
||||
created = False
|
||||
|
||||
if not created and policy.mode == normalized_mode:
|
||||
raise LicensingError("policy_unchanged", "请选择不同的客户端订阅策略")
|
||||
|
||||
previous_mode = "" if created else policy.mode
|
||||
if not created:
|
||||
policy.mode = normalized_mode
|
||||
policy.updated_by = actor
|
||||
policy.save(update_fields=("mode", "updated_by", "updated_at"))
|
||||
|
||||
ClientSubscriptionPolicyAudit.objects.create(
|
||||
policy=policy,
|
||||
previous_mode=previous_mode,
|
||||
mode=normalized_mode,
|
||||
reason=normalized_reason,
|
||||
changed_by=actor,
|
||||
)
|
||||
return policy
|
||||
|
||||
|
||||
def _create_license_event(
|
||||
*,
|
||||
entitlement: SoftwareEntitlement,
|
||||
|
||||
+189
-1
@@ -8,7 +8,7 @@ from unittest.mock import patch
|
||||
from django.contrib import admin
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import CommandError
|
||||
from django.db import close_old_connections
|
||||
from django.db import IntegrityError, close_old_connections
|
||||
from django.test import SimpleTestCase, TestCase, TransactionTestCase, override_settings
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
@@ -17,6 +17,8 @@ from rest_framework.test import APIClient
|
||||
from apps.billing.models import PointsLedger
|
||||
from apps.billing.payment_gateways import PaymentOrderCode, PaymentReceipt
|
||||
from apps.licensing.models import (
|
||||
ClientSubscriptionPolicy,
|
||||
ClientSubscriptionPolicyAudit,
|
||||
ClientDevice,
|
||||
DeviceBindingAudit,
|
||||
DeviceCredential,
|
||||
@@ -44,6 +46,7 @@ from apps.licensing.services import (
|
||||
evaluate_device_authorization,
|
||||
evaluate_subscription_access,
|
||||
get_subscription_mode,
|
||||
get_published_client_subscription_policy,
|
||||
grant_plan_to_existing_users,
|
||||
grant_software_entitlement,
|
||||
record_device_heartbeat,
|
||||
@@ -52,6 +55,7 @@ from apps.licensing.services import (
|
||||
revoke_software_entitlement,
|
||||
register_device,
|
||||
software_subscription_status,
|
||||
set_client_subscription_policy,
|
||||
)
|
||||
from apps.users.models import ApiKey, User, UserWallet
|
||||
|
||||
@@ -495,6 +499,114 @@ class DeviceRegistrationApiTests(TestCase):
|
||||
self.assertGreater(device.last_seen_at, stale_time)
|
||||
|
||||
|
||||
class ClientSubscriptionPolicyServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
self.operator = User.objects.create_user(
|
||||
username="client-policy-operator",
|
||||
email="client-policy-operator@example.com",
|
||||
password="test-password",
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY="enforce",
|
||||
CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY_UPDATED_AT="2026-07-28T10:00:00+08:00",
|
||||
)
|
||||
def test_missing_admin_policy_falls_back_to_environment_configuration(self):
|
||||
self.assertEqual(
|
||||
get_published_client_subscription_policy(),
|
||||
{
|
||||
"policy_version": 1,
|
||||
"subscription_check_enabled": True,
|
||||
"subscription_enforcement_enabled": True,
|
||||
"updated_at": "2026-07-28T10:00:00+08:00",
|
||||
},
|
||||
)
|
||||
|
||||
@override_settings(
|
||||
CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY="off",
|
||||
CMSHOPEE_CLIENT_SUBSCRIPTION_POLICY_UPDATED_AT="2026-07-28T00:00:00+08:00",
|
||||
)
|
||||
def test_policy_transition_writes_audit_and_immediately_overrides_environment(self):
|
||||
policy = set_client_subscription_policy(
|
||||
mode=ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
reason="新版客户端灰度检测",
|
||||
actor=self.operator,
|
||||
)
|
||||
|
||||
self.assertEqual(policy.pk, 1)
|
||||
self.assertEqual(policy.mode, ClientSubscriptionPolicy.Mode.OBSERVE)
|
||||
self.assertEqual(
|
||||
get_published_client_subscription_policy(),
|
||||
{
|
||||
"policy_version": 1,
|
||||
"subscription_check_enabled": True,
|
||||
"subscription_enforcement_enabled": False,
|
||||
"updated_at": policy.updated_at.isoformat(),
|
||||
},
|
||||
)
|
||||
first_audit = ClientSubscriptionPolicyAudit.objects.get(policy=policy)
|
||||
self.assertEqual(first_audit.previous_mode, "")
|
||||
self.assertEqual(first_audit.mode, ClientSubscriptionPolicy.Mode.OBSERVE)
|
||||
self.assertEqual(first_audit.reason, "新版客户端灰度检测")
|
||||
self.assertEqual(first_audit.changed_by, self.operator)
|
||||
|
||||
updated = set_client_subscription_policy(
|
||||
mode=ClientSubscriptionPolicy.Mode.ENFORCE,
|
||||
reason="灰度验收完成",
|
||||
actor=self.operator,
|
||||
)
|
||||
|
||||
self.assertEqual(updated.pk, policy.pk)
|
||||
self.assertEqual(updated.mode, ClientSubscriptionPolicy.Mode.ENFORCE)
|
||||
self.assertEqual(ClientSubscriptionPolicy.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
ClientSubscriptionPolicyAudit.objects.filter(policy=policy).count(),
|
||||
2,
|
||||
)
|
||||
latest_audit = ClientSubscriptionPolicyAudit.objects.latest("id")
|
||||
self.assertEqual(latest_audit.previous_mode, ClientSubscriptionPolicy.Mode.OBSERVE)
|
||||
self.assertEqual(latest_audit.mode, ClientSubscriptionPolicy.Mode.ENFORCE)
|
||||
|
||||
def test_policy_transition_requires_reason_and_rejects_same_mode(self):
|
||||
with self.assertRaises(LicensingError) as missing_reason:
|
||||
set_client_subscription_policy(
|
||||
mode=ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
reason="",
|
||||
actor=self.operator,
|
||||
)
|
||||
self.assertEqual(missing_reason.exception.code, "reason_required")
|
||||
self.assertFalse(ClientSubscriptionPolicy.objects.exists())
|
||||
|
||||
set_client_subscription_policy(
|
||||
mode=ClientSubscriptionPolicy.Mode.OFF,
|
||||
reason="创建默认策略",
|
||||
actor=self.operator,
|
||||
)
|
||||
with self.assertRaises(LicensingError) as unchanged:
|
||||
set_client_subscription_policy(
|
||||
mode=ClientSubscriptionPolicy.Mode.OFF,
|
||||
reason="不应产生伪审计",
|
||||
actor=self.operator,
|
||||
)
|
||||
self.assertEqual(unchanged.exception.code, "policy_unchanged")
|
||||
self.assertEqual(ClientSubscriptionPolicyAudit.objects.count(), 1)
|
||||
|
||||
def test_database_singleton_rejects_a_second_primary_key(self):
|
||||
ClientSubscriptionPolicy.objects.create(mode=ClientSubscriptionPolicy.Mode.OFF)
|
||||
|
||||
with self.assertRaises(IntegrityError):
|
||||
ClientSubscriptionPolicy.objects.bulk_create(
|
||||
[
|
||||
ClientSubscriptionPolicy(
|
||||
singleton_id=2,
|
||||
mode=ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SoftwareEntitlementServiceTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
@@ -973,6 +1085,7 @@ class SoftwareEntitlementAdminTests(TestCase):
|
||||
visible_models = (
|
||||
SoftwarePlan,
|
||||
SoftwareEntitlement,
|
||||
ClientSubscriptionPolicy,
|
||||
)
|
||||
request = SimpleNamespace(user=self.operator)
|
||||
|
||||
@@ -991,6 +1104,81 @@ class SoftwareEntitlementAdminTests(TestCase):
|
||||
)
|
||||
self.assertEqual(SoftwarePlan._meta.verbose_name, "会员套餐")
|
||||
self.assertEqual(SoftwareEntitlement._meta.verbose_name, "用户会员")
|
||||
self.assertNotIn(ClientSubscriptionPolicyAudit, admin.site._registry)
|
||||
|
||||
def test_client_subscription_policy_admin_requires_superuser_and_writes_audit(self):
|
||||
staff = User.objects.create_user(
|
||||
username="licensing-policy-staff",
|
||||
email="licensing-policy-staff@example.com",
|
||||
password="test-password",
|
||||
is_staff=True,
|
||||
)
|
||||
add_url = reverse("admin:licensing_clientsubscriptionpolicy_add")
|
||||
|
||||
self.client.force_login(staff)
|
||||
self.assertEqual(self.client.get(add_url).status_code, 403)
|
||||
|
||||
self.client.force_login(self.operator)
|
||||
audit_management = {
|
||||
"audits-TOTAL_FORMS": "0",
|
||||
"audits-INITIAL_FORMS": "0",
|
||||
"audits-MIN_NUM_FORMS": "0",
|
||||
"audits-MAX_NUM_FORMS": "0",
|
||||
}
|
||||
missing_reason = self.client.post(
|
||||
add_url,
|
||||
{
|
||||
"mode": ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
"reason": "",
|
||||
**audit_management,
|
||||
},
|
||||
)
|
||||
self.assertEqual(missing_reason.status_code, 200)
|
||||
self.assertFalse(ClientSubscriptionPolicy.objects.exists())
|
||||
|
||||
created = self.client.post(
|
||||
add_url,
|
||||
{
|
||||
"mode": ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
"reason": "客户端灰度",
|
||||
**audit_management,
|
||||
},
|
||||
)
|
||||
self.assertEqual(created.status_code, 302)
|
||||
policy = ClientSubscriptionPolicy.objects.get(pk=1)
|
||||
self.assertEqual(policy.mode, ClientSubscriptionPolicy.Mode.OBSERVE)
|
||||
self.assertTrue(
|
||||
ClientSubscriptionPolicyAudit.objects.filter(
|
||||
policy=policy,
|
||||
reason="客户端灰度",
|
||||
changed_by=self.operator,
|
||||
).exists()
|
||||
)
|
||||
|
||||
self.client.force_login(staff)
|
||||
change_url = reverse(
|
||||
"admin:licensing_clientsubscriptionpolicy_change",
|
||||
args=(policy.pk,),
|
||||
)
|
||||
self.assertEqual(self.client.get(change_url).status_code, 403)
|
||||
|
||||
self.client.force_login(self.operator)
|
||||
same_mode = self.client.post(
|
||||
change_url,
|
||||
{
|
||||
"mode": ClientSubscriptionPolicy.Mode.OBSERVE,
|
||||
"reason": "不应写入",
|
||||
"audits-TOTAL_FORMS": "1",
|
||||
"audits-INITIAL_FORMS": "1",
|
||||
"audits-MIN_NUM_FORMS": "0",
|
||||
"audits-MAX_NUM_FORMS": "0",
|
||||
"audits-0-id": ClientSubscriptionPolicyAudit.objects.get(policy=policy).pk,
|
||||
"audits-0-policy": policy.pk,
|
||||
},
|
||||
)
|
||||
self.assertEqual(same_mode.status_code, 200)
|
||||
self.assertEqual(ClientSubscriptionPolicyAudit.objects.count(), 1)
|
||||
self.assertEqual(self.client.get(add_url).status_code, 403)
|
||||
|
||||
|
||||
class LicenseSeatConcurrencyTests(TransactionTestCase):
|
||||
|
||||
Reference in New Issue
Block a user