214 lines
8.6 KiB
Python
214 lines
8.6 KiB
Python
"""
|
||
职级服务单元测试(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"]
|