| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213 |
- import json
- from pathlib import Path
- from fastapi.testclient import TestClient
- from ontorefactor_governance import api
- from ontorefactor_governance.db import Database
- SAMPLES = Path(__file__).parents[1] / "src" / "ontorefactor_governance" / "static" / "samples"
- def test_standalone_api_and_web_console(monkeypatch):
- db = Database("sqlite:///:memory:")
- monkeypatch.setattr(api, "get_database", lambda: db)
- client = TestClient(api.app)
- headers = {"X-Tenant-ID": "api-test"}
- assert client.get("/api/health").json()["ok"] is True
- page = client.get("/")
- assert page.status_code == 200
- assert "OntoRefactor" in page.text
- seeded = client.post("/api/v1/governance/bootstrap/tenant-isolation", headers=headers)
- assert seeded.status_code == 200
- project_id = seeded.json()["project"]["id"]
- projects = client.get("/api/v1/governance/projects", headers=headers).json()["projects"]
- assert len(projects) == 1
- assert projects[0]["id"] == project_id
- overview = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers=headers).json()
- assert overview["totals"]["elements"] == 48
- assert overview["totals"]["open_issues"] == 0
- exported = client.get(f"/api/v1/governance/projects/{project_id}/export.jsonld", headers=headers)
- assert exported.status_code == 200
- assert exported.json()["@graph"]
- other_tenant = client.get(f"/api/v1/governance/projects/{project_id}/overview", headers={"X-Tenant-ID": "other"})
- assert other_tenant.status_code == 404
- db.close()
- def test_agent_endpoint(monkeypatch):
- db = Database("sqlite:///:memory:")
- monkeypatch.setattr(api, "get_database", lambda: db)
- client = TestClient(api.app)
- headers = {"X-Tenant-ID": "agent-api"}
- project_id = client.post("/api/v1/governance/projects", headers=headers, json={
- "name": "Imported governance", "namespace": "urn:api:imported"
- }).json()["id"]
- response = client.post(f"/api/v1/governance/projects/{project_id}/agent-runs/analyze", headers=headers, json={
- "source_type": "ddl",
- "source_name": "orders.sql",
- "environment": "development",
- "content": "CREATE TABLE orders (id INTEGER PRIMARY KEY, tenant_id INTEGER NOT NULL);",
- })
- assert response.status_code == 200
- assert response.json()["persisted"]["elements"] == 7
- runs = client.get(f"/api/v1/governance/projects/{project_id}/agent-runs", headers=headers).json()["runs"]
- assert len(runs) == 1
- assert runs[0]["status"] == "completed"
- db.close()
- def test_source_file_preview_and_upload_agent_endpoint(monkeypatch):
- db = Database("sqlite:///:memory:")
- monkeypatch.setattr(api, "get_database", lambda: db)
- client = TestClient(api.app)
- headers = {"X-Tenant-ID": "upload-api"}
- project_id = client.post("/api/v1/governance/projects", headers=headers, json={
- "name": "Uploaded governance", "namespace": "urn:api:uploaded"
- }).json()["id"]
- ddl = b"CREATE TABLE iam.customer (id BIGINT PRIMARY KEY, mobile VARCHAR(32));"
- preview = client.post(
- f"/api/v1/governance/projects/{project_id}/sources/preview",
- headers=headers,
- files={"file": ("customer.sql", ddl, "text/plain")},
- data={"source_type": "auto"},
- )
- assert preview.status_code == 200
- assert preview.json()["source_type"] == "ddl"
- assert preview.json()["detected"] == {
- "tables": 1, "columns": 2, "apis": 0, "schemas": 0, "assets": 0,
- "relationships": 0, "evidence": 0, "rules": 0, "sensitive_fields": ["mobile"],
- }
- assert preview.json()["content"].startswith("CREATE TABLE")
- analyzed = client.post(
- f"/api/v1/governance/projects/{project_id}/agent-runs/upload",
- headers=headers,
- files={"file": ("customer.sql", ddl, "text/plain")},
- data={"source_type": "auto", "semantic_mode": "deterministic", "instruction": "识别敏感字段"},
- )
- assert analyzed.status_code == 200
- assert analyzed.json()["status"] == "completed"
- assert analyzed.json()["persisted"]["elements"] > 0
- evidence = client.get(
- f"/api/v1/governance/projects/{project_id}/evidence", headers=headers
- ).json()["evidence"]
- source = next(item for item in evidence if item["kind"] == "SourceArtifact")
- assert source["content"].startswith("CREATE TABLE")
- assert source["metadata"]["upload"]["name"] == "customer.sql"
- db.close()
- def test_llm_status_is_safe_and_forced_mode_requires_key(monkeypatch):
- monkeypatch.delenv("LONGCAT_API_KEY", raising=False)
- monkeypatch.setenv("ONTOREFACTOR_LLM_MODE", "auto")
- db = Database("sqlite:///:memory:")
- monkeypatch.setattr(api, "get_database", lambda: db)
- client = TestClient(api.app)
- headers = {"X-Tenant-ID": "llm-api"}
- status = client.get("/api/v1/governance/llm/status", headers=headers)
- assert status.status_code == 200
- assert status.json()["configured"] is False
- assert "api_key" not in json.dumps(status.json()).lower()
- project_id = client.post("/api/v1/governance/projects", headers=headers, json={
- "name": "LLM project", "namespace": "urn:api:llm"
- }).json()["id"]
- response = client.post(f"/api/v1/governance/projects/{project_id}/agent-runs/analyze", headers=headers, json={
- "source_type": "ddl", "semantic_mode": "llm",
- "content": "CREATE TABLE customer (id INTEGER PRIMARY KEY);",
- })
- assert response.status_code == 503
- assert response.json()["detail"]["code"] == "LLM_NOT_CONFIGURED"
- db.close()
- def test_llm_connection_failure_is_a_structured_api_error(monkeypatch):
- def fail_connection():
- from ontorefactor_governance.service import GovernanceError
- raise GovernanceError("LongCat 鉴权失败,请检查 LONGCAT_API_KEY", code="LLM_AUTH_FAILED", status=502)
- monkeypatch.setattr(api, "test_llm_connection", fail_connection)
- client = TestClient(api.app)
- response = client.post("/api/v1/governance/llm/test", headers={"X-Tenant-ID": "llm-api"})
- assert response.status_code == 502
- assert response.json()["detail"]["code"] == "LLM_AUTH_FAILED"
- def test_challenge_cup_demo_bootstrap_endpoint(monkeypatch):
- db = Database("sqlite:///:memory:")
- monkeypatch.setattr(api, "get_database", lambda: db)
- client = TestClient(api.app)
- headers = {"X-Tenant-ID": "challenge-demo"}
- response = client.post("/api/v1/governance/bootstrap/challenge-cup", headers=headers)
- assert response.status_code == 200
- payload = response.json()
- project_id = payload["project"]["id"]
- assert payload["overview"]["demo"]["decision"] == "尚未构建本体"
- assert payload["overview"]["totals"]["elements"] == 0
- assert payload["overview"]["totals"]["assertions"] == 0
- assert payload["overview"]["totals"]["evidence"] == 0
- names = [
- "01-dcp-user.sql", "02-user-profile-openapi.yaml",
- "03-dcp-asset-inventory.json", "04-governance-evidence.json",
- ]
- files = [("files", (name, (SAMPLES / name).read_bytes(), "application/octet-stream")) for name in names]
- built = client.post(
- f"/api/v1/governance/projects/{project_id}/ontology-builds/upload",
- headers=headers,
- files=files,
- data={"semantic_mode": "deterministic", "environment": "production", "decision_target": "dcp_user.mobile"},
- )
- assert built.status_code == 200
- build = built.json()
- assert build["status"] == "review_required"
- assert build["diff"]["new_elements"] > 0
- published = client.post(
- f"/api/v1/governance/projects/{project_id}/ontology-builds/{build['build_id']}/publish",
- headers=headers,
- json={"reviewer": "api-test", "accept_semantic_assertions": False, "decision_target": "dcp_user.mobile"},
- )
- assert published.status_code == 200
- assert published.json()["decision"]["status"] == "BLOCKED"
- assert published.json()["version"]["label"] == "v1"
- evaluated = client.post(
- f"/api/v1/governance/projects/{project_id}/decisions/retirement",
- headers=headers,
- json={"target": "dcp_user.mobile", "include_pending_assertions": True, "persist": True},
- )
- assert evaluated.status_code == 200
- assert evaluated.json()["status"] == "BLOCKED"
- assert evaluated.json()["summary"] == {"passed": 1, "failed": 4, "unknown": 0, "total": 5}
- answered = client.post(
- f"/api/v1/governance/projects/{project_id}/governance-agent/ask",
- headers=headers,
- json={"question": "dcp_user.mobile 是否可以迁移并删除?", "persist": True},
- )
- assert answered.status_code == 200
- assert answered.json()["agent"] == "governance-decision-agent"
- assert answered.json()["decision"]["status"] == "BLOCKED"
- assert answered.json()["resolved_target"]["id"]
- decisions = client.get(
- f"/api/v1/governance/projects/{project_id}/decisions", headers=headers
- ).json()["decisions"]
- assert len(decisions) >= 1
- assert decisions[0]["checks"]
- db.close()
|