test_api.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. import json
  2. from pathlib import Path
  3. from fastapi.testclient import TestClient
  4. from ontorefactor_governance import api
  5. from ontorefactor_governance.db import Database
  6. SAMPLES = Path(__file__).parents[1] / "src" / "ontorefactor_governance" / "static" / "samples"
  7. def test_standalone_api_and_web_console(monkeypatch):
  8. db = Database("sqlite:///:memory:")
  9. monkeypatch.setattr(api, "get_database", lambda: db)
  10. client = TestClient(api.app)
  11. headers = {"X-Tenant-ID": "api-test"}
  12. assert client.get("/api/health").json()["ok"] is True
  13. page = client.get("/")
  14. assert page.status_code == 200
  15. assert "OntoRefactor" in page.text
  16. seeded = client.post("/api/v1/governance/bootstrap/tenant-isolation", headers=headers)
  17. assert seeded.status_code == 200
  18. project_id = seeded.json()["project"]["id"]
  19. projects = client.get("/api/v1/governance/projects", headers=headers).json()["projects"]
  20. assert len(projects) == 1
  21. assert projects[0]["id"] == project_id
  22. overview = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers=headers).json()
  23. assert overview["totals"]["elements"] == 48
  24. assert overview["totals"]["open_issues"] == 0
  25. exported = client.get(f"/api/v1/governance/projects/{project_id}/export.jsonld", headers=headers)
  26. assert exported.status_code == 200
  27. assert exported.json()["@graph"]
  28. other_tenant = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers={"X-Tenant-ID": "other"})
  29. assert other_tenant.status_code == 404
  30. db.close()
  31. def test_agent_endpoint(monkeypatch):
  32. db = Database("sqlite:///:memory:")
  33. monkeypatch.setattr(api, "get_database", lambda: db)
  34. client = TestClient(api.app)
  35. headers = {"X-Tenant-ID": "agent-api"}
  36. project_id = client.post("/api/v1/governance/projects", headers=headers, json={
  37. "name": "Imported governance", "namespace": "urn:api:imported"
  38. }).json()["id"]
  39. response = client.post(f"/api/v1/governance/projects/{project_id}/agent-runs/analyze", headers=headers, json={
  40. "source_type": "ddl",
  41. "source_name": "orders.sql",
  42. "environment": "development",
  43. "content": "CREATE TABLE orders (id INTEGER PRIMARY KEY, tenant_id INTEGER NOT NULL);",
  44. })
  45. assert response.status_code == 200
  46. assert response.json()["persisted"]["elements"] == 7
  47. runs = client.get(f"/api/v1/governance/projects/{project_id}/agent-runs", headers=headers).json()["runs"]
  48. assert len(runs) == 1
  49. assert runs[0]["status"] == "completed"
  50. db.close()
  51. def test_source_file_preview_and_upload_agent_endpoint(monkeypatch):
  52. db = Database("sqlite:///:memory:")
  53. monkeypatch.setattr(api, "get_database", lambda: db)
  54. client = TestClient(api.app)
  55. headers = {"X-Tenant-ID": "upload-api"}
  56. project_id = client.post("/api/v1/governance/projects", headers=headers, json={
  57. "name": "Uploaded governance", "namespace": "urn:api:uploaded"
  58. }).json()["id"]
  59. ddl = b"CREATE TABLE iam.customer (id BIGINT PRIMARY KEY, mobile VARCHAR(32));"
  60. preview = client.post(
  61. f"/api/v1/governance/projects/{project_id}/sources/preview",
  62. headers=headers,
  63. files={"file": ("customer.sql", ddl, "text/plain")},
  64. data={"source_type": "auto"},
  65. )
  66. assert preview.status_code == 200
  67. assert preview.json()["source_type"] == "ddl"
  68. assert preview.json()["detected"] == {
  69. "tables": 1, "columns": 2, "apis": 0, "schemas": 0, "assets": 0,
  70. "relationships": 0, "evidence": 0, "rules": 0, "sensitive_fields": ["mobile"],
  71. }
  72. assert preview.json()["content"].startswith("CREATE TABLE")
  73. analyzed = client.post(
  74. f"/api/v1/governance/projects/{project_id}/agent-runs/upload",
  75. headers=headers,
  76. files={"file": ("customer.sql", ddl, "text/plain")},
  77. data={"source_type": "auto", "semantic_mode": "deterministic", "instruction": "识别敏感字段"},
  78. )
  79. assert analyzed.status_code == 200
  80. assert analyzed.json()["status"] == "completed"
  81. assert analyzed.json()["persisted"]["elements"] > 0
  82. evidence = client.get(
  83. f"/api/v1/governance/projects/{project_id}/evidence", headers=headers
  84. ).json()["evidence"]
  85. source = next(item for item in evidence if item["kind"] == "SourceArtifact")
  86. assert source["content"].startswith("CREATE TABLE")
  87. assert source["metadata"]["upload"]["name"] == "customer.sql"
  88. db.close()
  89. def test_llm_status_is_safe_and_forced_mode_requires_key(monkeypatch):
  90. monkeypatch.delenv("LONGCAT_API_KEY", raising=False)
  91. monkeypatch.setenv("ONTOREFACTOR_LLM_MODE", "auto")
  92. db = Database("sqlite:///:memory:")
  93. monkeypatch.setattr(api, "get_database", lambda: db)
  94. client = TestClient(api.app)
  95. headers = {"X-Tenant-ID": "llm-api"}
  96. status = client.get("/api/v1/governance/llm/status", headers=headers)
  97. assert status.status_code == 200
  98. assert status.json()["configured"] is False
  99. assert "api_key" not in json.dumps(status.json()).lower()
  100. project_id = client.post("/api/v1/governance/projects", headers=headers, json={
  101. "name": "LLM project", "namespace": "urn:api:llm"
  102. }).json()["id"]
  103. response = client.post(f"/api/v1/governance/projects/{project_id}/agent-runs/analyze", headers=headers, json={
  104. "source_type": "ddl", "semantic_mode": "llm",
  105. "content": "CREATE TABLE customer (id INTEGER PRIMARY KEY);",
  106. })
  107. assert response.status_code == 503
  108. assert response.json()["detail"]["code"] == "LLM_NOT_CONFIGURED"
  109. db.close()
  110. def test_llm_connection_failure_is_a_structured_api_error(monkeypatch):
  111. def fail_connection():
  112. from ontorefactor_governance.service import GovernanceError
  113. raise GovernanceError("LongCat 鉴权失败,请检查 LONGCAT_API_KEY", code="LLM_AUTH_FAILED", status=502)
  114. monkeypatch.setattr(api, "test_llm_connection", fail_connection)
  115. client = TestClient(api.app)
  116. response = client.post("/api/v1/governance/llm/test", headers={"X-Tenant-ID": "llm-api"})
  117. assert response.status_code == 502
  118. assert response.json()["detail"]["code"] == "LLM_AUTH_FAILED"
  119. def test_challenge_cup_demo_bootstrap_endpoint(monkeypatch):
  120. db = Database("sqlite:///:memory:")
  121. monkeypatch.setattr(api, "get_database", lambda: db)
  122. client = TestClient(api.app)
  123. headers = {"X-Tenant-ID": "challenge-demo"}
  124. response = client.post("/api/v1/governance/bootstrap/challenge-cup", headers=headers)
  125. assert response.status_code == 200
  126. payload = response.json()
  127. project_id = payload["project"]["id"]
  128. assert payload["overview"]["demo"]["decision"] == "尚未构建本体"
  129. assert payload["overview"]["totals"]["elements"] == 0
  130. assert payload["overview"]["totals"]["assertions"] == 0
  131. assert payload["overview"]["totals"]["evidence"] == 0
  132. names = [
  133. "01-dcp-user.sql", "02-user-profile-openapi.yaml",
  134. "03-dcp-asset-inventory.json", "04-governance-evidence.json",
  135. ]
  136. files = [("files", (name, (SAMPLES / name).read_bytes(), "application/octet-stream")) for name in names]
  137. built = client.post(
  138. f"/api/v1/governance/projects/{project_id}/ontology-builds/upload",
  139. headers=headers,
  140. files=files,
  141. data={"semantic_mode": "deterministic", "environment": "production", "decision_target": "dcp_user.mobile"},
  142. )
  143. assert built.status_code == 200
  144. build = built.json()
  145. assert build["status"] == "review_required"
  146. assert build["diff"]["new_elements"] > 0
  147. published = client.post(
  148. f"/api/v1/governance/projects/{project_id}/ontology-builds/{build['build_id']}/publish",
  149. headers=headers,
  150. json={"reviewer": "api-test", "accept_semantic_assertions": False, "decision_target": "dcp_user.mobile"},
  151. )
  152. assert published.status_code == 200
  153. assert published.json()["decision"]["status"] == "BLOCKED"
  154. assert published.json()["version"]["label"] == "v1"
  155. evaluated = client.post(
  156. f"/api/v1/governance/projects/{project_id}/decisions/retirement",
  157. headers=headers,
  158. json={"target": "dcp_user.mobile", "include_pending_assertions": True, "persist": True},
  159. )
  160. assert evaluated.status_code == 200
  161. assert evaluated.json()["status"] == "BLOCKED"
  162. assert evaluated.json()["summary"] == {"passed": 1, "failed": 4, "unknown": 0, "total": 5}
  163. answered = client.post(
  164. f"/api/v1/governance/projects/{project_id}/governance-agent/ask",
  165. headers=headers,
  166. json={"question": "dcp_user.mobile 是否可以迁移并删除?", "persist": True},
  167. )
  168. assert answered.status_code == 200
  169. assert answered.json()["agent"] == "governance-decision-agent"
  170. assert answered.json()["decision"]["status"] == "BLOCKED"
  171. assert answered.json()["resolved_target"]["id"]
  172. decisions = client.get(
  173. f"/api/v1/governance/projects/{project_id}/decisions", headers=headers
  174. ).json()["decisions"]
  175. assert len(decisions) >= 1
  176. assert decisions[0]["checks"]
  177. db.close()