feat: add legacy device migration flow
This commit is contained in:
@@ -11,20 +11,27 @@ from rest_framework.test import APIClient
|
||||
from apps.licensing.models import (
|
||||
ClientDevice,
|
||||
DeviceBindingAudit,
|
||||
DeviceCredential,
|
||||
DeviceSession,
|
||||
LegacyMigrationGrant,
|
||||
LicenseEvent,
|
||||
LicenseSeat,
|
||||
MigrationRequest,
|
||||
SoftwareEntitlement,
|
||||
SoftwarePlan,
|
||||
)
|
||||
from apps.licensing.services import (
|
||||
LicensingError,
|
||||
assign_license_seat,
|
||||
confirm_migration_request,
|
||||
create_legacy_migration_grant,
|
||||
create_migration_request,
|
||||
grant_software_entitlement,
|
||||
record_device_heartbeat,
|
||||
release_license_seat,
|
||||
renew_software_entitlement,
|
||||
revoke_software_entitlement,
|
||||
register_device,
|
||||
)
|
||||
from apps.users.models import ApiKey, User, UserWallet
|
||||
|
||||
@@ -446,3 +453,175 @@ class LicenseSeatConcurrencyTests(TransactionTestCase):
|
||||
self.assertEqual(sum(outcome[0] == "seat_limit_reached" for outcome in outcomes), 1)
|
||||
seat = LicenseSeat.objects.get(entitlement=self.entitlement)
|
||||
self.assertIn(seat.device_id, {self.first_device.pk, self.second_device.pk})
|
||||
|
||||
|
||||
class LegacyMigrationFlowTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user(
|
||||
username="migration-user",
|
||||
email="migration@example.com",
|
||||
password="test-password",
|
||||
)
|
||||
self.other_user = User.objects.create_user(
|
||||
username="migration-other",
|
||||
email="migration-other@example.com",
|
||||
password="test-password",
|
||||
)
|
||||
self.plan = SoftwarePlan.objects.create(
|
||||
product_code=ClientDevice.ProductCode.CMSHOPEE,
|
||||
name="存量迁移套餐",
|
||||
duration_days=30,
|
||||
price=Decimal("1.00"),
|
||||
device_limit=1,
|
||||
)
|
||||
self.api_key, self.raw_api_key = ApiKey.create_for_user(self.user, name="legacy")
|
||||
self.device, self.device_session_token = self.register_device_session()
|
||||
self.client = APIClient()
|
||||
|
||||
def register_device_session(self):
|
||||
result = register_device(
|
||||
user=self.user,
|
||||
api_key=self.api_key,
|
||||
product_code=ClientDevice.ProductCode.CMSHOPEE,
|
||||
device_id_version="v1",
|
||||
device_id="migration-device-id",
|
||||
public_key="migration-device-public-key",
|
||||
platform=ClientDevice.Platform.WINDOWS,
|
||||
client_version="0.2.0",
|
||||
)
|
||||
return result.device, result.session_token
|
||||
|
||||
def grant_migration(self):
|
||||
return create_legacy_migration_grant(
|
||||
user=self.user,
|
||||
plan=self.plan,
|
||||
reason="历史付费用户迁移",
|
||||
eligibility_snapshot={"legacy_customer_id": "legacy-001"},
|
||||
)
|
||||
|
||||
def api_headers(self):
|
||||
return {
|
||||
"HTTP_AUTHORIZATION": f"Bearer {self.raw_api_key}",
|
||||
"HTTP_X_DEVICE_SESSION": self.device_session_token,
|
||||
}
|
||||
|
||||
def test_request_confirm_poll_and_revoke_flow_keeps_credential_hashed(self):
|
||||
self.grant_migration()
|
||||
create_response = self.client.post(
|
||||
reverse("api-client-migration-request-create"),
|
||||
{},
|
||||
format="json",
|
||||
**self.api_headers(),
|
||||
)
|
||||
self.assertEqual(create_response.status_code, 201)
|
||||
raw_credential = create_response.data["device_credential_token"]
|
||||
self.assertTrue(create_response.data["confirmation_url"].endswith(create_response.data["request_id"]))
|
||||
migration_request = MigrationRequest.objects.get(
|
||||
request_id=create_response.data["request_id"]
|
||||
)
|
||||
self.assertNotEqual(migration_request.credential_token_hash, raw_credential)
|
||||
|
||||
pending = self.client.get(
|
||||
reverse(
|
||||
"api-client-migration-request-detail",
|
||||
args=(migration_request.request_id,),
|
||||
),
|
||||
**self.api_headers(),
|
||||
)
|
||||
self.assertEqual(pending.status_code, 200)
|
||||
self.assertEqual(pending.data["status"], MigrationRequest.Status.PENDING)
|
||||
self.assertNotIn("device_credential_token", pending.data)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
confirm_url = reverse("portal-migration-confirm", args=(migration_request.request_id,))
|
||||
self.assertEqual(self.client.get(confirm_url).status_code, 200)
|
||||
self.assertEqual(self.client.post(confirm_url).status_code, 302)
|
||||
self.assertEqual(self.client.post(confirm_url).status_code, 302)
|
||||
|
||||
credential = DeviceCredential.objects.get(migration_request=migration_request)
|
||||
self.assertNotEqual(credential.token_hash, raw_credential)
|
||||
self.assertEqual(credential.token_hash, MigrationRequest.hash_credential_token(raw_credential))
|
||||
self.assertEqual(credential.seat.device_id, self.device.id)
|
||||
self.assertEqual(DeviceCredential.objects.count(), 1)
|
||||
self.assertEqual(
|
||||
LicenseEvent.objects.filter(
|
||||
action=LicenseEvent.Action.CREDENTIAL_ISSUED,
|
||||
).count(),
|
||||
1,
|
||||
)
|
||||
|
||||
confirmed = self.client.get(
|
||||
reverse(
|
||||
"api-client-migration-request-detail",
|
||||
args=(migration_request.request_id,),
|
||||
),
|
||||
**self.api_headers(),
|
||||
)
|
||||
self.assertEqual(confirmed.status_code, 200)
|
||||
self.assertEqual(confirmed.data["status"], MigrationRequest.Status.CONFIRMED)
|
||||
|
||||
revoke = self.client.post(
|
||||
reverse("portal-device-credential-revoke", args=(credential.pk,))
|
||||
)
|
||||
self.assertEqual(revoke.status_code, 302)
|
||||
credential.refresh_from_db()
|
||||
credential.seat.refresh_from_db()
|
||||
self.assertIsNotNone(credential.revoked_at)
|
||||
self.assertIsNone(credential.seat.device_id)
|
||||
self.assertTrue(
|
||||
LicenseEvent.objects.filter(
|
||||
action=LicenseEvent.Action.CREDENTIAL_REVOKED,
|
||||
reason="用户自助解绑设备",
|
||||
).exists()
|
||||
)
|
||||
|
||||
def test_request_requires_eligible_current_device_and_web_confirmation_same_user(self):
|
||||
no_grant = self.client.post(
|
||||
reverse("api-client-migration-request-create"),
|
||||
{},
|
||||
format="json",
|
||||
**self.api_headers(),
|
||||
)
|
||||
self.assertEqual(no_grant.status_code, 403)
|
||||
self.assertEqual(no_grant.data["error"]["code"], "migration_not_eligible")
|
||||
|
||||
self.grant_migration()
|
||||
missing_device_session = self.client.post(
|
||||
reverse("api-client-migration-request-create"),
|
||||
{},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"Bearer {self.raw_api_key}",
|
||||
)
|
||||
self.assertEqual(missing_device_session.status_code, 401)
|
||||
self.assertEqual(missing_device_session.data["error"]["code"], "device_session_required")
|
||||
|
||||
created = self.client.post(
|
||||
reverse("api-client-migration-request-create"),
|
||||
{},
|
||||
format="json",
|
||||
**self.api_headers(),
|
||||
)
|
||||
migration_request = MigrationRequest.objects.get(request_id=created.data["request_id"])
|
||||
self.client.force_login(self.other_user)
|
||||
confirmation = self.client.get(
|
||||
reverse("portal-migration-confirm", args=(migration_request.request_id,))
|
||||
)
|
||||
self.assertEqual(confirmation.status_code, 403)
|
||||
self.assertFalse(DeviceCredential.objects.exists())
|
||||
|
||||
def test_expired_request_and_duplicate_migration_grant_are_rejected(self):
|
||||
self.grant_migration()
|
||||
with self.assertRaisesRegex(LicensingError, "已有此产品的迁移资格"):
|
||||
self.grant_migration()
|
||||
migration_request, _raw_token = create_migration_request(
|
||||
user=self.user,
|
||||
device=self.device,
|
||||
now=timezone.now() - timedelta(minutes=20),
|
||||
)
|
||||
with self.assertRaisesRegex(LicensingError, "迁移请求已过期"):
|
||||
confirm_migration_request(
|
||||
request_id=migration_request.request_id,
|
||||
user=self.user,
|
||||
now=timezone.now(),
|
||||
)
|
||||
self.assertFalse(DeviceCredential.objects.exists())
|
||||
|
||||
Reference in New Issue
Block a user