feat: 凭证生成、成本分析、AI问答、前端页面、集成测试与E2E测试

- 后端: 凭证生成引擎、金蝶导出器、凭证模板服务
- 后端: 成本分析服务、AI问答服务
- 后端: 科目映射CRUD API、分析API、QA API
- 后端: 集成测试(认证/任务/凭证) 49个测试全部通过
- 前端: 凭证管理、成本分析、导出中心、知识库、系统设置页面
- 前端: AuthGuard认证守卫、Dashboard AI聊天功能
- 前端: Playwright E2E测试 16 passed, 1 skipped
- 基础设施: Docker Compose、Nginx反向代理、.env.example
- 文档: 用户手册、管理员手册、发布检查清单
This commit is contained in:
selfrelease
2026-07-07 21:21:29 +08:00
parent feebbb10ac
commit 5278190750
47 changed files with 6567 additions and 156 deletions
+2
View File
@@ -0,0 +1,2 @@
"""
"""
+97
View File
@@ -0,0 +1,97 @@
"""
集成测试公共夹具
使用同步 TestClient + Mock AsyncSession 进行 API 集成测试
避免 AsyncClient + ASGITransport 在 pytest-asyncio 下的死锁问题
"""
from typing import AsyncGenerator
from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi.testclient import TestClient
from app.core.database import get_db
from app.main import app
@pytest.fixture
def mock_db() -> AsyncMock:
"""
创建 Mock AsyncSession
返回一个 AsyncMock 对象,模拟异步数据库会话
"""
session = AsyncMock()
session.commit = AsyncMock()
session.refresh = AsyncMock()
session.rollback = AsyncMock()
session.close = AsyncMock()
session.add = MagicMock()
session.delete = AsyncMock()
session.get = AsyncMock(return_value=None)
result_mock = MagicMock()
result_mock.scalars.return_value.all.return_value = []
result_mock.scalars.return_value.first.return_value = None
result_mock.scalar_one_or_none.return_value = None
result_mock.scalar.return_value = 0
result_mock.one.return_value = MagicMock()
session.execute = AsyncMock(return_value=result_mock)
return session
@pytest.fixture
def client(mock_db: AsyncMock) -> TestClient:
"""
创建同步测试客户端,覆盖数据库依赖
"""
async def override_get_db() -> AsyncGenerator[AsyncMock, None]:
yield mock_db
app.dependency_overrides[get_db] = override_get_db
with TestClient(app) as tc:
yield tc
app.dependency_overrides.clear()
def make_db_result(items: list = None, scalar=None, one=None):
"""
构造 db.execute() 返回值的辅助函数
Args:
items: scalars().all() 返回的列表
scalar: scalar() 返回的标量值
one: one() 返回的行对象
"""
result = MagicMock()
result.scalars.return_value.all.return_value = items or []
result.scalars.return_value.first.return_value = items[0] if items else None
result.scalar_one_or_none.return_value = scalar
result.scalar.return_value = scalar if scalar is not None else 0
if one:
result.one.return_value = one
return result
@pytest.fixture
def auth_token() -> str:
"""
生成测试用 JWT Token(不依赖数据库)
"""
from app.core.security import create_access_token
return create_access_token(data={"sub": "1", "email": "test@example.com"})
@pytest.fixture
def auth_headers(auth_token: str) -> dict:
"""
返回认证请求头
Returns:
包含 Authorization 和 X-Company-ID 的请求头
"""
return {
"Authorization": f"Bearer {auth_token}",
"X-Company-ID": "1",
}
+174
View File
@@ -0,0 +1,174 @@
"""
认证 API 集成测试
测试注册、登录、获取当前用户、刷新 Token 等端点
使用同步 TestClient + Mock 数据库会话
"""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from fastapi.testclient import TestClient
from app.core.security import hash_password
from app.models.user import User, UserStatus
from tests.conftest import make_db_result
class TestAuthAPI:
"""认证 API 集成测试"""
def test_register_user_success(self, client: TestClient, mock_db: AsyncMock):
"""测试用户注册成功"""
# mock: 邮箱不存在
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=None)
)
# mock: commit + refresh 后返回带 id 的 user
def refresh_side_effect(obj, *args, **kwargs):
obj.id = 1
obj.created_at = datetime.utcnow()
obj.updated_at = datetime.utcnow()
obj.last_login_at = None
obj.permissions = None
obj.status = UserStatus.ACTIVE.value
mock_db.refresh.side_effect = refresh_side_effect
response = client.post(
"/api/auth/register",
json={
"email": "newuser@example.com",
"password": "password123",
"full_name": "新用户",
"company_id": 1,
},
)
assert response.status_code == 201
data = response.json()
assert data["email"] == "newuser@example.com"
assert data["full_name"] == "新用户"
def test_register_duplicate_email(self, client: TestClient, mock_db: AsyncMock):
"""测试重复邮箱注册失败"""
existing_user = MagicMock()
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=existing_user)
)
response = client.post(
"/api/auth/register",
json={
"email": "dup@example.com",
"password": "password123",
"full_name": "用户1",
"company_id": 1,
},
)
assert response.status_code == 400
def test_login_success(self, client: TestClient, mock_db: AsyncMock):
"""测试登录成功"""
password = "pass123456"
user = User(
id=1,
company_id=1,
email="login@example.com",
hashed_password=hash_password(password),
full_name="登录用户",
role="会计",
status=UserStatus.ACTIVE.value,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=user)
)
response = client.post(
"/api/auth/login",
json={"email": "login@example.com", "password": password},
)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
assert data["token_type"] == "bearer"
assert data["user"]["email"] == "login@example.com"
def test_login_wrong_password(self, client: TestClient, mock_db: AsyncMock):
"""测试密码错误登录失败"""
user = User(
id=1,
company_id=1,
email="wrong@example.com",
hashed_password=hash_password("correctpass"),
full_name="用户",
status=UserStatus.ACTIVE.value,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=user)
)
response = client.post(
"/api/auth/login",
json={"email": "wrong@example.com", "password": "wrongpass"},
)
assert response.status_code == 401
def test_login_nonexistent_user(self, client: TestClient, mock_db: AsyncMock):
"""测试不存在的用户登录失败"""
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=None)
)
response = client.post(
"/api/auth/login",
json={"email": "nobody@example.com", "password": "anypass"},
)
assert response.status_code == 401
def test_get_current_user_no_token(self, client: TestClient):
"""测试无 Token 访问被拒"""
response = client.get("/api/auth/me")
assert response.status_code == 401
def test_refresh_token(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试刷新 Token"""
user = User(
id=1,
company_id=1,
email="test@example.com",
hashed_password="x",
full_name="测试用户",
status=UserStatus.ACTIVE.value,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=user)
)
response = client.post("/api/auth/refresh", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert "access_token" in data
def test_logout(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试登出"""
user = User(
id=1,
company_id=1,
email="test@example.com",
hashed_password="x",
full_name="测试用户",
status=UserStatus.ACTIVE.value,
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
mock_db.execute = AsyncMock(
return_value=make_db_result(scalar=user)
)
response = client.post("/api/auth/logout", headers=auth_headers)
assert response.status_code == 200
+149
View File
@@ -0,0 +1,149 @@
"""
成本分析计算服务单元测试
测试 CostCalculatorService 和 CostSummary / DepartmentCost 的核心逻辑
"""
import pytest
from unittest.mock import AsyncMock, patch
from app.services.analysis.cost_calculator import (
CostCalculatorService,
CostSummary,
DepartmentCost,
)
@pytest.fixture
def mock_db():
"""模拟数据库会话"""
return AsyncMock()
@pytest.fixture
def cost_calculator(mock_db):
"""创建成本计算服务实例"""
return CostCalculatorService(mock_db)
class TestCostSummary:
"""成本汇总结果测试"""
def test_init_defaults(self):
"""测试默认值"""
summary = CostSummary()
assert summary.total_cost == 0.0
assert summary.salary_cost == 0.0
assert summary.social_security_cost == 0.0
assert summary.fund_cost == 0.0
assert summary.employee_count == 0
def test_init_with_values(self):
"""测试带值初始化"""
summary = CostSummary(
total_cost=50000,
salary_cost=30000,
social_security_cost=10000,
fund_cost=10000,
employee_count=10,
)
assert summary.total_cost == 50000
assert summary.salary_cost == 30000
assert summary.employee_count == 10
def test_to_dict(self):
"""测试转字典"""
summary = CostSummary(total_cost=10000, salary_cost=8000, employee_count=5)
d = summary.to_dict()
assert d["total_cost"] == 10000
assert d["salary_cost"] == 8000
assert d["employee_count"] == 5
class TestDepartmentCost:
"""部门成本测试"""
def test_init(self):
"""测试初始化"""
dept = DepartmentCost(department="技术部", employee_count=10, salary_cost=100000)
assert dept.department == "技术部"
assert dept.employee_count == 10
assert dept.salary_cost == 100000
def test_total_cost_property(self):
"""测试 total_cost 属性计算"""
dept = DepartmentCost(
department="财务部",
salary_cost=10000,
social_security_cost=3000,
fund_cost=1200,
)
assert dept.total_cost == 14200
def test_to_dict(self):
"""测试转字典"""
dept = DepartmentCost(department="技术部", salary_cost=10000, employee_count=5)
d = dept.to_dict()
assert d["department"] == "技术部"
assert d["total_cost"] == 10000
assert d["employee_count"] == 5
class TestCostCalculatorService:
"""成本计算服务测试"""
def test_init(self, mock_db):
"""测试初始化"""
service = CostCalculatorService(mock_db)
assert service.db == mock_db
@pytest.mark.asyncio
async def test_calculate_total_cost(self, cost_calculator):
"""测试计算总成本"""
mock_data = [
{"应发工资": 10000, "养老保险(公司)": 2000, "医疗保险(公司)": 1000, "失业保险(公司)": 500, "公积金(公司)": 1200},
{"应发工资": 8000, "养老保险(公司)": 1600, "医疗保险(公司)": 800, "失业保险(公司)": 400, "公积金(公司)": 960},
]
with patch.object(
cost_calculator, "_load_cleaned_data", return_value=mock_data
):
summary = await cost_calculator.calculate_total_cost(task_id=1)
assert summary.employee_count == 2
assert summary.salary_cost == 18000
assert summary.social_security_cost == 6300 # (2000+1000+500) + (1600+800+400)
assert summary.fund_cost == 2160 # 1200 + 960
assert summary.total_cost == 18000 + 6300 + 2160
@pytest.mark.asyncio
async def test_calculate_total_cost_empty(self, cost_calculator):
"""测试空数据计算"""
with patch.object(
cost_calculator, "_load_cleaned_data", return_value=[]
):
summary = await cost_calculator.calculate_total_cost(task_id=1)
assert summary.employee_count == 0
assert summary.total_cost == 0.0
@pytest.mark.asyncio
async def test_calculate_by_department(self, cost_calculator):
"""测试按部门汇总"""
mock_data = [
{"部门": "技术部", "应发工资": 10000, "养老保险(公司)": 2000, "公积金(公司)": 1200},
{"部门": "技术部", "应发工资": 8000, "养老保险(公司)": 1600, "公积金(公司)": 960},
{"部门": "财务部", "应发工资": 12000, "养老保险(公司)": 2400, "公积金(公司)": 1440},
]
with patch.object(
cost_calculator, "_load_cleaned_data", return_value=mock_data
):
departments = await cost_calculator.calculate_by_department(task_id=1)
assert len(departments) == 2
tech = [d for d in departments if d.department == "技术部"][0]
assert tech.employee_count == 2
assert tech.salary_cost == 18000
finance = [d for d in departments if d.department == "财务部"][0]
assert finance.employee_count == 1
assert finance.salary_cost == 12000
+110
View File
@@ -0,0 +1,110 @@
"""
对账任务 API 集成测试
测试任务列表、统计、详情等端点
使用同步 TestClient + Mock 数据库会话
"""
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock
from fastapi.testclient import TestClient
from app.models.reconciliation_task import ReconciliationTask
from tests.conftest import make_db_result
def _make_task(task_id=1, period="2026-07", status="COMPLETED",
total=100, matched=95, exceptions=5):
"""构造测试任务对象"""
return ReconciliationTask(
id=task_id,
company_id=1,
period=period,
status=status,
total_employees=total,
matched_count=matched,
exception_count=exceptions,
file_ids=[],
reconciliation_result={},
created_at=datetime.utcnow(),
updated_at=datetime.utcnow(),
)
class TestTasksAPI:
"""对账任务 API 集成测试"""
def test_list_tasks_empty(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试空任务列表"""
mock_db.execute = AsyncMock(return_value=make_db_result(items=[], scalar=0))
response = client.get("/api/tasks/", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["items"] == []
assert data["total"] == 0
def test_list_tasks_with_data(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试有数据的任务列表"""
task = _make_task()
# tasks.py 先执行 count 查询,再执行 list 查询
call_count = [0]
async def side_effect(*args, **kwargs):
call_count[0] += 1
if call_count[0] == 1:
return make_db_result(scalar=1) # count
return make_db_result(items=[task]) # list
mock_db.execute = AsyncMock(side_effect=side_effect)
response = client.get("/api/tasks/", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["total"] == 1
assert data["items"][0]["period"] == "2026-07"
assert data["items"][0]["status"] == "COMPLETED"
def test_list_tasks_pagination(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试分页"""
tasks = [_make_task(task_id=i, period=f"2025-{i:02d}") for i in range(1, 6)]
call_count = [0]
async def side_effect(*args, **kwargs):
call_count[0] += 1
if call_count[0] == 1:
return make_db_result(scalar=15)
return make_db_result(items=tasks)
mock_db.execute = AsyncMock(side_effect=side_effect)
response = client.get("/api/tasks/?page=1&page_size=5", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert len(data["items"]) == 5
assert data["total"] == 15
assert data["page"] == 1
def test_get_task_detail(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试获取任务详情"""
task = _make_task(task_id=42)
mock_db.get = AsyncMock(return_value=task)
response = client.get("/api/tasks/42", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert data["id"] == 42
assert data["period"] == "2026-07"
def test_get_task_not_found(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试获取不存在的任务"""
mock_db.get = AsyncMock(return_value=None)
response = client.get("/api/tasks/99999", headers=auth_headers)
assert response.status_code == 404
def test_list_tasks_no_company_header(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试无企业 ID 头的任务列表(company_id=None 时查不到数据)"""
mock_db.execute = AsyncMock(return_value=make_db_result(items=[], scalar=0))
response = client.get("/api/tasks/")
assert response.status_code == 200
data = response.json()
assert data["total"] == 0
+113
View File
@@ -0,0 +1,113 @@
"""
凭证生成引擎单元测试
测试 VoucherGeneratorService 的核心逻辑
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
from app.services.voucher.generator import VoucherGeneratorService, VoucherEntry
@pytest.fixture
def mock_db():
return AsyncMock()
@pytest.fixture
def generator(mock_db):
return VoucherGeneratorService(mock_db)
class TestVoucherEntry:
"""凭证分录测试"""
def test_entry_creation(self):
"""测试创建借方分录"""
entry = VoucherEntry(
account_code="6601.01",
account_name="管理费用-工资",
debit_amount=10000,
summary="基本工资",
)
assert entry.account_code == "6601.01"
assert entry.debit_amount == 10000
assert entry.credit_amount == 0
def test_entry_to_dict(self):
"""测试分录转字典"""
entry = VoucherEntry(
account_code="2211.01",
account_name="应付职工薪酬",
credit_amount=10000,
summary="基本工资",
department="技术部",
)
d = entry.to_dict()
assert d["account_code"] == "2211.01"
assert d["credit_amount"] == 10000
assert d["department"] == "技术部"
class TestVoucherGeneratorService:
"""凭证生成引擎测试"""
def test_aggregate_by_field(self, generator):
"""测试按字段汇总"""
records = [
{"基本工资": 10000, "奖金": 2000, "养老保险(公司)": 2000},
{"基本工资": 8000, "奖金": 1000, "养老保险(公司)": 1600},
]
totals = generator._aggregate_by_field(records)
assert totals["基本工资"] == 18000
assert totals["奖金"] == 3000
assert totals["养老保险(公司)"] == 3600
def test_aggregate_empty(self, generator):
"""测试空数据汇总"""
totals = generator._aggregate_by_field([])
assert totals == {}
def test_generate_entries_basic(self, generator):
"""测试基本分录生成"""
field_totals = {"基本工资": 18000}
mappings = {
"基本工资": {
"debit_account": "6601.01",
"debit_account_name": "管理费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
}
}
entries = generator._generate_entries(field_totals, mappings)
assert len(entries) >= 2
total_debit = sum(e.debit_amount for e in entries)
total_credit = sum(e.credit_amount for e in entries)
assert total_debit == 18000
assert total_credit == 18000
def test_generate_entries_skip_missing_mapping(self, generator):
"""测试跳过无映射的字段"""
field_totals = {"基本工资": 18000, "未知字段": 5000}
mappings = {
"基本工资": {
"debit_account": "6601.01",
"debit_account_name": "管理费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
}
}
entries = generator._generate_entries(field_totals, mappings)
total_debit = sum(e.debit_amount for e in entries)
assert total_debit == 18000
def test_merge_entries_same_account(self, generator):
"""测试合并相同科目"""
entries = [
VoucherEntry("6601.01", "管理费用-工资", debit_amount=10000, summary="基本工资"),
VoucherEntry("6601.01", "管理费用-工资", debit_amount=8000, summary="奖金"),
]
merged = generator._merge_entries(entries)
assert len(merged) == 1
assert merged[0].debit_amount == 18000
+65
View File
@@ -0,0 +1,65 @@
"""
凭证模板服务单元测试
测试 VoucherTemplate 的默认科目映射
"""
import pytest
from app.services.voucher.template import VoucherTemplate, DEFAULT_ACCOUNT_TEMPLATES
class TestVoucherTemplate:
"""凭证模板服务测试"""
def test_get_default_template_existing(self):
"""测试获取已存在的字段模板"""
template = VoucherTemplate.get_default_template("基本工资")
assert template is not None
assert template["debit_account"] == "6601.01"
assert template["credit_account"] == "2211.01"
assert "管理费用" in template["debit_account_name"]
assert "应付职工薪酬" in template["credit_account_name"]
def test_get_default_template_nonexistent(self):
"""测试获取不存在的字段模板"""
template = VoucherTemplate.get_default_template("不存在的字段")
assert template == {}
def test_get_all_templates(self):
"""测试获取所有模板"""
templates = VoucherTemplate.get_all_templates()
assert len(templates) > 0
assert "基本工资" in templates
assert "养老保险(公司)" in templates
assert "公积金(公司)" in templates
assert "实发工资" in templates
def test_get_template_fields(self):
"""测试获取模板字段列表"""
fields = VoucherTemplate.get_template_fields()
assert len(fields) > 0
assert "基本工资" in fields
assert isinstance(fields, list)
def test_template_structure(self):
"""测试模板结构完整性"""
for field, template in DEFAULT_ACCOUNT_TEMPLATES.items():
assert "debit_account" in template, f"字段 {field} 缺少 debit_account"
assert "debit_account_name" in template, f"字段 {field} 缺少 debit_account_name"
assert "credit_account" in template, f"字段 {field} 缺少 credit_account"
assert "credit_account_name" in template, f"字段 {field} 缺少 credit_account_name"
def test_social_security_templates(self):
"""测试社保相关模板"""
for field in ["养老保险(公司)", "医疗保险(公司)", "失业保险(公司)"]:
template = VoucherTemplate.get_default_template(field)
assert template != {}
assert "社保" in template["debit_account_name"]
def test_fund_template(self):
"""测试公积金模板"""
template = VoucherTemplate.get_default_template("公积金(公司)")
assert template != {}
assert "公积金" in template["debit_account_name"]
assert "公积金" in template["credit_account_name"]
+178
View File
@@ -0,0 +1,178 @@
"""
凭证 API 集成测试
测试科目映射 CRUD、凭证模板等端点
使用同步 TestClient + Mock 数据库会话
"""
from unittest.mock import AsyncMock, MagicMock
from fastapi.testclient import TestClient
from app.models.account_mapping import AccountMapping
from tests.conftest import make_db_result
def _make_mapping(mapping_id=1, field="基本工资", debit="6601.01", credit="2211.01"):
"""构造测试科目映射对象"""
return AccountMapping(
id=mapping_id,
company_id=1,
standard_field=field,
debit_account=debit,
debit_account_name=f"管理费用-{field}",
credit_account=credit,
credit_account_name="应付职工薪酬",
cost_center=None,
is_active=True,
)
class TestVoucherTemplatesAPI:
"""凭证模板 API 测试"""
def test_get_templates(self, client: TestClient, auth_headers: dict):
"""测试获取默认凭证模板"""
response = client.get("/api/vouchers/templates/list", headers=auth_headers)
assert response.status_code == 200
data = response.json()
assert "基本工资" in data
assert "养老保险(公司)" in data
def test_get_templates_no_auth(self, client: TestClient):
"""测试无认证访问模板(该端点不需要认证)"""
response = client.get("/api/vouchers/templates/list")
assert response.status_code == 200
class TestAccountMappingsAPI:
"""科目映射 CRUD API 测试"""
def test_list_mappings_empty(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试空映射列表"""
mock_db.execute = AsyncMock(return_value=make_db_result(items=[]))
response = client.get(
"/api/vouchers/account-mappings/list", headers=auth_headers
)
assert response.status_code == 200
assert response.json() == []
def test_create_mapping(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试创建科目映射"""
def refresh_side_effect(obj, *args, **kwargs):
obj.id = 1
obj.is_active = True
mock_db.refresh.side_effect = refresh_side_effect
response = client.post(
"/api/vouchers/account-mappings",
headers=auth_headers,
json={
"standard_field": "基本工资",
"debit_account": "6601.01",
"debit_account_name": "管理费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
},
)
assert response.status_code == 200
data = response.json()
assert data["standard_field"] == "基本工资"
assert data["debit_account"] == "6601.01"
assert data["is_active"] is True
def test_create_and_list_mappings(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试创建后查询映射列表"""
mappings = [_make_mapping(1, "基本工资"), _make_mapping(2, "奖金", "6601.02")]
mock_db.execute = AsyncMock(return_value=make_db_result(items=mappings))
response = client.get(
"/api/vouchers/account-mappings/list", headers=auth_headers
)
assert response.status_code == 200
data = response.json()
assert len(data) == 2
def test_update_mapping(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试更新科目映射"""
existing = _make_mapping(1)
mock_db.get = AsyncMock(return_value=existing)
def refresh_side_effect(obj, *args, **kwargs):
pass
mock_db.refresh.side_effect = refresh_side_effect
response = client.put(
"/api/vouchers/account-mappings/1",
headers=auth_headers,
json={
"standard_field": "基本工资",
"debit_account": "6601.03",
"debit_account_name": "销售费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
},
)
assert response.status_code == 200
data = response.json()
assert data["debit_account"] == "6601.03"
def test_update_mapping_not_found(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试更新不存在的映射"""
mock_db.get = AsyncMock(return_value=None)
response = client.put(
"/api/vouchers/account-mappings/99999",
headers=auth_headers,
json={
"standard_field": "基本工资",
"debit_account": "6601.01",
"debit_account_name": "管理费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
},
)
assert response.status_code == 404
def test_delete_mapping(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试删除科目映射"""
existing = _make_mapping(1, "奖金")
mock_db.get = AsyncMock(return_value=existing)
response = client.delete(
"/api/vouchers/account-mappings/1", headers=auth_headers
)
assert response.status_code == 200
def test_delete_mapping_not_found(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试删除不存在的映射"""
mock_db.get = AsyncMock(return_value=None)
response = client.delete(
"/api/vouchers/account-mappings/99999", headers=auth_headers
)
assert response.status_code == 404
def test_create_mapping_with_cost_center(self, client: TestClient, auth_headers: dict, mock_db: AsyncMock):
"""测试带成本中心创建映射"""
def refresh_side_effect(obj, *args, **kwargs):
obj.id = 1
obj.is_active = True
mock_db.refresh.side_effect = refresh_side_effect
response = client.post(
"/api/vouchers/account-mappings",
headers=auth_headers,
json={
"standard_field": "基本工资",
"debit_account": "6601.01",
"debit_account_name": "管理费用-工资",
"credit_account": "2211.01",
"credit_account_name": "应付职工薪酬-工资",
"cost_center": "技术部",
},
)
assert response.status_code == 200
data = response.json()
assert data["cost_center"] == "技术部"