test_data_rule_api.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741
  1. from __future__ import annotations
  2. from datetime import UTC, datetime, timedelta
  3. import pytest
  4. from app.core.common.identifiers import new_governance_uid
  5. from app.core.data_rules.contracts import rule_spec_hash
  6. from app.core.system.tokens import decode_access_token, issue_access_token
  7. from tests.core.data_rules.test_contracts import (
  8. valid_dataflow_spec,
  9. valid_rule_spec,
  10. )
  11. from tests.core.data_rules.test_production_line import (
  12. assertion_only_rule,
  13. published_rule,
  14. published_standard,
  15. )
  16. class FakeAuthoringAgent:
  17. def __init__(self):
  18. self.calls = []
  19. def interpret(self, **kwargs):
  20. self.calls.append(kwargs)
  21. return {
  22. "status": "ready",
  23. "source_text": kwargs["source_text"],
  24. "candidate_hash": "a" * 64,
  25. "context_hash": "b" * 64,
  26. "candidate": {
  27. "candidate_type": "rule",
  28. "rule_spec": valid_rule_spec(),
  29. },
  30. }
  31. class FakeRuleRepository:
  32. def __init__(self):
  33. self.calls = []
  34. def create_rule_version(self, **kwargs):
  35. self.calls.append(("create_rule_version", kwargs))
  36. return {
  37. "id": new_governance_uid(),
  38. "rule_uid": kwargs["rule_spec"]["rule_uid"],
  39. "version_no": 1,
  40. "status": "draft",
  41. "spec_hash": rule_spec_hash(kwargs["rule_spec"]),
  42. "created": True,
  43. }
  44. def record_generation_run(self, **kwargs):
  45. self.calls.append(("record_generation_run", kwargs))
  46. return {
  47. "id": new_governance_uid(),
  48. "correlation_id": new_governance_uid(),
  49. "decision": kwargs["evidence"]["status"],
  50. }
  51. def resolve_validation_context(self, context):
  52. self.calls.append(("resolve_validation_context", {"context": context}))
  53. return {
  54. "input_schema_snapshot_id": new_governance_uid(),
  55. "input_schema_hash": "c" * 64,
  56. "input_fields": [{"name": "mobile", "type": "string"}],
  57. "output_schema_snapshot_id": new_governance_uid(),
  58. "output_schema_hash": "d" * 64,
  59. "output_fields": [{"name": "mobile", "type": "string"}],
  60. "input_sample_artifact_ref": "minio://trusted/input.parquet",
  61. "input_sample_artifact_digest": "e" * 64,
  62. "golden_output_artifact_ref": None,
  63. "golden_output_artifact_digest": None,
  64. }
  65. def publish_rule_version(self, **kwargs):
  66. self.calls.append(("publish_rule_version", kwargs))
  67. return {
  68. "id": kwargs["version_id"],
  69. "rule_uid": new_governance_uid(),
  70. "version_no": 1,
  71. "status": "published",
  72. "spec_hash": "a" * 64,
  73. }
  74. def create_standard_version(self, **kwargs):
  75. self.calls.append(("create_standard_version", kwargs))
  76. return {
  77. "id": new_governance_uid(),
  78. "standard_uid": kwargs["standard_spec"]["standard_uid"],
  79. "version_no": 1,
  80. "status": "validated",
  81. "spec_hash": "b" * 64,
  82. "created": True,
  83. }
  84. def publish_standard_version(self, **kwargs):
  85. self.calls.append(("publish_standard_version", kwargs))
  86. return {
  87. "id": kwargs["version_id"],
  88. "standard_uid": new_governance_uid(),
  89. "version_no": 1,
  90. "status": "published",
  91. "spec_hash": "b" * 64,
  92. }
  93. class FakeReleaseService:
  94. def __init__(self):
  95. self.calls = []
  96. def release(self, **kwargs):
  97. self.calls.append(kwargs)
  98. return {
  99. "id": new_governance_uid(),
  100. "version_no": 1,
  101. "status": "released",
  102. "package_hash": "c" * 64,
  103. "package": {
  104. "package_hash": "c" * 64,
  105. "standard_version_ids": [],
  106. "rule_version_ids": [],
  107. },
  108. }
  109. class FakePublicationService:
  110. def __init__(self, repository):
  111. self.repository = repository
  112. self.plan_id = new_governance_uid()
  113. self.version_id = None
  114. def create_draft(self, **kwargs):
  115. self.repository.calls.append(("create_rule_version", kwargs))
  116. self.version_id = new_governance_uid()
  117. return {
  118. "id": self.version_id,
  119. "rule_uid": kwargs["rule_spec"]["rule_uid"],
  120. "version_no": 1,
  121. "status": "draft",
  122. "spec_hash": rule_spec_hash(kwargs["rule_spec"]),
  123. "generation_run_id": new_governance_uid(),
  124. "created": True,
  125. }
  126. def validate(self, version_id, actor_uid):
  127. self.repository.calls.append(
  128. ("validate_rule_version", {"version_id": version_id, "actor_uid": actor_uid})
  129. )
  130. return {
  131. "version_id": version_id,
  132. "version_status": "draft",
  133. "plan_id": self.plan_id,
  134. "plan_status": "compiled",
  135. "plan_hash": "d" * 64,
  136. }
  137. def test(self, version_id, actor_uid, *, plan_id):
  138. self.repository.calls.append(
  139. (
  140. "test_rule_version",
  141. {
  142. "version_id": version_id,
  143. "actor_uid": actor_uid,
  144. "plan_id": plan_id,
  145. },
  146. )
  147. )
  148. return {
  149. "version_id": version_id,
  150. "version_status": "validated",
  151. "plan_id": plan_id,
  152. "plan_status": "tested",
  153. "plan_hash": "d" * 64,
  154. }
  155. def publish(self, version_id, actor_uid):
  156. self.repository.calls.append(
  157. ("publish_rule_version", {"version_id": version_id, "actor_uid": actor_uid})
  158. )
  159. return {
  160. "id": version_id,
  161. "status": "published",
  162. "plan_id": self.plan_id,
  163. "plan_status": "published",
  164. }
  165. def evidence(self, version_id):
  166. return {"version_id": version_id, "version_status": "validated"}
  167. def catalog(self, *, query, limit):
  168. return []
  169. class FakeGraphSession:
  170. def __init__(self):
  171. self.calls = []
  172. def run(self, query, parameters):
  173. self.calls.append((query, parameters))
  174. return [
  175. {
  176. "domain_id": 9,
  177. "domain_key": "customer_raw",
  178. "revision": "v2",
  179. "field_name": "customer_id",
  180. "data_type": "string",
  181. "nullable": False,
  182. "precision": None,
  183. "scale": None,
  184. "timezone": None,
  185. }
  186. ]
  187. def __enter__(self):
  188. return self
  189. def __exit__(self, *_args):
  190. return False
  191. class FakeGraphDriver:
  192. def __init__(self):
  193. self.session = FakeGraphSession()
  194. def get_session(self):
  195. return self.session
  196. class SnapshotOnlyRepository:
  197. def __init__(self):
  198. self.snapshots = {}
  199. def find_schema_snapshot(self, *, schema_ref, schema_hash):
  200. return self.snapshots.get((schema_ref, schema_hash))
  201. def persist_schema_snapshot(self, *, snapshot):
  202. value = {"id": new_governance_uid(), **snapshot}
  203. self.snapshots[(snapshot["schema_ref"], snapshot["schema_hash"])] = value
  204. return value
  205. def _headers(app, role):
  206. token = issue_access_token(
  207. user_id=new_governance_uid(),
  208. roles=[role],
  209. secret=app.config["SECRET_KEY"],
  210. now=datetime.now(UTC),
  211. lifetime=timedelta(minutes=10),
  212. )
  213. return {"Authorization": f"Bearer {token}"}
  214. def _use_token_identity(monkeypatch):
  215. def load(token, *, secret):
  216. claims = decode_access_token(token, secret=secret)
  217. return {
  218. "id": claims["sub"],
  219. "username": "contract-test",
  220. "display_name": "Contract Test",
  221. "roles": claims["roles"],
  222. }
  223. monkeypatch.setattr(
  224. "app.core.system.auth.load_identity_from_token",
  225. load,
  226. )
  227. def test_rule_capabilities_and_validation_are_registered_and_governed(monkeypatch):
  228. from app import create_app
  229. app = create_app()
  230. _use_token_identity(monkeypatch)
  231. app.config["TESTING"] = True
  232. app.config["RULE_GENERATION_RECEIPT_SECRET"] = (
  233. "dedicated-test-receipt-secret-with-entropy"
  234. )
  235. client = app.test_client()
  236. response = client.get("/api/rules/capabilities", headers=_headers(app, "viewer"))
  237. assert response.status_code == 200
  238. capabilities = response.get_json()["data"]
  239. assert capabilities["natural_language_authoring"] is True
  240. assert capabilities["immutable_asset_versions"] is True
  241. assert capabilities["server_side_publishing"] is True
  242. assert capabilities["production_line_release"] is True
  243. assert capabilities["data_factory_activation"] is False
  244. spec = valid_rule_spec()
  245. response = client.post(
  246. "/api/rules/validate",
  247. json={"asset_type": "rule", "spec": spec},
  248. headers=_headers(app, "editor"),
  249. )
  250. assert response.status_code == 200
  251. result = response.get_json()["data"]
  252. assert result["spec_hash"] == rule_spec_hash(spec)
  253. assert result["normalized"]["rule_uid"] == spec["rule_uid"]
  254. forbidden = client.post(
  255. "/api/rules/validate",
  256. json={"asset_type": "rule", "spec": spec},
  257. headers=_headers(app, "viewer"),
  258. )
  259. assert forbidden.status_code == 403
  260. def test_rule_interpret_uses_configured_agent_and_preserves_surface(monkeypatch):
  261. from app import create_app
  262. app = create_app()
  263. _use_token_identity(monkeypatch)
  264. app.config["TESTING"] = True
  265. app.config["RULE_GENERATION_RECEIPT_SECRET"] = (
  266. "dedicated-test-receipt-secret-with-entropy"
  267. )
  268. agent = FakeAuthoringAgent()
  269. app.extensions["data_rule_authoring_agent"] = agent
  270. repository = FakeRuleRepository()
  271. app.extensions["data_rule_repository"] = repository
  272. client = app.test_client()
  273. response = client.post(
  274. "/api/rules/interpret",
  275. json={
  276. "source_text": "手机号去空格后必须为11位数字",
  277. "authoring_surface": "data_standard",
  278. "context": {
  279. "input_schema_snapshot_id": new_governance_uid(),
  280. "output_schema_snapshot_id": new_governance_uid(),
  281. "input_sample_artifact_ref": (
  282. "minio://trusted/input.parquet"
  283. ),
  284. "golden_output_artifact_ref": None,
  285. },
  286. },
  287. headers=_headers(app, "editor"),
  288. )
  289. assert response.status_code == 200
  290. assert response.get_json()["data"]["status"] == "ready"
  291. assert response.get_json()["data"]["generation_run_id"]
  292. assert response.get_json()["data"]["generation_receipt"]
  293. assert agent.calls[0]["authoring_surface"] == "data_standard"
  294. assert repository.calls[0][0] == "resolve_validation_context"
  295. assert repository.calls[1][0] == "record_generation_run"
  296. def test_rule_interpret_and_validate_reject_unknown_fields(monkeypatch):
  297. from app import create_app
  298. app = create_app()
  299. _use_token_identity(monkeypatch)
  300. app.config["TESTING"] = True
  301. app.config["RULE_GENERATION_RECEIPT_SECRET"] = (
  302. "dedicated-test-receipt-secret-with-entropy"
  303. )
  304. repository = FakeRuleRepository()
  305. app.extensions["data_rule_repository"] = repository
  306. app.extensions["data_rule_authoring_agent"] = FakeAuthoringAgent()
  307. client = app.test_client()
  308. headers = _headers(app, "editor")
  309. interpreted = client.post(
  310. "/api/rules/interpret",
  311. json={
  312. "source_text": "手机号必须为11位数字",
  313. "authoring_surface": "data_standard",
  314. "context": {},
  315. "status": "published",
  316. },
  317. headers=headers,
  318. )
  319. validated = client.post(
  320. "/api/rules/validate",
  321. json={
  322. "asset_type": "rule",
  323. "spec": valid_rule_spec(),
  324. "evidence": {"status": "success"},
  325. },
  326. headers=headers,
  327. )
  328. assert interpreted.status_code == 400
  329. assert validated.status_code == 400
  330. assert repository.calls == []
  331. def test_published_rule_catalog_uses_canonical_closed_contract(monkeypatch):
  332. from app import create_app
  333. app = create_app()
  334. _use_token_identity(monkeypatch)
  335. app.config["TESTING"] = True
  336. repository = FakeRuleRepository()
  337. app.extensions["rule_publication_service"] = FakePublicationService(
  338. repository
  339. )
  340. client = app.test_client()
  341. headers = _headers(app, "viewer")
  342. response = client.get(
  343. "/api/rules/catalog?query=mobile&limit=10",
  344. headers=headers,
  345. )
  346. rejected = client.get(
  347. "/api/rules/catalog?query=mobile&status=published",
  348. headers=headers,
  349. )
  350. assert response.status_code == 200
  351. assert response.get_json()["data"] == {"items": []}
  352. assert rejected.status_code == 400
  353. def test_production_line_resolve_preview_expands_standard_without_writing(monkeypatch):
  354. from app import create_app
  355. app = create_app()
  356. _use_token_identity(monkeypatch)
  357. app.config["TESTING"] = True
  358. client = app.test_client()
  359. standard_id = new_governance_uid()
  360. standard_rule_id = new_governance_uid()
  361. direct_rule_id = new_governance_uid()
  362. standard_rule = published_rule(standard_rule_id, assertion_only_rule())
  363. direct_rule = published_rule(direct_rule_id)
  364. response = client.post(
  365. "/api/rules/production-lines/resolve",
  366. json={
  367. "dataflow_spec": valid_dataflow_spec(standard_id, direct_rule_id),
  368. "standard_versions": {
  369. standard_id: published_standard(standard_id, standard_rule_id)
  370. },
  371. "rule_versions": {
  372. standard_rule_id: standard_rule,
  373. direct_rule_id: direct_rule,
  374. },
  375. "component_binding_ids": {
  376. "normalize_customer": new_governance_uid(),
  377. "customer_standard:mobile_format": new_governance_uid(),
  378. },
  379. },
  380. headers=_headers(app, "editor"),
  381. )
  382. assert response.status_code == 200
  383. result = response.get_json()["data"]
  384. assert result["preview"] is True
  385. assert result["release_ready"] is False
  386. assert result["package"]["package_hash"]
  387. assert result["package"]["standard_version_ids"] == [standard_id]
  388. def test_rule_api_rejects_invalid_or_unauthenticated_requests(monkeypatch):
  389. from app import create_app
  390. app = create_app()
  391. _use_token_identity(monkeypatch)
  392. app.config["TESTING"] = True
  393. client = app.test_client()
  394. assert client.get("/api/rules/capabilities").status_code == 401
  395. response = client.post(
  396. "/api/rules/validate",
  397. json={"asset_type": "rule", "spec": {"schema_version": "1.0"}},
  398. headers=_headers(app, "editor"),
  399. )
  400. assert response.status_code == 400
  401. assert "missing" not in str(response.get_json()).lower()
  402. def test_rule_and_standard_versions_are_created_then_published_by_separate_roles(
  403. monkeypatch,
  404. ):
  405. from app import create_app
  406. from tests.core.data_rules.test_contracts import valid_standard_spec
  407. app = create_app()
  408. _use_token_identity(monkeypatch)
  409. app.config["TESTING"] = True
  410. repository = FakeRuleRepository()
  411. app.extensions["data_rule_repository"] = repository
  412. publication = FakePublicationService(repository)
  413. app.extensions["rule_publication_service"] = publication
  414. client = app.test_client()
  415. rule_spec = valid_rule_spec()
  416. created = client.post(
  417. "/api/rules/rule-versions",
  418. json={
  419. "source_text": "手机号必须为11位数字",
  420. "rule_spec": rule_spec,
  421. "category": "standard_clause",
  422. "generation_receipt": "signed-test-receipt",
  423. },
  424. headers=_headers(app, "editor"),
  425. )
  426. assert created.status_code == 201
  427. assert created.get_json()["data"]["status"] == "draft"
  428. rule_version_id = created.get_json()["data"]["id"]
  429. compiled = client.post(
  430. f"/api/rules/rule-versions/{rule_version_id}/validate",
  431. headers=_headers(app, "editor"),
  432. )
  433. assert compiled.status_code == 200
  434. plan_id = compiled.get_json()["data"]["plan_id"]
  435. tested = client.post(
  436. f"/api/rules/rule-versions/{rule_version_id}/test",
  437. json={"plan_id": plan_id},
  438. headers=_headers(app, "editor"),
  439. )
  440. assert tested.status_code == 200
  441. assert tested.get_json()["data"]["version_status"] == "validated"
  442. forbidden = client.post(
  443. f"/api/rules/rule-versions/{rule_version_id}/publish",
  444. headers=_headers(app, "editor"),
  445. )
  446. assert forbidden.status_code == 403
  447. published = client.post(
  448. f"/api/rules/rule-versions/{rule_version_id}/publish",
  449. headers=_headers(app, "admin"),
  450. )
  451. assert published.status_code == 200
  452. assert published.get_json()["data"]["status"] == "published"
  453. standard_spec = valid_standard_spec(rule_version_id)
  454. standard = client.post(
  455. "/api/rules/standard-versions",
  456. json={
  457. "source_text": "客户手机号遵循统一格式",
  458. "standard_spec": standard_spec,
  459. },
  460. headers=_headers(app, "editor"),
  461. )
  462. assert standard.status_code == 201
  463. standard_version_id = standard.get_json()["data"]["id"]
  464. standard_published = client.post(
  465. f"/api/rules/standard-versions/{standard_version_id}/publish",
  466. headers=_headers(app, "admin"),
  467. )
  468. assert standard_published.status_code == 200
  469. assert standard_published.get_json()["data"]["status"] == "published"
  470. methods = [method for method, _kwargs in repository.calls]
  471. assert methods == [
  472. "create_rule_version",
  473. "validate_rule_version",
  474. "test_rule_version",
  475. "publish_rule_version",
  476. "create_standard_version",
  477. "publish_standard_version",
  478. ]
  479. def test_create_version_rejects_client_selected_lifecycle_status(monkeypatch):
  480. from app import create_app
  481. app = create_app()
  482. _use_token_identity(monkeypatch)
  483. app.config["TESTING"] = True
  484. repository = FakeRuleRepository()
  485. app.extensions["data_rule_repository"] = repository
  486. app.extensions["rule_publication_service"] = FakePublicationService(repository)
  487. client = app.test_client()
  488. response = client.post(
  489. "/api/rules/rule-versions",
  490. json={
  491. "source_text": "手机号必须为11位数字",
  492. "rule_spec": valid_rule_spec(),
  493. "generation_receipt": "signed-test-receipt",
  494. "status": "published",
  495. },
  496. headers=_headers(app, "editor"),
  497. )
  498. assert response.status_code == 400
  499. def test_generation_receipt_signer_requires_dedicated_secret(monkeypatch):
  500. from app import create_app
  501. from app.api.data_rules.routes import _receipt_signer
  502. monkeypatch.delenv("RULE_GENERATION_RECEIPT_SECRET", raising=False)
  503. app = create_app()
  504. app.config["RULE_GENERATION_RECEIPT_SECRET"] = None
  505. with app.app_context(), pytest.raises(
  506. RuntimeError, match="receipt secret"
  507. ):
  508. _receipt_signer()
  509. app.config["RULE_GENERATION_RECEIPT_SECRET"] = (
  510. "dedicated-test-receipt-secret-with-entropy"
  511. )
  512. with app.app_context():
  513. signer = _receipt_signer()
  514. assert signer is not None
  515. def test_rule_gates_reject_caller_supplied_compile_or_test_evidence(monkeypatch):
  516. from app import create_app
  517. app = create_app()
  518. _use_token_identity(monkeypatch)
  519. app.config["TESTING"] = True
  520. repository = FakeRuleRepository()
  521. app.extensions["rule_publication_service"] = FakePublicationService(repository)
  522. client = app.test_client()
  523. version_id = new_governance_uid()
  524. forged_compile = client.post(
  525. f"/api/rules/rule-versions/{version_id}/validate",
  526. json={"status": "success", "plan_hash": "a" * 64},
  527. headers=_headers(app, "editor"),
  528. )
  529. forged_test = client.post(
  530. f"/api/rules/rule-versions/{version_id}/test",
  531. json={
  532. "plan_id": new_governance_uid(),
  533. "evidence": {"status": "success"},
  534. },
  535. headers=_headers(app, "editor"),
  536. )
  537. assert forged_compile.status_code == 409
  538. assert forged_test.status_code == 409
  539. assert repository.calls == []
  540. def test_create_rule_version_rejects_legacy_v1_payload_before_repository(monkeypatch):
  541. from app import create_app
  542. app = create_app()
  543. _use_token_identity(monkeypatch)
  544. app.config["TESTING"] = True
  545. repository = FakeRuleRepository()
  546. app.extensions["data_rule_repository"] = repository
  547. client = app.test_client()
  548. legacy = valid_rule_spec()
  549. legacy["schema_version"] = "1.0"
  550. response = client.post(
  551. "/api/rules/rule-versions",
  552. json={"source_text": "旧版规则不能再创建", "rule_spec": legacy},
  553. headers=_headers(app, "editor"),
  554. )
  555. assert response.status_code == 400
  556. assert repository.calls == []
  557. def test_dataflow_release_uses_server_assets_and_release_permission(monkeypatch):
  558. from app import create_app
  559. app = create_app()
  560. _use_token_identity(monkeypatch)
  561. app.config["TESTING"] = True
  562. service = FakeReleaseService()
  563. app.extensions["production_line_release_service"] = service
  564. client = app.test_client()
  565. flow = valid_dataflow_spec()
  566. payload = {
  567. "source_text": "客户数据生产线",
  568. "dataflow_spec": flow,
  569. }
  570. forbidden = client.post(
  571. f"/api/rules/production-lines/{flow['dataflow_uid']}/release",
  572. json=payload,
  573. headers=_headers(app, "editor"),
  574. )
  575. assert forbidden.status_code == 403
  576. response = client.post(
  577. f"/api/rules/production-lines/{flow['dataflow_uid']}/release",
  578. json=payload,
  579. headers=_headers(app, "admin"),
  580. )
  581. assert response.status_code == 201
  582. assert response.get_json()["data"]["status"] == "released"
  583. assert service.calls[0]["dataflow_uid"] == flow["dataflow_uid"]
  584. assert "standard_versions" not in service.calls[0]
  585. assert "rule_versions" not in service.calls[0]
  586. assert "component_binding_ids" not in service.calls[0]
  587. def test_dataflow_release_rejects_client_authored_schema_hashes(monkeypatch):
  588. from app import create_app
  589. app = create_app()
  590. _use_token_identity(monkeypatch)
  591. app.config["TESTING"] = True
  592. service = FakeReleaseService()
  593. app.extensions["production_line_release_service"] = service
  594. client = app.test_client()
  595. flow = valid_dataflow_spec()
  596. response = client.post(
  597. f"/api/rules/production-lines/{flow['dataflow_uid']}/release",
  598. json={
  599. "source_text": "客户数据生产线",
  600. "dataflow_spec": flow,
  601. "input_schema_hashes": {"bd:customer_raw:v2": "a" * 64},
  602. "output_schema_hash": "b" * 64,
  603. },
  604. headers=_headers(app, "admin"),
  605. )
  606. assert response.status_code == 409
  607. assert service.calls == []
  608. def test_default_release_service_uses_lazy_neo4j_schema_catalog(monkeypatch):
  609. from app import create_app
  610. from app.api.data_rules.routes import _release_service
  611. app = create_app()
  612. repository = SnapshotOnlyRepository()
  613. driver = FakeGraphDriver()
  614. monkeypatch.setattr(
  615. "app.core.data_rules.schema_resolver.neo4j_driver", driver
  616. )
  617. app.extensions["data_rule_repository"] = repository
  618. with app.app_context():
  619. service = _release_service()
  620. snapshot = service.schema_resolver.resolve("bd:customer_raw:v2")
  621. assert snapshot["source_revision"] == "neo4j:9:v2"
  622. assert driver.session.calls[0][1] == {
  623. "domain_key": "customer_raw",
  624. "revision": "v2",
  625. }