| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- from __future__ import annotations
- from dataclasses import replace
- import pytest
- from app.core.data_research.ontology.models import DomainLink, GraphDocument, Ontology, OntologyVersion
- from app.core.data_research.ontology.validation import ValidationIssue
- class FakeOntologyService:
- def __init__(self):
- self.ontology = Ontology(
- uid="ontology-1",
- code="CUSTOMER",
- name="客户本体",
- owner_uid="owner-1",
- domain_links=(DomainLink("domain-1", "owner"), DomainLink("domain-2", "contributor")),
- )
- self.version = OntologyVersion(
- uid="version-1",
- ontology_uid="ontology-1",
- version=1,
- graph_document=GraphDocument.from_dict({"domain_links": [{"domain_uid": "domain-1", "role": "owner"}]}),
- content_hash="a" * 64,
- )
- self.calls = []
- def list(self):
- return [self.ontology]
- def get(self, uid):
- assert uid == self.ontology.uid
- return self.ontology
- def list_versions(self, uid):
- assert uid == self.ontology.uid
- return [self.version]
- def latest_version(self, uid):
- assert uid == self.ontology.uid
- return self.version
- def create(self, payload, actor_uid):
- self.calls.append(("create", payload, actor_uid))
- return self.ontology
- def save_draft(self, uid, graph, expected_revision, actor_uid):
- self.calls.append(("draft", uid, expected_revision, actor_uid))
- return self.version
- def validate(self, uid):
- self.calls.append(("validate", uid))
- return [ValidationIssue("ONTOLOGY_OWNER_REQUIRED", "owner required", "domain_links")]
- def publish(self, uid, idempotency_key, actor_uid):
- self.calls.append(("publish", uid, idempotency_key, actor_uid))
- return replace(self.version, status="published")
- def diff(self, uid, left, right):
- return {"classes": {"added": [], "removed": [], "changed": []}}
- def rollback(self, uid, target_version_uid, expected_revision, actor_uid):
- self.calls.append(("rollback", uid, target_version_uid, expected_revision, actor_uid))
- return replace(self.version, uid="version-2", version=2, parent_version_uid="version-1")
- @pytest.fixture()
- def client(monkeypatch):
- from flask import request
- from app import create_app
- from app.api.data_development import routes
- from app.core.system import permissions
- service = FakeOntologyService()
- def identity():
- token = request.headers.get("Authorization", "")
- role = token.removeprefix("Bearer ")
- return {"id": f"{role}-1", "roles": [role]} if role in {"viewer", "editor", "admin"} else None
- monkeypatch.setattr(permissions, "authenticate_request", identity)
- monkeypatch.setattr(routes, "get_ontology_service", lambda: service)
- app = create_app()
- app.config.update(TESTING=True)
- return app.test_client(), service
- def test_viewer_is_read_only_and_multi_domain_roles_are_returned(client):
- http, _service = client
- headers = {"Authorization": "Bearer viewer"}
- listed = http.get("/api/development/v1/ontologies", headers=headers)
- denied = http.post(
- "/api/development/v1/ontologies",
- headers=headers,
- json={"code": "CUSTOMER", "name": "客户本体"},
- )
- assert listed.status_code == 200
- assert listed.get_json()["data"][0]["domain_links"][1]["role"] == "contributor"
- assert denied.status_code == 403
- def test_viewer_loads_ontology_detail_versions_and_latest_graph(client):
- http, _service = client
- headers = {"Authorization": "Bearer viewer"}
- detail = http.get(
- "/api/development/v1/ontologies/ontology-1", headers=headers
- )
- versions = http.get(
- "/api/development/v1/ontologies/ontology-1/versions", headers=headers
- )
- graph = http.get(
- "/api/development/v1/ontologies/ontology-1/graph", headers=headers
- )
- assert detail.status_code == 200
- assert detail.get_json()["data"]["code"] == "CUSTOMER"
- assert versions.status_code == 200
- assert versions.get_json()["data"][0]["uid"] == "version-1"
- assert graph.status_code == 200
- assert graph.headers["ETag"] == '"0"'
- assert graph.get_json()["data"]["graph_document"]["domain_links"][0] == {
- "domain_uid": "domain-1",
- "role": "owner",
- }
- def test_editor_saves_etag_draft_but_cannot_publish(client):
- http, service = client
- headers = {"Authorization": "Bearer editor", "If-Match": '"0"'}
- saved = http.patch(
- "/api/development/v1/ontologies/ontology-1/graph",
- headers=headers,
- json={"classes": [], "domain_links": []},
- )
- denied = http.post(
- "/api/development/v1/ontologies/ontology-1/publish",
- headers={"Authorization": "Bearer editor", "Idempotency-Key": "publish-1"},
- )
- assert saved.status_code == 200
- assert service.calls[-1][2] == 0
- assert denied.status_code == 403
- def test_admin_validates_publishes_diffs_and_rolls_back(client):
- http, service = client
- headers = {"Authorization": "Bearer admin"}
- validation = http.post("/api/development/v1/ontologies/ontology-1/validate", headers=headers)
- published = http.post(
- "/api/development/v1/ontologies/ontology-1/publish",
- headers={**headers, "Idempotency-Key": "publish-1"},
- )
- diffed = http.get(
- "/api/development/v1/ontologies/ontology-1/diff?left=version-0&right=version-1",
- headers=headers,
- )
- rolled_back = http.post(
- "/api/development/v1/ontologies/ontology-1/rollback",
- headers=headers,
- json={"target_version_uid": "version-1", "expected_revision": 1},
- )
- assert validation.status_code == 200
- assert validation.get_json()["data"][0]["code"] == "ONTOLOGY_OWNER_REQUIRED"
- assert published.get_json()["data"]["status"] == "published"
- assert diffed.status_code == 200
- assert rolled_back.get_json()["data"]["parent_version_uid"] == "version-1"
|