| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750 |
- from io import BytesIO
- import json
- import shutil
- import subprocess
- import tempfile
- import unittest
- from pathlib import Path
- from unittest.mock import patch
- from docx import Document
- from fastapi.testclient import TestClient
- import pymupdf
- import api_worker
- import http_api
- def _write_docx(path: Path, text: str = "ok") -> None:
- path.parent.mkdir(parents=True, exist_ok=True)
- doc = Document()
- doc.add_paragraph(text)
- doc.save(path)
- def _docx_bytes(text: str = "document") -> bytes:
- stream = BytesIO()
- doc = Document()
- doc.add_paragraph(text)
- doc.save(stream)
- return stream.getvalue()
- def _pdf_bytes(text: str = "tender text") -> bytes:
- document = pymupdf.open()
- page = document.new_page()
- if text:
- page.insert_text((72, 72), text)
- content = document.tobytes()
- document.close()
- return content
- def _valid_files(tender_name: str = "招标文件.pdf") -> dict:
- return {
- "TENDER_FILE": (tender_name, _pdf_bytes(), "application/pdf"),
- "PROCUREMENT_FILE": (
- "采购需求.docx",
- _docx_bytes("procurement"),
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ),
- "REFERENCE_BID": (
- "参考投书.docx",
- _docx_bytes("reference"),
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- ),
- }
- def _write_json_input_files(root: Path):
- tender = root / "招标文件.pdf"
- procurement = root / "采购需求.docx"
- reference = root / "参考投书.docx"
- tender.write_bytes(_pdf_bytes())
- procurement.write_bytes(_docx_bytes("procurement"))
- reference.write_bytes(_docx_bytes("reference"))
- return tender, procurement, reference
- class RecordingExecutor:
- def __init__(self):
- self.submitted = []
- def submit(self, function, *args):
- self.submitted.append((function, args))
- return object()
- class ApiWorkerTests(unittest.TestCase):
- def test_worker_runs_test_step1_through_test_step6_with_fixed_local_assets(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- tender = root / "tender.pdf"
- procurement = root / "procurement.docx"
- reference = root / "reference.docx"
- step6 = root / "step6.docx"
- for path in (tender, procurement, reference):
- path.write_bytes(b"input")
- calls = []
- def fake_run(command, **kwargs):
- calls.append((command, kwargs))
- script_name = Path(command[1]).name
- if script_name == "test_step6.py":
- _write_docx(step6, "step6")
- return subprocess.CompletedProcess(command, 0)
- with patch("api_worker.subprocess.run", side_effect=fake_run):
- result = api_worker.run_test_pipeline(
- tender, procurement, reference, step6
- )
- self.assertEqual(result, step6)
- self.assertEqual(
- [Path(call[0][1]).name for call in calls],
- [
- "test_step1.py",
- "test_step2.py",
- "test_step3.py",
- "test_step4.py",
- "test_step5.py",
- "test_step6.py",
- ],
- )
- env = calls[0][1]["env"]
- self.assertEqual(
- Path(env["PROPOSA_COMPANY_INFO_DIR"]),
- api_worker.COMPANY_INFO_DIR.resolve(),
- )
- self.assertEqual(
- Path(env["PROPOSA_TEMPLATE_PATH"]),
- api_worker.TEMPLATE_PATH.resolve(),
- )
- self.assertEqual(
- Path(env["PROPOSA_STEP1_ITEMS_FILE"]),
- (step6.parent / "step1_extracted_items.pkl").resolve(),
- )
- progress_path = step6.parent / api_worker.PROGRESS_FILE_NAME
- self.assertTrue(progress_path.is_file())
- progress = json.loads(progress_path.read_text(encoding="utf-8"))
- self.assertEqual(progress["current_step"], 6)
- self.assertEqual(progress["status"], "completed")
- self.assertEqual(progress["completed_steps"], [1, 2, 3, 4, 5, 6])
- class FinalReviewApiTests(unittest.TestCase):
- def setUp(self):
- with http_api._JOBS_LOCK:
- http_api._JOBS.clear()
- def _post(
- self,
- files=None,
- callback_url="127.0.0.1:9999/callback",
- txb_id="dms-tender-001",
- ):
- return TestClient(http_api.app).post(
- "/api/v1/final-review",
- data={"CALLBACK_URL": callback_url, "txbId": txb_id},
- files=files or _valid_files(),
- )
- def test_valid_request_returns_202_before_pipeline_runs(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- work_root = root / "jobs"
- executor = RecordingExecutor()
- with patch.object(http_api, "API_WORK_ROOT", work_root), patch.object(
- http_api, "API_WORK_SETTING", "output/api_jobs"
- ), patch.object(http_api, "_JOB_EXECUTOR", executor), patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = self._post(
- _valid_files("上海市群众艺术馆物业管理服务采购项目招标文件.pdf")
- )
- self.assertEqual(response.status_code, 202)
- body = response.json()
- self.assertEqual(body["status"], "processing")
- self.assertEqual(body["txbId"], "dms-tender-001")
- self.assertEqual(body["message"], "三份文件校验通过,已开始处理")
- self.assertEqual(
- body["resultPath"],
- f"output/api_jobs/{body['request_id']}/"
- "上海市群众艺术馆物业管理服务采购项目招标文件.docx",
- )
- self.assertEqual(
- body["output_path"],
- f"output/api_jobs/{body['request_id']}/"
- "上海市群众艺术馆物业管理服务采购项目招标文件.docx",
- )
- self.assertEqual(len(executor.submitted), 1)
- worker.assert_not_called()
- shutil.rmtree(work_root, ignore_errors=True)
- def test_background_success_publishes_file_and_sends_callback(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- executor = RecordingExecutor()
- callback_payloads = []
- def fake_worker(_t, _p, _r, step6, _log):
- _write_docx(step6, "final review")
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api, "_run_pipeline_worker", side_effect=fake_worker
- ), patch.object(
- http_api,
- "_send_callback",
- side_effect=lambda _url, payload: callback_payloads.append(payload),
- ):
- response = self._post(_valid_files("项目A.pdf"))
- function, args = executor.submitted[0]
- job = args[0]
- function(*args)
- request_id = response.json()["request_id"]
- status = TestClient(http_api.app).get(
- f"/api/v1/jobs/{request_id}"
- )
- download = TestClient(http_api.app).get(
- f"/api/v1/jobs/{request_id}/file"
- )
- self.assertEqual(status.status_code, 200)
- self.assertEqual(status.json()["status"], "completed")
- self.assertTrue(status.json()["callback_delivered"])
- self.assertEqual(callback_payloads[0]["status"], "completed")
- self.assertEqual(callback_payloads[0]["txbId"], "dms-tender-001")
- self.assertEqual(
- callback_payloads[0]["files"]["TENDER_FILE"]["filename"],
- "项目A.pdf",
- )
- self.assertTrue(
- callback_payloads[0]["resultPath"].endswith(
- "项目A.docx"
- )
- )
- self.assertEqual(job["final_output"].parent.name, request_id)
- self.assertNotIn("final_review_path", callback_payloads[0])
- self.assertEqual(download.status_code, 200)
- self.assertTrue(download.content.startswith(b"PK"))
- def test_progress_endpoint_returns_step_progress(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- executor = RecordingExecutor()
- def fake_worker(_t, _p, _r, step6, _log):
- _write_docx(step6, "final review")
- progress_path = step6.parent / api_worker.PROGRESS_FILE_NAME
- progress_path.write_text(
- json.dumps(
- {
- "current_step": 6,
- "total_steps": 6,
- "status": "completed",
- "message": "全部步骤处理完成",
- "completed_steps": [1, 2, 3, 4, 5, 6],
- },
- ensure_ascii=False,
- ),
- encoding="utf-8",
- )
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api, "_run_pipeline_worker", side_effect=fake_worker
- ), patch.object(
- http_api,
- "_send_callback",
- side_effect=lambda _url, payload: None,
- ):
- response = self._post(_valid_files("项目A.pdf"))
- request_id = response.json()["request_id"]
- function, args = executor.submitted[0]
- function(*args)
- progress_response = TestClient(http_api.app).get(
- f"/api/v1/jobs/{request_id}/progress"
- )
- self.assertEqual(response.status_code, 202)
- self.assertEqual(progress_response.status_code, 200)
- self.assertEqual(progress_response.json()["current_step"], 6)
- self.assertEqual(progress_response.json()["status"], "completed")
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_optional_cla_file_is_recorded_and_does_not_affect_worker_submission(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- executor = RecordingExecutor()
- files = _valid_files()
- files["CLA_FILE"] = (
- "澄清公告.docx",
- _docx_bytes("clarification"),
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
- )
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = self._post(files=files)
- function, args = executor.submitted[0]
- job = args[0]
- self.assertEqual(response.status_code, 202)
- self.assertEqual(job["files"]["CLA_FILE"]["filename"], "澄清公告.docx")
- self.assertGreater(job["files"]["CLA_FILE"]["size"], 0)
- worker.assert_not_called()
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_json_body_accepts_server_local_paths_and_records_files(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- tender, procurement, reference = _write_json_input_files(root)
- executor = RecordingExecutor()
- payload = {
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": 1,
- "TENDER_FILE": str(tender),
- "PROCUREMENT_FILE": str(procurement),
- "TPC_IS_ABSOLUTE": True,
- "REFERENCE_BID": str(reference),
- "REFERENCE_IS_ABSOLUTE": True,
- "CLA_FILE": "",
- }
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json=payload,
- )
- function, args = executor.submitted[0]
- job = args[0]
- self.assertEqual(response.status_code, 202)
- self.assertEqual(job["txbId"], "1")
- self.assertEqual(job["files"]["TENDER_FILE"]["filename"], "招标文件.pdf")
- self.assertEqual(job["files"]["PROCUREMENT_FILE"]["filename"], "采购需求.docx")
- self.assertEqual(job["files"]["REFERENCE_BID"]["filename"], "参考投书.docx")
- self.assertNotIn("CLA_FILE", job["files"])
- worker.assert_not_called()
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_json_relative_reference_bid_joins_reference_dir(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- reference_dir = root / "reference_root"
- reference_dir.mkdir()
- reference_file = reference_dir / "参考投书.docx"
- reference_file.write_bytes(_docx_bytes("reference"))
- tender = root / "招标文件.pdf"
- procurement = root / "采购需求.docx"
- tender.write_bytes(_pdf_bytes())
- procurement.write_bytes(_docx_bytes("procurement"))
- executor = RecordingExecutor()
- payload = {
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": 1,
- "TENDER_FILE": str(tender),
- "PROCUREMENT_FILE": str(procurement),
- "TPC_IS_ABSOLUTE": True,
- "REFERENCE_BID": "/参考投书.docx",
- "REFERENCE_IS_ABSOLUTE": False,
- "CLA_FILE": "",
- }
- with patch.object(http_api, "REFERENCE_DIR", str(reference_dir)), patch.object(
- http_api, "API_WORK_ROOT", root / "jobs"
- ), patch.object(http_api, "_JOB_EXECUTOR", executor), patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json=payload,
- )
- function, args = executor.submitted[0]
- job = args[0]
- self.assertEqual(response.status_code, 202)
- self.assertEqual(job["files"]["REFERENCE_BID"]["filename"], "参考投书.docx")
- worker.assert_not_called()
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_json_relative_tpc_files_join_tpc_dir(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- tpc_dir = root / "tpc_root"
- tpc_dir.mkdir()
- tender = tpc_dir / "招标文件.pdf"
- procurement = tpc_dir / "采购需求.docx"
- clarification = tpc_dir / "澄清公告.docx"
- reference = root / "参考投书.docx"
- tender.write_bytes(_pdf_bytes())
- procurement.write_bytes(_docx_bytes("procurement"))
- clarification.write_bytes(_docx_bytes("clarification"))
- reference.write_bytes(_docx_bytes("reference"))
- executor = RecordingExecutor()
- payload = {
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": 1,
- "TENDER_FILE": f"/{tender.name}",
- "PROCUREMENT_FILE": f"/{procurement.name}",
- "REFERENCE_BID": str(reference),
- "REFERENCE_IS_ABSOLUTE": True,
- "CLA_FILE": f"/{clarification.name}",
- "TPC_IS_ABSOLUTE": False,
- }
- with patch.object(http_api, "TPC_DIR", str(tpc_dir)), patch.object(
- http_api, "API_WORK_ROOT", root / "jobs"
- ), patch.object(http_api, "_JOB_EXECUTOR", executor), patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json=payload,
- )
- function, args = executor.submitted[0]
- job = args[0]
- self.assertEqual(response.status_code, 202)
- self.assertEqual(job["files"]["TENDER_FILE"]["filename"], tender.name)
- self.assertEqual(
- job["files"]["PROCUREMENT_FILE"]["filename"], procurement.name
- )
- self.assertEqual(
- job["files"]["CLA_FILE"]["filename"], clarification.name
- )
- worker.assert_not_called()
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_invalid_tpc_is_absolute_value_is_rejected(self):
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "bad-tpc",
- "TENDER_FILE": "tender.pdf",
- "PROCUREMENT_FILE": "procurement.docx",
- "REFERENCE_BID": "reference.docx",
- "TPC_IS_ABSOLUTE": "false",
- "REFERENCE_IS_ABSOLUTE": True,
- },
- )
- self.assertEqual(response.status_code, 400)
- self.assertEqual(
- response.json()["detail"],
- "TPC_IS_ABSOLUTE 必须是布尔值 true/false",
- )
- executor.submit.assert_not_called()
- def test_relative_tpc_files_require_tpc_dir(self):
- with patch.object(http_api, "TPC_DIR", ""), patch.object(
- http_api, "_JOB_EXECUTOR"
- ) as executor:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "missing-tpc-dir",
- "TENDER_FILE": "tender.pdf",
- "PROCUREMENT_FILE": "procurement.docx",
- "REFERENCE_BID": "reference.docx",
- "TPC_IS_ABSOLUTE": False,
- "REFERENCE_IS_ABSOLUTE": True,
- },
- )
- self.assertEqual(response.status_code, 400)
- self.assertEqual(
- response.json()["detail"],
- "TENDER_FILE 为相对路径,但未配置 TPC_DIR",
- )
- executor.submit.assert_not_called()
- def test_invalid_reference_is_absolute_value_is_rejected(self):
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "bad-ref",
- "TENDER_FILE": "tender.pdf",
- "PROCUREMENT_FILE": "procurement.docx",
- "TPC_IS_ABSOLUTE": True,
- "REFERENCE_BID": "reference.docx",
- "REFERENCE_IS_ABSOLUTE": "maybe",
- },
- )
- self.assertEqual(response.status_code, 400)
- executor.submit.assert_not_called()
- def test_missing_absolute_flags_are_rejected(self):
- base_payload = {
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "missing-flags",
- "TENDER_FILE": "tender.pdf",
- "PROCUREMENT_FILE": "procurement.docx",
- "REFERENCE_BID": "reference.docx",
- }
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- missing_tpc = TestClient(http_api.app).post(
- "/api/v1/final-review", json=base_payload
- )
- missing_reference = TestClient(http_api.app).post(
- "/api/v1/final-review",
- json={**base_payload, "TPC_IS_ABSOLUTE": True},
- )
- self.assertEqual(missing_tpc.status_code, 400)
- self.assertEqual(
- missing_tpc.json()["detail"],
- "TPC_IS_ABSOLUTE 必须是布尔值 true/false",
- )
- self.assertEqual(missing_reference.status_code, 400)
- self.assertEqual(
- missing_reference.json()["detail"],
- "REFERENCE_IS_ABSOLUTE 必须是布尔值 true/false",
- )
- executor.submit.assert_not_called()
- def test_multipart_accepts_reference_is_absolute_without_path_join(self):
- with tempfile.TemporaryDirectory() as temp_dir, patch.object(
- http_api, "API_WORK_ROOT", Path(temp_dir)
- ), patch.object(http_api, "_JOB_EXECUTOR") as executor, patch.object(
- http_api, "_run_pipeline_worker"
- ) as worker:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- data={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "multipart-ref",
- "REFERENCE_IS_ABSOLUTE": "false",
- },
- files=_valid_files(),
- )
- self.assertEqual(response.status_code, 202)
- executor.submit.assert_called_once()
- worker.assert_not_called()
- shutil.rmtree(Path(temp_dir), ignore_errors=True)
- def test_clean_intermediate_false_keeps_job_directory(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- executor = RecordingExecutor()
- def fake_worker(_t, _p, _r, step6, _log):
- _write_docx(step6, "final review")
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api, "_run_pipeline_worker", side_effect=fake_worker
- ), patch.object(
- http_api,
- "_send_callback",
- side_effect=lambda _url, payload: None,
- ):
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- data={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "keep-001",
- "CLEAN_INTERMEDIATE": "false",
- },
- files=_valid_files("项目A.pdf"),
- )
- function, args = executor.submitted[0]
- job = args[0]
- function(*args)
- self.assertEqual(response.status_code, 202)
- self.assertIn("intermediate_dir", response.json())
- self.assertTrue(Path(job["job_dir"]).exists())
- status = http_api._job_snapshot(response.json()["request_id"])
- self.assertEqual(status["intermediate_dir"], str(job["job_dir"]))
- shutil.rmtree(root / "jobs", ignore_errors=True)
- def test_invalid_clean_intermediate_value_is_rejected(self):
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- response = TestClient(http_api.app).post(
- "/api/v1/final-review",
- data={
- "CALLBACK_URL": "http://127.0.0.1:9999/callback",
- "txbId": "bad-001",
- "CLEAN_INTERMEDIATE": "maybe",
- },
- files=_valid_files(),
- )
- self.assertEqual(response.status_code, 400)
- executor.submit.assert_not_called()
- def test_background_failure_sends_error_callback(self):
- with tempfile.TemporaryDirectory() as temp_dir:
- root = Path(temp_dir)
- executor = RecordingExecutor()
- callback_payloads = []
- with patch.object(http_api, "API_WORK_ROOT", root / "jobs"), patch.object(
- http_api, "_JOB_EXECUTOR", executor
- ), patch.object(
- http_api,
- "_run_pipeline_worker",
- side_effect=subprocess.CalledProcessError(7, ["worker"]),
- ), patch.object(
- http_api,
- "_send_callback",
- side_effect=lambda _url, payload: callback_payloads.append(payload),
- ):
- response = self._post()
- function, args = executor.submitted[0]
- function(*args)
- request_id = response.json()["request_id"]
- status = http_api._job_snapshot(request_id)
- self.assertEqual(status["status"], "failed")
- self.assertTrue(status["callback_delivered"])
- self.assertEqual(callback_payloads[0]["status"], "failed")
- self.assertEqual(callback_payloads[0]["txbId"], "dms-tender-001")
- self.assertEqual(
- callback_payloads[0]["resultPath"],
- "error: Step1-6 处理进程异常退出(退出码 7)",
- )
- self.assertIn("退出码 7", callback_payloads[0]["error"])
- def test_callback_ip_without_scheme_defaults_to_http(self):
- self.assertEqual(
- http_api._normalize_callback_url("127.0.0.1:9000/callback"),
- "http://127.0.0.1:9000/callback",
- )
- def test_callback_url_with_scheme_but_missing_slashes_is_normalized(self):
- self.assertEqual(
- http_api._normalize_callback_url(
- "http:121.43.55.7:10026/shenqin/tender/callback"
- ),
- "http://121.43.55.7:10026/shenqin/tender/callback",
- )
- def test_callback_posts_json_without_authorization_token(self):
- captured = {}
- class Response:
- status = 200
- def __enter__(self):
- return self
- def __exit__(self, *_args):
- return False
- def fake_urlopen(request, timeout):
- captured["request"] = request
- captured["timeout"] = timeout
- return Response()
- payload = {
- "txbId": "dms-tender-001",
- "resultPath": "output/result.docx",
- }
- with patch("http_api.urlopen", side_effect=fake_urlopen):
- http_api._send_callback(
- "http://121.43.55.7:10026/shenqin/tender/callback", payload
- )
- request = captured["request"]
- self.assertEqual(json.loads(request.data.decode("utf-8")), payload)
- self.assertNotIn(
- "authorization", {key.lower() for key, _value in request.header_items()}
- )
- def test_txb_id_is_required_and_cannot_be_blank(self):
- client = TestClient(http_api.app)
- missing = client.post(
- "/api/v1/final-review",
- data={"CALLBACK_URL": "127.0.0.1:9999/callback"},
- files=_valid_files(),
- )
- blank = self._post(txb_id=" ")
- self.assertEqual(missing.status_code, 422)
- self.assertEqual(blank.status_code, 400)
- self.assertEqual(blank.json()["detail"], "txbId 不能为空")
- def test_invalid_callback_is_rejected_before_job_submission(self):
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- response = self._post(callback_url="ftp://127.0.0.1/callback")
- self.assertEqual(response.status_code, 400)
- executor.submit.assert_not_called()
- def test_missing_required_upload_is_rejected(self):
- response = self._post(
- files={
- "TENDER_FILE": ("tender.pdf", _pdf_bytes()),
- "REFERENCE_BID": ("reference.docx", _docx_bytes()),
- }
- )
- self.assertEqual(response.status_code, 422)
- def test_invalid_extension_is_rejected_before_job_submission(self):
- files = _valid_files()
- files["TENDER_FILE"] = ("tender.exe", b"bad")
- with patch.object(http_api, "_JOB_EXECUTOR") as executor:
- response = self._post(files)
- self.assertEqual(response.status_code, 400)
- executor.submit.assert_not_called()
- def test_procurement_and_reference_bid_must_be_docx(self):
- for field_name in ("PROCUREMENT_FILE", "REFERENCE_BID"):
- files = _valid_files()
- files[field_name] = ("wrong.pdf", _pdf_bytes())
- with self.subTest(field_name=field_name), patch.object(
- http_api, "_JOB_EXECUTOR"
- ) as executor:
- response = self._post(files)
- self.assertEqual(response.status_code, 400)
- self.assertIn("只能上传 .DOCX", response.json()["detail"])
- executor.submit.assert_not_called()
- def test_empty_tender_is_rejected(self):
- with tempfile.TemporaryDirectory() as temp_dir, patch.object(
- http_api, "API_WORK_ROOT", Path(temp_dir)
- ), patch.object(http_api, "_JOB_EXECUTOR") as executor:
- files = _valid_files()
- files["TENDER_FILE"] = ("tender.pdf", b"")
- response = self._post(files)
- self.assertEqual(response.status_code, 400)
- self.assertEqual(response.json()["detail"], "TENDER_FILE 不能为空")
- executor.submit.assert_not_called()
- self.assertEqual(list(Path(temp_dir).iterdir()), [])
- def test_scanned_pdf_without_extractable_text_is_rejected(self):
- with tempfile.TemporaryDirectory() as temp_dir, patch.object(
- http_api, "API_WORK_ROOT", Path(temp_dir)
- ), patch.object(http_api, "_JOB_EXECUTOR") as executor:
- files = _valid_files()
- files["TENDER_FILE"] = ("tender.pdf", _pdf_bytes(text=""))
- response = self._post(files)
- self.assertEqual(response.status_code, 400)
- self.assertIn("文字类 PDF", response.json()["detail"])
- executor.submit.assert_not_called()
- def test_docx_extension_with_invalid_content_is_rejected(self):
- with tempfile.TemporaryDirectory() as temp_dir, patch.object(
- http_api, "API_WORK_ROOT", Path(temp_dir)
- ), patch.object(http_api, "_JOB_EXECUTOR") as executor:
- files = _valid_files()
- files["PROCUREMENT_FILE"] = ("procurement.docx", b"not a docx")
- response = self._post(files)
- self.assertEqual(response.status_code, 400)
- self.assertIn("不是有效的 DOCX", response.json()["detail"])
- executor.submit.assert_not_called()
- if __name__ == "__main__":
- unittest.main()
|