签入版本
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
"""
|
||||
职级服务单元测试(Excel 导入/导出复用 excel_utils)
|
||||
=============================================================
|
||||
|
||||
验证:
|
||||
- export_levels 支持与分页一致的筛选参数(name 模糊、status 精确)
|
||||
- 无筛选时导出全部记录;表头与数据列对齐
|
||||
- import_levels 上传/解析/入库/清理(临时文件生命周期)
|
||||
- 上传失败与文件缺失时透传错误
|
||||
|
||||
文件落盘通过 monkeypatch excel_utils.save_export_file 跳过,
|
||||
仅验证查询、列构建与写盘调用契约。
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from src.common.utils import excel_utils
|
||||
from src.core.exceptions import BizError
|
||||
from src.modules.system.level import service as level_service
|
||||
from src.modules.system.level.repository import LevelRepository
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def seed_levels(app):
|
||||
"""插入职级测试数据(sqlite 内存库)"""
|
||||
from src.core.database import db
|
||||
|
||||
with app.app_context():
|
||||
LevelRepository.create(name="初级工程师", status=1, sort=1)
|
||||
LevelRepository.create(name="中级工程师", status=1, sort=2)
|
||||
LevelRepository.create(name="高级工程师", status=2, sort=3)
|
||||
LevelRepository.create(name="产品经理", status=1, sort=4)
|
||||
db.session.commit()
|
||||
|
||||
|
||||
def _run_export(app, monkeypatch, params):
|
||||
"""调用导出并捕获表头/行数据(跳过文件落盘,仅验证查询与列构建)"""
|
||||
captured = {}
|
||||
|
||||
def fake_save(headers, rows, filename_prefix="export"):
|
||||
captured["headers"] = headers
|
||||
captured["rows"] = rows
|
||||
return "/static/temp/level_xxx.xlsx"
|
||||
|
||||
monkeypatch.setattr(excel_utils, "save_export_file", fake_save)
|
||||
|
||||
with app.app_context():
|
||||
result = level_service.export_levels(params)
|
||||
url = result.get_json()["data"]
|
||||
|
||||
return captured, url
|
||||
|
||||
|
||||
class TestExportLevels:
|
||||
"""Excel 导出:筛选参数 + 列对齐"""
|
||||
|
||||
HEADER = ["职级ID", "职级名称", "职级排序", "职级状态", "创建时间", "更新时间"]
|
||||
|
||||
def test_export_no_filter_exports_all(self, app, monkeypatch, seed_levels):
|
||||
captured, url = _run_export(app, monkeypatch, None)
|
||||
assert captured["headers"] == self.HEADER
|
||||
assert [r[1] for r in captured["rows"]] == ["初级工程师", "中级工程师", "高级工程师", "产品经理"]
|
||||
assert url.startswith("/static")
|
||||
|
||||
def test_export_filters_by_name(self, app, monkeypatch, seed_levels):
|
||||
captured, _ = _run_export(app, monkeypatch, {"name": "工程师"})
|
||||
assert captured["headers"] == self.HEADER
|
||||
assert [r[1] for r in captured["rows"]] == ["初级工程师", "中级工程师", "高级工程师"]
|
||||
|
||||
def test_export_filters_by_status(self, app, monkeypatch, seed_levels):
|
||||
captured, _ = _run_export(app, monkeypatch, {"status": 2})
|
||||
assert [r[1] for r in captured["rows"]] == ["高级工程师"]
|
||||
|
||||
def test_export_filters_by_name_and_status(self, app, monkeypatch, seed_levels):
|
||||
captured, _ = _run_export(app, monkeypatch, {"name": "工程师", "status": 1})
|
||||
assert [r[1] for r in captured["rows"]] == ["初级工程师", "中级工程师"]
|
||||
|
||||
def test_export_columns_aligned(self, app, monkeypatch, seed_levels):
|
||||
captured, _ = _run_export(app, monkeypatch, {"name": "产品经理"})
|
||||
assert captured["headers"] == self.HEADER
|
||||
row = captured["rows"][0]
|
||||
assert row[0] is not None # 职级ID
|
||||
assert row[1] == "产品经理" # 职级名称
|
||||
assert row[2] == 4 # 职级排序
|
||||
assert row[3] == "正常" # 职级状态(formatter 展示为中文)
|
||||
assert row[4] # 创建时间
|
||||
assert row[5] # 更新时间
|
||||
|
||||
def test_export_empty_result_keeps_header(self, app, monkeypatch, seed_levels):
|
||||
captured, _ = _run_export(app, monkeypatch, {"name": "不存在的职级"})
|
||||
assert captured["headers"] == self.HEADER
|
||||
assert captured["rows"] == []
|
||||
|
||||
|
||||
class TestLevelList:
|
||||
"""职级列表(无分页):参数条件筛选 + status=1 硬性过滤"""
|
||||
|
||||
def test_list_no_filter_returns_enabled_only(self, app, seed_levels):
|
||||
with app.app_context():
|
||||
data = level_service.get_level_list(None)
|
||||
assert [d["name"] for d in data] == ["初级工程师", "中级工程师", "产品经理"]
|
||||
|
||||
def test_list_filters_by_name(self, app, seed_levels):
|
||||
with app.app_context():
|
||||
data = level_service.get_level_list({"name": "工程师"})
|
||||
assert [d["name"] for d in data] == ["初级工程师", "中级工程师"]
|
||||
|
||||
def test_list_status_param_is_ignored(self, app, seed_levels):
|
||||
# status 为硬性条件,即使传入 status=2 也只返回启用(status=1)记录
|
||||
with app.app_context():
|
||||
data = level_service.get_level_list({"name": "工程师", "status": 2})
|
||||
assert [d["name"] for d in data] == ["初级工程师", "中级工程师"]
|
||||
|
||||
|
||||
class TestImportLevels:
|
||||
"""职级Excel导入(复用 excel_utils 上传/解析/清理机制)"""
|
||||
|
||||
def _patch_upload(self, monkeypatch, tmp_path, file_name="levels.xlsx"):
|
||||
"""将上传模拟指向 tmp_path/temp/20260817 目录,返回该文件路径"""
|
||||
target_dir = tmp_path / "temp" / "20260817"
|
||||
target_dir.mkdir(parents=True, exist_ok=True)
|
||||
target = target_dir / file_name
|
||||
monkeypatch.setattr(
|
||||
excel_utils.upload, "upload_file",
|
||||
lambda *a, **k: {"filePath": "/temp/20260817/" + file_name},
|
||||
)
|
||||
monkeypatch.setattr(excel_utils, "FLASK_UPLOAD_DIR", str(tmp_path))
|
||||
return target
|
||||
|
||||
def test_import_success(self, app, monkeypatch, tmp_path):
|
||||
import shutil
|
||||
|
||||
from openpyxl.workbook import Workbook
|
||||
|
||||
from src.modules.system.level.models import Level
|
||||
|
||||
# 构造真实 xlsx 并放入模拟的上传目录(temp/日期/文件)
|
||||
src = tmp_path / "levels.xlsx"
|
||||
wb = Workbook()
|
||||
ws = wb.worksheets[0]
|
||||
ws.append(["职级ID", "职级名称", "职级排序", "职级状态"])
|
||||
ws.append([None, "初级", 1, "正常"])
|
||||
ws.append([None, "停用级", 2, "禁用"])
|
||||
ws.append([None, None, None, None]) # 整行为空 → 跳过
|
||||
wb.save(src)
|
||||
|
||||
target = self._patch_upload(monkeypatch, tmp_path)
|
||||
shutil.copy(src, target)
|
||||
|
||||
with app.app_context():
|
||||
result = level_service.import_levels()
|
||||
rows = Level.query.order_by(Level.sort).all()
|
||||
|
||||
assert result.get_json()["code"] == 0
|
||||
assert [r.name for r in rows] == ["初级", "停用级"]
|
||||
assert rows[0].status == 1 # "正常" → 1
|
||||
assert rows[1].status == 2 # "禁用" → 2
|
||||
assert not target.exists() # 临时导入文件已清理
|
||||
|
||||
def test_import_required_missing_fails_whole_import(self, app, monkeypatch, tmp_path):
|
||||
import shutil
|
||||
|
||||
from openpyxl.workbook import Workbook
|
||||
|
||||
from src.modules.system.level.models import Level
|
||||
|
||||
# 名称必填缺失 → 行级错误 → 整体失败,无任何数据入库
|
||||
src = tmp_path / "levels.xlsx"
|
||||
wb = Workbook()
|
||||
ws = wb.worksheets[0]
|
||||
ws.append(["职级ID", "职级名称", "职级排序", "职级状态"])
|
||||
ws.append([None, "", 1, "正常"])
|
||||
wb.save(src)
|
||||
|
||||
target = self._patch_upload(monkeypatch, tmp_path)
|
||||
shutil.copy(src, target)
|
||||
|
||||
with app.app_context():
|
||||
result = level_service.import_levels()
|
||||
count = Level.query.count()
|
||||
|
||||
body = result.get_json()
|
||||
assert body["code"] == 1
|
||||
assert "职级名称不能为空" in body["msg"]
|
||||
assert count == 0
|
||||
|
||||
def test_import_upload_failure_propagates(self, app, monkeypatch):
|
||||
# 上传失败由 upload_file 抛 BizError,import_levels 捕获后透传具体错误信息
|
||||
def _fail_upload(*args, **kwargs):
|
||||
raise BizError("文件格式不支持,仅支持 .xls, .xlsx 格式")
|
||||
|
||||
monkeypatch.setattr(excel_utils.upload, "upload_file", _fail_upload)
|
||||
|
||||
with app.app_context():
|
||||
result = level_service.import_levels()
|
||||
|
||||
body = result.get_json()
|
||||
assert body["code"] == 1
|
||||
assert "文件格式不支持" in body["msg"]
|
||||
|
||||
def test_import_file_missing_returns_failed(self, app, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
excel_utils.upload, "upload_file",
|
||||
lambda *a, **k: {"filePath": "/temp/20260817/nonexist.xlsx"},
|
||||
)
|
||||
monkeypatch.setattr(excel_utils, "FLASK_UPLOAD_DIR", "nonexistent_dir")
|
||||
|
||||
with app.app_context():
|
||||
result = level_service.import_levels()
|
||||
|
||||
body = result.get_json()
|
||||
assert body["code"] == 1
|
||||
assert "导入文件不存在" in body["msg"]
|
||||
Reference in New Issue
Block a user