614 lines
24 KiB
Python
614 lines
24 KiB
Python
"""测试配置与 Fixtures。
|
||
|
||
提供测试数据库、认证头、数据工厂等公共 fixtures。
|
||
使用 SQLite 内存数据库进行隔离测试,每个测试用例结束后自动回滚。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
import sys
|
||
import asyncio
|
||
import pytest
|
||
from datetime import datetime, timedelta, timezone
|
||
from typing import Generator
|
||
|
||
# 添加项目根目录到 Python 路径
|
||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
|
||
|
||
# 设置测试环境变量,必须在导入 app 之前
|
||
os.environ["APP_ENV"] = "test"
|
||
os.environ["MYSQL_DATABASE"] = ":memory:"
|
||
os.environ["SECRET_KEY"] = "test-secret-key-for-testing-only"
|
||
os.environ["JWT_EXPIRE_MINUTES"] = "60"
|
||
os.environ["AI_PROVIDER"] = "mock"
|
||
os.environ["AUTH_TOKEN_STORE_PATH"] = "D:/tmp/order-flow-test/revoked_tokens.json"
|
||
|
||
import uuid
|
||
from sqlalchemy import create_engine, event
|
||
from sqlalchemy.orm import Session, sessionmaker
|
||
|
||
from backend.app.core.security import create_access_token, hash_password
|
||
from backend.app.db import Base
|
||
from backend.app.main import app
|
||
|
||
|
||
# ============================================================
|
||
# 兼容 httpx 0.28+ 的同步测试客户端
|
||
# ============================================================
|
||
|
||
class SyncTestClient:
|
||
"""同步测试客户端,封装 httpx.AsyncClient + ASGITransport。
|
||
|
||
兼容 httpx 0.28+ (移除了 Client 的 app 参数)。
|
||
提供与 starlette.testclient.TestClient 相同的同步接口。
|
||
"""
|
||
|
||
def __init__(self, app_obj, raise_server_exceptions=True):
|
||
import httpx
|
||
self._app = app_obj
|
||
self._raise = raise_server_exceptions
|
||
self._transport = httpx.ASGITransport(app=app_obj, raise_app_exceptions=raise_server_exceptions)
|
||
self._client = httpx.AsyncClient(
|
||
transport=self._transport,
|
||
base_url="http://testserver",
|
||
follow_redirects=True,
|
||
)
|
||
self._loop = asyncio.new_event_loop()
|
||
|
||
def _run(self, coro):
|
||
return self._loop.run_until_complete(coro)
|
||
|
||
def request(self, method, url, **kwargs):
|
||
return self._run(self._client.request(method, url, **kwargs))
|
||
|
||
def get(self, url, **kwargs):
|
||
return self.request("GET", url, **kwargs)
|
||
|
||
def post(self, url, **kwargs):
|
||
return self.request("POST", url, **kwargs)
|
||
|
||
def put(self, url, **kwargs):
|
||
return self.request("PUT", url, **kwargs)
|
||
|
||
def delete(self, url, **kwargs):
|
||
return self.request("DELETE", url, **kwargs)
|
||
|
||
def patch(self, url, **kwargs):
|
||
return self.request("PATCH", url, **kwargs)
|
||
|
||
def options(self, url, **kwargs):
|
||
return self.request("OPTIONS", url, **kwargs)
|
||
|
||
def head(self, url, **kwargs):
|
||
return self.request("HEAD", url, **kwargs)
|
||
|
||
def close(self):
|
||
self._run(self._client.aclose())
|
||
self._loop.close()
|
||
|
||
def __enter__(self):
|
||
return self
|
||
|
||
def __exit__(self, *args):
|
||
self.close()
|
||
|
||
|
||
# ============================================================
|
||
# 数据库 Fixtures
|
||
# ============================================================
|
||
|
||
@pytest.fixture(scope="session")
|
||
def engine():
|
||
"""创建 SQLite 内存引擎,整个测试会话共享。
|
||
|
||
使用 check_same_thread=False 允许多线程访问。
|
||
"""
|
||
eng = create_engine(
|
||
"sqlite:///:memory:",
|
||
echo=False,
|
||
connect_args={"check_same_thread": False},
|
||
)
|
||
Base.metadata.create_all(eng)
|
||
yield eng
|
||
Base.metadata.drop_all(eng)
|
||
eng.dispose()
|
||
|
||
|
||
@pytest.fixture(scope="function")
|
||
def db_session(engine) -> Generator[Session, None, None]:
|
||
"""每个测试函数独立的数据库会话,测试结束后自动回滚。"""
|
||
connection = engine.connect()
|
||
transaction = connection.begin()
|
||
session = Session(bind=connection)
|
||
|
||
yield session
|
||
|
||
session.close()
|
||
transaction.rollback()
|
||
connection.close()
|
||
|
||
|
||
@pytest.fixture(scope="function")
|
||
def client(db_session, seed_data):
|
||
"""FastAPI 测试客户端,使用测试数据库会话。
|
||
|
||
使用 SyncTestClient (封装 httpx.AsyncClient + ASGITransport),
|
||
兼容 httpx 0.28+ 版本。
|
||
"""
|
||
from backend.app.api.deps import get_db_session
|
||
|
||
def override_get_db():
|
||
try:
|
||
yield db_session
|
||
finally:
|
||
pass
|
||
|
||
app.dependency_overrides[get_db_session] = override_get_db
|
||
|
||
with SyncTestClient(app, raise_server_exceptions=False) as c:
|
||
yield c
|
||
|
||
app.dependency_overrides.clear()
|
||
|
||
|
||
# ============================================================
|
||
# 基础数据 Fixtures
|
||
# ============================================================
|
||
|
||
@pytest.fixture
|
||
def seed_roles(db_session):
|
||
"""预置系统角色。"""
|
||
from backend.app.models.system import Role
|
||
|
||
roles = [
|
||
Role(id=1, role_name="管理员", role_code="admin", status=1),
|
||
Role(id=2, role_name="业务员", role_code="salesman", status=1),
|
||
Role(id=3, role_name="管理层", role_code="manager", status=1),
|
||
Role(id=4, role_name="司机", role_code="driver", status=1),
|
||
Role(id=5, role_name="秘书", role_code="secretary", status=1),
|
||
]
|
||
db_session.add_all(roles)
|
||
db_session.flush()
|
||
return roles
|
||
|
||
|
||
@pytest.fixture
|
||
def seed_users(db_session, seed_roles):
|
||
"""预置测试用户。"""
|
||
from backend.app.models.system import User
|
||
|
||
users = [
|
||
User(id=1, username="admin01", password_hash=hash_password("admin123"),
|
||
real_name="管理员", mobile="13800000001", role_id=1, status=1),
|
||
User(id=2, username="sales01", password_hash=hash_password("sales123"),
|
||
real_name="业务员A", mobile="13800000002", role_id=2, status=1),
|
||
User(id=3, username="sales02", password_hash=hash_password("sales123"),
|
||
real_name="业务员B", mobile="13800000003", role_id=2, status=1),
|
||
User(id=4, username="manager01", password_hash=hash_password("manager123"),
|
||
real_name="管理层", mobile="13800000004", role_id=3, status=1),
|
||
User(id=5, username="driver01", password_hash=hash_password("driver123"),
|
||
real_name="司机A", mobile="13800000005", role_id=4, status=1),
|
||
User(id=6, username="driver02", password_hash=hash_password("driver123"),
|
||
real_name="司机B", mobile="13800000006", role_id=4, status=1),
|
||
User(id=7, username="secretary01", password_hash=hash_password("secretary123"),
|
||
real_name="秘书", mobile="13800000007", role_id=5, status=1),
|
||
]
|
||
db_session.add_all(users)
|
||
db_session.flush()
|
||
return users
|
||
|
||
|
||
@pytest.fixture
|
||
def seed_menus(db_session):
|
||
"""预置菜单权限。"""
|
||
from backend.app.models.system import Menu, RoleMenu
|
||
|
||
menus = [
|
||
# === 订单相关 ===
|
||
Menu(id=1, menu_name="订单管理", menu_path="/orders", menu_type="page",
|
||
permission_code="order:list", sort_no=1, status=1),
|
||
Menu(id=2, menu_name="创建订单", menu_path="/orders/create", menu_type="button",
|
||
permission_code="order:create", parent_id=1, sort_no=1, status=1),
|
||
Menu(id=3, menu_name="审批订单", menu_path="/orders/approve", menu_type="button",
|
||
permission_code="order:approve", parent_id=1, sort_no=2, status=1),
|
||
Menu(id=12, menu_name="提交订单", menu_path="", menu_type="button",
|
||
permission_code="order:submit", parent_id=1, sort_no=3, status=1),
|
||
Menu(id=13, menu_name="取消订单", menu_path="", menu_type="button",
|
||
permission_code="order:cancel", parent_id=1, sort_no=4, status=1),
|
||
Menu(id=14, menu_name="取消审批", menu_path="", menu_type="button",
|
||
permission_code="order:cancel-approve", parent_id=1, sort_no=5, status=1),
|
||
Menu(id=15, menu_name="供应商文本", menu_path="", menu_type="button",
|
||
permission_code="order:supplier-text", parent_id=1, sort_no=6, status=1),
|
||
Menu(id=16, menu_name="更新订单", menu_path="", menu_type="button",
|
||
permission_code="order:update", parent_id=1, sort_no=7, status=1),
|
||
# === 客户相关 ===
|
||
Menu(id=4, menu_name="客户管理", menu_path="/customers", menu_type="page",
|
||
permission_code="customer:list", sort_no=2, status=1),
|
||
Menu(id=22, menu_name="创建客户", menu_path="", menu_type="button",
|
||
permission_code="customer:create", parent_id=4, sort_no=1, status=1),
|
||
Menu(id=23, menu_name="更新客户", menu_path="", menu_type="button",
|
||
permission_code="customer:update", parent_id=4, sort_no=2, status=1),
|
||
# === 产品相关 ===
|
||
Menu(id=5, menu_name="产品管理", menu_path="/products", menu_type="page",
|
||
permission_code="product:list", sort_no=3, status=1),
|
||
# === 报表相关 ===
|
||
Menu(id=7, menu_name="报表统计", menu_path="/reports", menu_type="page",
|
||
permission_code="report:performance:view", sort_no=5, status=1),
|
||
Menu(id=24, menu_name="导出报表", menu_path="", menu_type="button",
|
||
permission_code="report:performance:export", parent_id=7, sort_no=1, status=1),
|
||
# === 司机任务相关 ===
|
||
Menu(id=8, menu_name="司机任务", menu_path="/driver/tasks", menu_type="page",
|
||
permission_code="driver:task:list", sort_no=6, status=1),
|
||
Menu(id=9, menu_name="接单", menu_path="", menu_type="button",
|
||
permission_code="driver:task:accept", parent_id=8, sort_no=1, status=1),
|
||
Menu(id=10, menu_name="揽货", menu_path="", menu_type="button",
|
||
permission_code="driver:task:pickup", parent_id=8, sort_no=2, status=1),
|
||
Menu(id=11, menu_name="送达", menu_path="", menu_type="button",
|
||
permission_code="driver:task:deliver", parent_id=8, sort_no=3, status=1),
|
||
# === 物流管理相关 ===
|
||
Menu(id=25, menu_name="物流任务查看", menu_path="", menu_type="button",
|
||
permission_code="logistics:task:list", parent_id=8, sort_no=4, status=1),
|
||
Menu(id=26, menu_name="创建物流任务", menu_path="", menu_type="button",
|
||
permission_code="logistics:task:create", parent_id=8, sort_no=5, status=1),
|
||
Menu(id=27, menu_name="取消物流任务", menu_path="", menu_type="button",
|
||
permission_code="logistics:task:cancel", parent_id=8, sort_no=6, status=1),
|
||
Menu(id=28, menu_name="物流轨迹查看", menu_path="", menu_type="button",
|
||
permission_code="logistics:trace:list", parent_id=8, sort_no=7, status=1),
|
||
Menu(id=29, menu_name="新增物流轨迹", menu_path="", menu_type="button",
|
||
permission_code="logistics:trace:create", parent_id=8, sort_no=8, status=1),
|
||
# === 系统管理相关 ===
|
||
Menu(id=6, menu_name="系统管理", menu_path="/system", menu_type="page",
|
||
permission_code="system:view", sort_no=10, status=1),
|
||
Menu(id=30, menu_name="用户列表", menu_path="", menu_type="button",
|
||
permission_code="system:user:list", parent_id=6, sort_no=1, status=1),
|
||
Menu(id=31, menu_name="创建用户", menu_path="", menu_type="button",
|
||
permission_code="system:user:create", parent_id=6, sort_no=2, status=1),
|
||
Menu(id=32, menu_name="更新用户", menu_path="", menu_type="button",
|
||
permission_code="system:user:update", parent_id=6, sort_no=3, status=1),
|
||
Menu(id=33, menu_name="重置密码", menu_path="", menu_type="button",
|
||
permission_code="system:user:reset-password", parent_id=6, sort_no=4, status=1),
|
||
Menu(id=34, menu_name="角色列表", menu_path="", menu_type="button",
|
||
permission_code="system:role:list", parent_id=6, sort_no=5, status=1),
|
||
Menu(id=35, menu_name="创建角色", menu_path="", menu_type="button",
|
||
permission_code="system:role:create", parent_id=6, sort_no=6, status=1),
|
||
Menu(id=36, menu_name="更新角色", menu_path="", menu_type="button",
|
||
permission_code="system:role:update", parent_id=6, sort_no=7, status=1),
|
||
Menu(id=37, menu_name="分配菜单", menu_path="", menu_type="button",
|
||
permission_code="system:role:assign-menus", parent_id=6, sort_no=8, status=1),
|
||
Menu(id=38, menu_name="菜单列表", menu_path="", menu_type="button",
|
||
permission_code="system:menu:list", parent_id=6, sort_no=9, status=1),
|
||
Menu(id=39, menu_name="创建菜单", menu_path="", menu_type="button",
|
||
permission_code="system:menu:create", parent_id=6, sort_no=10, status=1),
|
||
Menu(id=40, menu_name="更新菜单", menu_path="", menu_type="button",
|
||
permission_code="system:menu:update", parent_id=6, sort_no=11, status=1),
|
||
# === 主数据相关 ===
|
||
Menu(id=41, menu_name="主数据列表", menu_path="", menu_type="button",
|
||
permission_code="master-data:list", parent_id=5, sort_no=1, status=1),
|
||
Menu(id=42, menu_name="主数据更新", menu_path="", menu_type="button",
|
||
permission_code="master-data:update", parent_id=5, sort_no=2, status=1),
|
||
# === AI 相关 ===
|
||
Menu(id=43, menu_name="AI识别", menu_path="", menu_type="button",
|
||
permission_code="ai:recognize", sort_no=8, status=1),
|
||
Menu(id=44, menu_name="AI修正", menu_path="", menu_type="button",
|
||
permission_code="ai:correct", parent_id=43, sort_no=1, status=1),
|
||
Menu(id=45, menu_name="AI解析订单", menu_path="", menu_type="button",
|
||
permission_code="ai:parse-order", parent_id=43, sort_no=2, status=1),
|
||
]
|
||
db_session.add_all(menus)
|
||
db_session.flush()
|
||
|
||
# 角色-菜单关联
|
||
role_menus = [
|
||
# 管理员:全部权限
|
||
*[RoleMenu(role_id=1, menu_id=m.id) for m in menus],
|
||
# 业务员:订单 CRUD、客户、AI
|
||
RoleMenu(role_id=2, menu_id=1), # order:list
|
||
RoleMenu(role_id=2, menu_id=2), # order:create
|
||
RoleMenu(role_id=2, menu_id=12), # order:submit
|
||
RoleMenu(role_id=2, menu_id=13), # order:cancel
|
||
RoleMenu(role_id=2, menu_id=16), # order:update
|
||
RoleMenu(role_id=2, menu_id=4), # customer:list
|
||
RoleMenu(role_id=2, menu_id=22), # customer:create
|
||
RoleMenu(role_id=2, menu_id=23), # customer:update
|
||
RoleMenu(role_id=2, menu_id=45), # ai:parse-order
|
||
RoleMenu(role_id=2, menu_id=28), # logistics:trace:list
|
||
# 管理层:订单、审批、物流、报表、主数据、AI
|
||
RoleMenu(role_id=3, menu_id=1), # order:list
|
||
RoleMenu(role_id=3, menu_id=3), # order:approve
|
||
RoleMenu(role_id=3, menu_id=14), # order:cancel-approve
|
||
RoleMenu(role_id=3, menu_id=15), # order:supplier-text
|
||
RoleMenu(role_id=3, menu_id=7), # report:performance:view
|
||
RoleMenu(role_id=3, menu_id=24), # report:performance:export
|
||
RoleMenu(role_id=3, menu_id=25), # logistics:task:list
|
||
RoleMenu(role_id=3, menu_id=26), # logistics:task:create
|
||
RoleMenu(role_id=3, menu_id=27), # logistics:task:cancel
|
||
RoleMenu(role_id=3, menu_id=28), # logistics:trace:list
|
||
RoleMenu(role_id=3, menu_id=29), # logistics:trace:create
|
||
RoleMenu(role_id=3, menu_id=41), # master-data:list
|
||
RoleMenu(role_id=3, menu_id=42), # master-data:update
|
||
RoleMenu(role_id=3, menu_id=43), # ai:recognize
|
||
RoleMenu(role_id=3, menu_id=44), # ai:correct
|
||
# 司机:司机任务、物流轨迹
|
||
RoleMenu(role_id=4, menu_id=8), # driver:task:list
|
||
RoleMenu(role_id=4, menu_id=9), # driver:task:accept
|
||
RoleMenu(role_id=4, menu_id=10), # driver:task:pickup
|
||
RoleMenu(role_id=4, menu_id=11), # driver:task:deliver
|
||
RoleMenu(role_id=4, menu_id=28), # logistics:trace:list
|
||
RoleMenu(role_id=4, menu_id=29), # logistics:trace:create
|
||
# 秘书:AI 相关
|
||
RoleMenu(role_id=5, menu_id=43), # ai:recognize
|
||
RoleMenu(role_id=5, menu_id=44), # ai:correct
|
||
RoleMenu(role_id=5, menu_id=45), # ai:parse-order
|
||
]
|
||
db_session.add_all(role_menus)
|
||
db_session.flush()
|
||
return menus
|
||
|
||
|
||
@pytest.fixture
|
||
def seed_data(db_session, seed_users, seed_menus):
|
||
"""预置完整基础数据,返回数据库会话。"""
|
||
from backend.app.models.system import SystemConfig
|
||
|
||
# 预置系统配置
|
||
configs = [
|
||
SystemConfig(config_key="logistics_timeout_days", config_value="2",
|
||
config_name="物流超时天数", status=1),
|
||
SystemConfig(config_key="inactive_customer_days", config_value="30",
|
||
config_name="沉默客户天数", status=1),
|
||
SystemConfig(config_key="arrears_generate_mode", config_value="delivered",
|
||
config_name="欠款生成模式", status=1),
|
||
SystemConfig(config_key="arrears_reminder_start_type", config_value="order_date",
|
||
config_name="欠款起算口径", status=1),
|
||
SystemConfig(config_key="inactive_order_amount_threshold", config_value="1000",
|
||
config_name="沉默客户金额阈值", status=1),
|
||
]
|
||
db_session.add_all(configs)
|
||
db_session.flush()
|
||
return db_session
|
||
|
||
|
||
# ============================================================
|
||
# 认证头 Fixtures
|
||
# ============================================================
|
||
|
||
@pytest.fixture(autouse=True)
|
||
def _reset_test_state():
|
||
"""每个测试前重置全局状态,防止跨测试污染。"""
|
||
from backend.app.services.auth_service import auth_service
|
||
auth_service.revoked_tokens.clear()
|
||
# 清除 Redis 缓存,防止缓存导致的测试间数据泄漏
|
||
try:
|
||
from backend.app.core.cache import get_redis
|
||
r = get_redis()
|
||
if r is not None:
|
||
r.flushdb()
|
||
except Exception:
|
||
pass
|
||
yield
|
||
auth_service.revoked_tokens.clear()
|
||
|
||
|
||
@pytest.fixture
|
||
def admin_headers(seed_data) -> dict:
|
||
"""管理员认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 1, "role_code": "admin", "username": "admin01", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def salesman_headers(seed_data) -> dict:
|
||
"""业务员认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 2, "role_code": "salesman", "username": "sales01", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def salesman2_headers(seed_data) -> dict:
|
||
"""业务员B认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 3, "role_code": "salesman", "username": "sales02", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def manager_headers(seed_data) -> dict:
|
||
"""管理层认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 4, "role_code": "manager", "username": "manager01", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def driver_headers(seed_data) -> dict:
|
||
"""司机认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 5, "role_code": "driver", "username": "driver01", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def driver2_headers(seed_data) -> dict:
|
||
"""司机B认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 6, "role_code": "driver", "username": "driver02", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
@pytest.fixture
|
||
def secretary_headers(seed_data) -> dict:
|
||
"""秘书认证头(每次生成唯一 token)。"""
|
||
token = create_access_token({"user_id": 7, "role_code": "secretary", "username": "secretary01", "jti": uuid.uuid4().hex})
|
||
return {"Authorization": f"Bearer {token}"}
|
||
|
||
|
||
# ============================================================
|
||
# 数据工厂 Fixtures
|
||
# ============================================================
|
||
|
||
@pytest.fixture
|
||
def make_customer(db_session, seed_data):
|
||
"""客户工厂函数。"""
|
||
from backend.app.models.business import Customer
|
||
|
||
_counter = [0]
|
||
|
||
def _make(**kwargs):
|
||
_counter[0] += 1
|
||
defaults = {
|
||
"customer_name": f"测试客户{_counter[0]}",
|
||
"mobile": f"1380000{_counter[0]:04d}",
|
||
"settlement_type": "immediate",
|
||
"settlement_days": 0,
|
||
"salesman_id": 2,
|
||
"credit_limit": 0,
|
||
"deleted": 0,
|
||
}
|
||
defaults.update(kwargs)
|
||
customer = Customer(**defaults)
|
||
db_session.add(customer)
|
||
db_session.flush()
|
||
return customer
|
||
|
||
return _make
|
||
|
||
|
||
@pytest.fixture
|
||
def make_product(db_session, seed_data):
|
||
"""产品工厂函数。"""
|
||
from backend.app.models.business import Product
|
||
|
||
_counter = [0]
|
||
|
||
def _make(**kwargs):
|
||
_counter[0] += 1
|
||
defaults = {
|
||
"product_name": f"测试产品{_counter[0]}",
|
||
"specification": f"{100 + _counter[0]}x{100 + _counter[0]}cm",
|
||
"unit": "张",
|
||
"category": "工业品",
|
||
"cost_price": 60.0,
|
||
"sale_price": 100.0,
|
||
"status": 1,
|
||
}
|
||
defaults.update(kwargs)
|
||
product = Product(**defaults)
|
||
db_session.add(product)
|
||
db_session.flush()
|
||
return product
|
||
|
||
return _make
|
||
|
||
|
||
@pytest.fixture
|
||
def make_supplier(db_session, seed_data):
|
||
"""供应商工厂函数。"""
|
||
from backend.app.models.business import Supplier
|
||
|
||
_counter = [0]
|
||
|
||
def _make(**kwargs):
|
||
_counter[0] += 1
|
||
defaults = {
|
||
"supplier_name": f"测试工厂{_counter[0]}",
|
||
"supplier_type": "factory",
|
||
"contact_name": f"联系人{_counter[0]}",
|
||
"contact_mobile": f"1390000{_counter[0]:04d}",
|
||
"status": 1,
|
||
}
|
||
defaults.update(kwargs)
|
||
supplier = Supplier(**defaults)
|
||
db_session.add(supplier)
|
||
db_session.flush()
|
||
return supplier
|
||
|
||
return _make
|
||
|
||
|
||
@pytest.fixture
|
||
def make_order(db_session, seed_data, make_customer, make_product, make_supplier):
|
||
"""订单工厂函数,支持指定订单状态。"""
|
||
from backend.app.models.business import SalesOrder, SalesOrderItem
|
||
|
||
_counter = [0]
|
||
|
||
def _make(order_status="draft", salesman_id=2, **kwargs):
|
||
_counter[0] += 1
|
||
customer = make_customer()
|
||
product = make_product()
|
||
supplier = make_supplier()
|
||
|
||
order = SalesOrder(
|
||
order_no=f"TEST-ORD-{_counter[0]:06d}",
|
||
customer_id=customer.id,
|
||
customer_name=customer.customer_name,
|
||
customer_mobile=customer.mobile,
|
||
salesman_id=salesman_id,
|
||
order_status=order_status,
|
||
factory_id=supplier.id,
|
||
sale_price_total=1000.0,
|
||
cost_price_total=600.0,
|
||
profit_total=400.0,
|
||
profit_rate=40.0,
|
||
deleted=0,
|
||
)
|
||
for k, v in kwargs.items():
|
||
if hasattr(order, k):
|
||
setattr(order, k, v)
|
||
|
||
db_session.add(order)
|
||
db_session.flush()
|
||
|
||
item = SalesOrderItem(
|
||
order_id=order.id,
|
||
product_id=product.id,
|
||
product_name=product.product_name,
|
||
specification=product.specification,
|
||
unit=product.unit,
|
||
quantity=10,
|
||
sale_price=100.0,
|
||
cost_price=60.0,
|
||
deleted=0,
|
||
)
|
||
db_session.add(item)
|
||
db_session.flush()
|
||
|
||
# 附加关联对象到 order 上,方便测试使用
|
||
order._test_customer = customer
|
||
order._test_product = product
|
||
order._test_supplier = supplier
|
||
return order
|
||
|
||
return _make
|
||
|
||
|
||
@pytest.fixture
|
||
def make_task(db_session, seed_data, make_order):
|
||
"""司机任务工厂函数。"""
|
||
from backend.app.models.business import LogisticsTask
|
||
|
||
_counter = [0]
|
||
|
||
def _make(status="pending", driver_id=5, **kwargs):
|
||
_counter[0] += 1
|
||
order = make_order(order_status="approved")
|
||
|
||
task = LogisticsTask(
|
||
task_no=f"TASK-{_counter[0]:06d}",
|
||
order_id=order.id,
|
||
driver_id=driver_id,
|
||
factory_id=order.factory_id,
|
||
pickup_address="泰兴工厂",
|
||
delivery_address="上海仓库",
|
||
pickup_content="测试货物",
|
||
quantity=10,
|
||
status=status,
|
||
created_by=1,
|
||
)
|
||
for k, v in kwargs.items():
|
||
if hasattr(task, k):
|
||
setattr(task, k, v)
|
||
|
||
db_session.add(task)
|
||
db_session.flush()
|
||
task._test_order = order
|
||
return task
|
||
|
||
return _make
|