"""Dramatiq 调度任务测试。 方式: - TASK_BROKER=stub:进程内 StubBroker,验证 actor 注册与消息入队; - 直接调用 actor(dramatiq actor 可调用,同步执行函数体)验证业务结果, 避免测试进程内 Worker 线程与 SQLite 事务锁冲突。 """ import pytest from datetime import date, timedelta from decimal import Decimal from model_bakery import baker import dramatiq from dramatiq.brokers.stub import StubBroker from dramatiq.middleware import AgeLimit, Callbacks, Retries, TimeLimit # 显式安装 StubBroker(覆盖 core.ready 可能已装的 RedisBroker) broker = StubBroker(middleware=[AgeLimit(), TimeLimit(), Callbacks(), Retries()]) dramatiq.set_broker(broker) # 在 stub broker 就绪后导入 actors(完成注册) from apps.notify.tasks import ( # noqa: E402 run_alert_checks_for_tenant, run_all_alert_checks_for_all_tenants, ) from apps.channel.tasks import ( # noqa: E402 sync_orders_for_account, sync_orders_for_all_active_accounts, ) from apps.catalog.models import Product # noqa: E402 from apps.inventory.models import Warehouse, Stock # noqa: E402 from apps.partner.models import Customer # noqa: E402 from apps.finance.models import Receivable # noqa: E402 from apps.notify.models import Notification # noqa: E402 from apps.channel.models import ChannelAccount # noqa: E402 from apps.sales.models import SalesOrder # noqa: E402 @pytest.fixture(autouse=True) def _flush_broker(): broker.flush_all() yield broker.flush_all() @pytest.mark.django_db def test_actors_registered(): names = {a.actor_name for a in broker.actors.values()} assert "run_alert_checks_for_tenant" in names assert "run_all_alert_checks_for_all_tenants" in names assert "sync_orders_for_account" in names assert "sync_orders_for_all_active_accounts" in names @pytest.mark.django_db def test_alert_actor_enqueues_message(tenant): run_alert_checks_for_tenant.send(tenant.id) assert broker.queues["default"].qsize() == 1 import json raw = broker.queues["default"].queue[0] msg = json.loads(raw) assert msg["actor_name"] == "run_alert_checks_for_tenant" assert msg["args"] == [tenant.id] @pytest.mark.django_db def test_alert_actor_inline_execution_triggers_overdue_alert(tenant): from apps.notify.models import AlertRule baker.make(AlertRule, tenant=tenant, code="receivable_overdue_30d", name="逾期催收", rule_type="receivable_overdue", is_enabled=True) cust = baker.make(Customer, tenant=tenant, code="C_TQ", name="逾期客户") baker.make( Receivable, tenant=tenant, customer=cust, bill_no="RC-TQ-01", bill_date=date.today() - timedelta(days=60), due_date=date.today() - timedelta(days=10), total_amount=Decimal("500"), paid_amount=Decimal("0"), status="open", ) result = run_alert_checks_for_tenant(tenant.id) # 直接调用 = 同步执行 assert result["receivable_alerts_count"] == 1 assert Notification.objects.filter(tenant=tenant, category="warning").count() == 1 @pytest.mark.django_db def test_all_tenants_alert_actor_inline(tenant, other_tenant): for t in (tenant, other_tenant): wh = baker.make(Warehouse, tenant=t, code=f"W_{t.code}") p = baker.make(Product, tenant=t, code=f"P_{t.code}", name=f"缺货商品{t.code}") baker.make(Stock, tenant=t, warehouse=wh, product=p, on_hand=Decimal("2"), locked=Decimal("0")) summary = run_all_alert_checks_for_all_tenants() by_code = {row["tenant"]: row for row in summary} assert by_code[tenant.code]["stock_alerts_count"] == 1 assert by_code[other_tenant.code]["stock_alerts_count"] == 1 assert Notification.objects.filter(tenant=tenant, category="warning").count() == 1 assert Notification.objects.filter(tenant=other_tenant, category="warning").count() == 1 @pytest.mark.django_db def test_channel_sync_actor_inline_with_mock_fallback(tenant): account = baker.make( ChannelAccount, tenant=tenant, platform="douyin", shop_id="DY_TASK", shop_name="任务店铺", app_key="", access_token="", # 无凭证 → Mock 回退 ) baker.make(Product, tenant=tenant, code="P_TASK", status="active", sale_price=Decimal("30.00")) result = sync_orders_for_account(account.id) assert result["pulled_count"] == 1 assert result["converted_count"] == 1 assert SalesOrder.objects.filter(tenant=tenant, remark__contains=result["pulled_order"] if "pulled_order" in result else "").exists() or \ SalesOrder.objects.filter(tenant=tenant).count() == 1