test_dms_production.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. """Unit tests for template-driven DMS lifecycle extraction."""
  2. from __future__ import annotations
  3. import json
  4. import tempfile
  5. import unittest
  6. from pathlib import Path
  7. from urllib.parse import parse_qs, urlparse
  8. from urllib.request import Request
  9. from openpyxl import Workbook, load_workbook
  10. from step0_pre_prepare import DmsAuthSettings
  11. from step1_data_aggregation.dms_client import DmsClient, DmsReadError
  12. from step1_data_aggregation.dms_pipeline import DATA_SHEET, _excel_value, build_production_data
  13. from step1_data_aggregation.template_mapping import (
  14. DMS_FIELD,
  15. DMS_MODEL,
  16. MAPPING_SHEET,
  17. TEMPLATE_FIELD,
  18. load_template,
  19. )
  20. class StaticTokenManager:
  21. def call_with_refresh(self, operation):
  22. return operation("test-token")
  23. class FakeDmsTransport:
  24. def __init__(self) -> None:
  25. self.content_requests: list[dict[str, list[str]]] = []
  26. def __call__(self, request: Request, timeout: float) -> bytes:
  27. path = urlparse(request.full_url).path
  28. if path == "/static/config/config.js":
  29. return b'window.config={dmsDataProxy:"/proxy_dms"}'
  30. if path == "/proxy_dms/column/getColumnList":
  31. return self.response(
  32. {
  33. "code": 200,
  34. "content": [
  35. {
  36. "title": "Group",
  37. "columnList": [
  38. {
  39. "id": 11,
  40. "title": "Model",
  41. "modelId": 22,
  42. "modelName": "model_table",
  43. "tag": "model_tag",
  44. "type": 1,
  45. }
  46. ],
  47. },
  48. {
  49. "title": "Other",
  50. "columnList": [
  51. {
  52. "id": 33,
  53. "title": "Model",
  54. "modelId": 44,
  55. "modelName": "other_model",
  56. "type": 1,
  57. }
  58. ],
  59. },
  60. ],
  61. }
  62. )
  63. if path == "/proxy_dms/model/getModelById":
  64. model_id = parse_qs(urlparse(request.full_url).query)["modelId"]
  65. if model_id != ["22"]:
  66. return self.response({"code": 500, "message": "unexpected model"})
  67. return self.response(
  68. {
  69. "code": 200,
  70. "content": {
  71. "fieldList": json.dumps(
  72. {
  73. "c_code": {"name": "c_code"},
  74. "c_name": {"name": "c_name"},
  75. }
  76. )
  77. },
  78. }
  79. )
  80. if path == "/proxy_dms/content/selectContentList":
  81. form = parse_qs((request.data or b"").decode("utf-8"))
  82. self.content_requests.append(form)
  83. page = int(form["page"][0])
  84. state = int(form["states"][0])
  85. if state == 0:
  86. data = (
  87. [
  88. {"c_code": "P001", "c_name": "Alpha"},
  89. {"c_code": "P002", "c_name": "=not-a-formula"},
  90. ]
  91. if page == 0
  92. else [{"c_code": "P003", "c_name": "Gamma"}]
  93. )
  94. count = 3
  95. elif state == 1:
  96. return self.response({"code": 202, "message": "data does not exist"})
  97. else:
  98. data = [{"c_code": f"P00{state + 3}", "c_name": f"State-{state}"}]
  99. count = 1
  100. return self.response({"code": 200, "content": {"count": count, "data": data}})
  101. raise AssertionError(f"unexpected request: {request.full_url}")
  102. @staticmethod
  103. def response(payload: dict[str, object]) -> bytes:
  104. return json.dumps(payload).encode("utf-8")
  105. def make_template(path: Path) -> None:
  106. workbook = Workbook()
  107. sheet = workbook.active
  108. sheet.title = MAPPING_SHEET
  109. sheet.append([TEMPLATE_FIELD, DMS_MODEL, DMS_FIELD])
  110. sheet.append(["Code", "Group-Model", "c_code"])
  111. sheet.append(["Name", "Group-Model", "c_name"])
  112. workbook.save(path)
  113. workbook.close()
  114. class TemplateMappingTests(unittest.TestCase):
  115. def test_loads_mapping_rows_in_order(self) -> None:
  116. with tempfile.TemporaryDirectory() as raw_dir:
  117. path = Path(raw_dir) / "project.xlsx"
  118. make_template(path)
  119. template = load_template(path)
  120. self.assertEqual(template.name, "project")
  121. self.assertEqual(template.model_references, ("Group-Model",))
  122. self.assertEqual(
  123. [field.template_field for field in template.fields],
  124. ["Code", "Name"],
  125. )
  126. class DmsClientTests(unittest.TestCase):
  127. def settings(self) -> DmsAuthSettings:
  128. return DmsAuthSettings(
  129. dms_base_url="https://dms.example/dms",
  130. oauth_api_url="https://oauth.example/oauth",
  131. )
  132. def test_timestamp_is_exported_as_shanghai_time_text(self) -> None:
  133. self.assertEqual(
  134. _excel_value(1769616000000, "timestamp"),
  135. "2026-01-29 00:00:00",
  136. )
  137. def test_discovers_proxy_resolves_exact_path_and_reads_all_states(self) -> None:
  138. transport = FakeDmsTransport()
  139. client = DmsClient(
  140. self.settings(),
  141. token_manager=StaticTokenManager(),
  142. transport=transport,
  143. )
  144. column = client.resolve_columns(["Group-Model"])["Group-Model"]
  145. self.assertEqual((column.column_id, column.model_id), (11, 22))
  146. self.assertEqual(client.get_model_fields(22), {"c_code", "c_name"})
  147. rows = client.fetch_rows(column, page_size=2)
  148. self.assertEqual([row["c_code"] for row in rows], ["P001", "P002", "P003", "P005", "P006"])
  149. self.assertEqual(len(transport.content_requests), 5)
  150. self.assertTrue(
  151. [request["states"][0] for request in transport.content_requests] == ["0", "0", "1", "2", "3"]
  152. )
  153. def test_rejects_ambiguous_leaf_title(self) -> None:
  154. client = DmsClient(
  155. self.settings(),
  156. token_manager=StaticTokenManager(),
  157. transport=FakeDmsTransport(),
  158. )
  159. with self.assertRaisesRegex(DmsReadError, "ambiguous"):
  160. client.resolve_columns(["Model"])
  161. class ProductionPipelineTests(unittest.TestCase):
  162. def test_builds_valid_workbook_and_report(self) -> None:
  163. with tempfile.TemporaryDirectory() as raw_dir:
  164. root = Path(raw_dir)
  165. template_dir = root / "templates"
  166. output_dir = root / "production"
  167. template_dir.mkdir()
  168. make_template(template_dir / "project.xlsx")
  169. transport = FakeDmsTransport()
  170. client = DmsClient(
  171. DmsClientTests().settings(),
  172. token_manager=StaticTokenManager(),
  173. transport=transport,
  174. )
  175. results = build_production_data(
  176. client,
  177. template_dir=template_dir,
  178. output_dir=output_dir,
  179. page_size=2,
  180. )
  181. self.assertEqual(results[0].content_rows, 5)
  182. self.assertEqual(results[0].state_rows, {0: 3, 1: 0, 2: 1, 3: 1})
  183. output_path = output_dir / "project.xlsx"
  184. workbook = load_workbook(output_path, data_only=False)
  185. try:
  186. self.assertEqual(workbook.sheetnames, [DATA_SHEET])
  187. sheet = workbook[DATA_SHEET]
  188. self.assertEqual(
  189. [cell.value for cell in sheet[1]],
  190. ["Code", "Name"],
  191. )
  192. self.assertEqual(sheet.max_row, 6)
  193. self.assertEqual(sheet["B3"].value, "=not-a-formula")
  194. self.assertEqual(sheet["B3"].data_type, "s")
  195. finally:
  196. workbook.close()
  197. report = json.loads(
  198. (output_dir / "fetch_report.json").read_text(encoding="utf-8")
  199. )
  200. self.assertEqual(report["content_states"], [0, 1, 2, 3])
  201. self.assertEqual(report["templates"][0]["state_rows"], {"0": 3, "1": 0, "2": 1, "3": 1})
  202. self.assertEqual(report["templates"][0]["content_rows"], 5)
  203. if __name__ == "__main__":
  204. unittest.main()