Files
certificate-system/backend/tests/test_import_rules.py
2026-08-14 12:13:13 +08:00

185 lines
6.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import json
from datetime import date, datetime
import pytest
from openpyxl import load_workbook
from pydantic import ValidationError
from sqlalchemy import create_engine
from sqlalchemy.orm import Session
from app.api.routes import admin_imports
from app.api.routes.admin_imports import (
COL_ISSUE_DATE,
COL_COURSE_END_DATE,
COL_COURSE_NAME,
COL_COURSE_START_DATE,
COL_NAME,
COL_PHONE,
COL_PROJECT,
COL_STAGE_NAME,
TEMPLATE_HEADERS,
build_import_template_workbook,
date_is_valid,
detect_template_from_workbook,
normalize_row_data,
parse_issue_date,
row_errors,
write_error_report,
)
from app.db.base import Base
from app.models import ImportBatch, ImportBatchRow
from app.schemas.certificate import CertificateCreate
def test_parse_issue_date_accepts_common_formats():
assert parse_issue_date("2026-06-01") == date(2026, 6, 1)
assert parse_issue_date("2026/06/01") == date(2026, 6, 1)
assert parse_issue_date("2026-06-01 00:00:00") == date(2026, 6, 1)
assert parse_issue_date("2026-06-01T08:30:00") == date(2026, 6, 1)
def test_normalize_row_data_removes_time_from_excel_dates():
row = normalize_row_data(
{
COL_NAME: "张三",
COL_COURSE_START_DATE: datetime(2026, 6, 1, 0, 0, 0),
COL_ISSUE_DATE: date(2026, 7, 5),
}
)
assert row[COL_COURSE_START_DATE] == "2026-06-01"
assert row[COL_ISSUE_DATE] == "2026-07-05"
def test_detect_template_from_workbook_uses_excel_headers(tmp_path):
source_path = tmp_path / "practice-camp.xlsx"
build_import_template_workbook("practice-camp").save(source_path)
detected = detect_template_from_workbook(source_path)
assert detected is not None
assert detected.code == "practice-camp"
def test_row_errors_require_project_code_to_exist():
row = {
COL_NAME: "张三",
COL_PHONE: "13800000000",
COL_PROJECT: "BAD",
COL_COURSE_START_DATE: "2026-05-01",
COL_COURSE_END_DATE: "2026-05-31",
COL_ISSUE_DATE: "2026-06-01",
}
assert "项目代码不存在或已停用" in row_errors(row, {"DBY"})
def test_date_is_valid_rejects_bad_text():
assert not date_is_valid("not-a-date")
def test_row_errors_rejects_reversed_course_period():
row = {
COL_NAME: "张三",
COL_PHONE: "13800000000",
COL_PROJECT: "DBY",
COL_COURSE_START_DATE: "2026-06-30",
COL_COURSE_END_DATE: "2026-06-01",
COL_ISSUE_DATE: "2026-07-05",
}
assert "课程结束日期不能早于课程开始日期" in row_errors(row, {"DBY"})
def test_row_errors_explains_required_date_format():
row = {
COL_NAME: "张三",
COL_PHONE: "13800000000",
COL_PROJECT: "DBY",
COL_COURSE_START_DATE: "2026年6月1日",
COL_COURSE_END_DATE: "2026-06-30",
COL_ISSUE_DATE: "2026-07-05",
}
assert "课程开始日期格式错误请使用YYYY-MM-DD例如2026-06-01" in row_errors(row, {"DBY"})
def test_import_template_contains_date_examples_and_validation():
workbook = build_import_template_workbook()
data_sheet = workbook["证书导入模板"]
instruction_sheet = workbook["填写说明"]
assert [cell.value for cell in data_sheet[1]] == TEMPLATE_HEADERS
assert data_sheet.column_dimensions["F"].number_format == "yyyy-mm-dd"
assert len(data_sheet.data_validations.dataValidation) == 3
instruction_rows = {row[0].value: row for row in instruction_sheet.iter_rows(min_row=2)}
assert instruction_rows[COL_COURSE_START_DATE][2].value == "2026-06-01"
assert "实际开始日期" in instruction_rows[COL_COURSE_START_DATE][3].value
assert COL_COURSE_NAME in TEMPLATE_HEADERS
assert COL_STAGE_NAME in TEMPLATE_HEADERS
def test_error_report_flushes_pending_rows_and_exports_reason(tmp_path, monkeypatch):
engine = create_engine("sqlite+pysqlite:///:memory:")
Base.metadata.create_all(engine)
def temporary_data_path(name: str):
folder = tmp_path / name
folder.mkdir(parents=True, exist_ok=True)
return folder
monkeypatch.setattr(admin_imports, "data_path", temporary_data_path)
with Session(engine) as db:
batch = ImportBatch(filename="bad.xlsx", file_path="/tmp/bad.xlsx", template_code="practice-camp")
db.add(batch)
db.flush()
db.add(
ImportBatchRow(
batch_id=batch.id,
row_no=2,
status="failed",
error_message="发证日期格式错误",
raw_json=json.dumps(
{
COL_NAME: "张三",
COL_PHONE: "13800000000",
COL_PROJECT: "DBY",
COL_COURSE_START_DATE: "2026-06-01 00:00:00",
COL_ISSUE_DATE: "错误日期",
},
ensure_ascii=False,
),
)
)
report_path = write_error_report(db, batch.id)
rows = list(load_workbook(report_path, data_only=True).active.iter_rows(values_only=True))
assert rows[1][0] == 2
assert rows[1][1] == "发证日期格式错误"
assert rows[1][2] == "张三"
assert rows[1][5] == "2026-06-01"
assert rows[1][-1] == "错误日期"
def test_certificate_create_rejects_reversed_course_period():
with pytest.raises(ValidationError, match="课程结束日期不能早于课程开始日期"):
CertificateCreate(
learner_name="张三",
learner_phone="13800000000",
project_code="DBY",
course_start_date="2026-06-30",
course_end_date="2026-06-01",
issue_date="2026-07-05",
)
def test_certificate_create_has_no_issuer_field():
certificate = CertificateCreate(
learner_name="张三",
learner_phone="13800000000",
project_code="DBY",
course_start_date="2026-06-01",
course_end_date="2026-06-30",
issue_date="2026-07-05",
)
assert "issuer_name" not in certificate.model_dump()