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, 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_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()