feat: 实现受控真实下单安全边界 (#99)
This commit is contained in:
@@ -7,7 +7,11 @@ from pathlib import Path
|
||||
from src.android_device_service import AndroidDeviceSearchError
|
||||
from src.admin_gateway import AdminTask, ClientInfo, SubmissionReceipt
|
||||
from src.mock_admin_gateway import MockAdminGateway
|
||||
from src.pdd_purchase_adapter import PddPurchaseAdapter, PurchasePageState
|
||||
from src.pdd_purchase_adapter import (
|
||||
PddLivePurchaseAdapter,
|
||||
PddPurchaseAdapter,
|
||||
PurchasePageState,
|
||||
)
|
||||
from src.task_dispatcher import TaskDispatcher, admin_task_to_new_claimed_task
|
||||
from src.task_models import TaskStatus, TaskType
|
||||
from src.task_repository import TaskRepository
|
||||
@@ -70,12 +74,31 @@ class ReadyPurchaseAdapter(PddPurchaseAdapter):
|
||||
pass
|
||||
|
||||
|
||||
def purchase_task(task_id="PUR-001"):
|
||||
class ReadyLivePurchaseAdapter(ReadyPurchaseAdapter, PddLivePurchaseAdapter):
|
||||
def read_state(self):
|
||||
state = super().read_state()
|
||||
return PurchasePageState(
|
||||
page_kind=state.page_kind,
|
||||
goods_id=state.goods_id,
|
||||
selected_options=state.selected_options,
|
||||
quantity=state.quantity,
|
||||
price_cent=state.price_cent,
|
||||
candidate_count=state.candidate_count,
|
||||
in_stock=True,
|
||||
submit_candidate_count=1,
|
||||
)
|
||||
|
||||
def submit_order_once(self):
|
||||
self.calls.append(("purchase", "submitted_once"))
|
||||
|
||||
|
||||
def purchase_task(task_id="PUR-001", execution_mode="dry_run"):
|
||||
return AdminTask(
|
||||
task_id=task_id,
|
||||
task_type=TaskType.PURCHASE,
|
||||
version=1,
|
||||
priority=10,
|
||||
execution_mode=execution_mode,
|
||||
payload={
|
||||
"goods_url": "https://example.test/PUR-GOODS",
|
||||
"goods_id": "PUR-GOODS",
|
||||
@@ -103,7 +126,14 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
self.temporary.cleanup()
|
||||
|
||||
def _dispatcher(self, *, purchase_ready, device_checker=lambda _serial: None):
|
||||
def _dispatcher(
|
||||
self,
|
||||
*,
|
||||
purchase_ready,
|
||||
live_ready=False,
|
||||
purchase_mode="dry_run",
|
||||
device_checker=lambda _serial: None,
|
||||
):
|
||||
purchase_factory = None
|
||||
if purchase_ready:
|
||||
purchase_factory = (
|
||||
@@ -118,6 +148,16 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
lambda *_args: RecordingCollector(self.calls)
|
||||
),
|
||||
purchase_adapter_factory=purchase_factory,
|
||||
live_purchase_adapter_factory=(
|
||||
(
|
||||
lambda _address, _cancelled: ReadyLivePurchaseAdapter(
|
||||
self.calls
|
||||
)
|
||||
)
|
||||
if live_ready
|
||||
else None
|
||||
),
|
||||
purchase_mode=purchase_mode,
|
||||
device_connection_checker=device_checker,
|
||||
)
|
||||
|
||||
@@ -137,6 +177,61 @@ class TaskDispatcherTest(unittest.TestCase):
|
||||
ready.claim_capabilities().purchase_mode, "dry_run"
|
||||
)
|
||||
|
||||
def test_live_capability_requires_authorization_and_live_factory(self):
|
||||
no_factory = self._dispatcher(
|
||||
purchase_ready=True, purchase_mode="live"
|
||||
)
|
||||
authorized = self._dispatcher(
|
||||
purchase_ready=True,
|
||||
live_ready=True,
|
||||
purchase_mode="live",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
no_factory.claim_capabilities().purchase_mode, "dry_run"
|
||||
)
|
||||
self.assertEqual(
|
||||
authorized.claim_capabilities().purchase_mode, "live"
|
||||
)
|
||||
|
||||
def test_local_live_task_never_runs_after_authorization_is_disabled(self):
|
||||
task = purchase_task(execution_mode="live")
|
||||
self.repository.add_claimed_task(admin_task_to_new_claimed_task(task))
|
||||
|
||||
with self.assertRaisesRegex(RuntimeError, "授权已关闭"):
|
||||
self._dispatcher(
|
||||
purchase_ready=True,
|
||||
live_ready=True,
|
||||
purchase_mode="dry_run",
|
||||
).execute_one()
|
||||
|
||||
self.assertNotIn(("purchase", "submitted_once"), self.calls)
|
||||
self.assertEqual(
|
||||
self.repository.get_task(task.task_id).status,
|
||||
TaskStatus.CLAIMED,
|
||||
)
|
||||
|
||||
def test_live_task_is_claimed_only_by_authorized_live_dispatcher(self):
|
||||
task = purchase_task(execution_mode="live")
|
||||
self.gateway.enqueue_task(task, self.client.client_id)
|
||||
|
||||
dry_outcome = self._dispatcher(purchase_ready=True).execute_one()
|
||||
live_outcome = self._dispatcher(
|
||||
purchase_ready=True,
|
||||
live_ready=True,
|
||||
purchase_mode="live",
|
||||
).execute_one()
|
||||
|
||||
self.assertEqual(dry_outcome.kind, "no_task")
|
||||
self.assertEqual(live_outcome.kind, "manual_review")
|
||||
self.assertEqual(
|
||||
self.repository.get_task(task.task_id).execution_mode,
|
||||
"live",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.calls.count(("purchase", "submitted_once")), 1
|
||||
)
|
||||
|
||||
def test_claims_saves_then_dispatches_purchase_dry_run(self):
|
||||
task = purchase_task()
|
||||
self.gateway.enqueue_task(task, self.client.client_id)
|
||||
|
||||
Reference in New Issue
Block a user