| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424 |
- from __future__ import annotations
- import os
- import uuid
- from datetime import UTC, datetime, timedelta
- import pytest
- from sqlalchemy import text
- pytestmark = pytest.mark.integration
- def test_governance_metrics_apply_scope_and_return_safe_details(monkeypatch):
- platform_url = os.environ.get("TEST_DATABASE_URL")
- if not platform_url:
- pytest.skip("TEST_DATABASE_URL is required")
- monkeypatch.setenv("DATABASE_URL", platform_url)
- from app import create_app, db
- from app.core.data_research.governance_metric_repository import (
- SqlAlchemyGovernanceMetricRepository,
- )
- from app.core.data_research.governance_metrics import GovernanceMetricAccess
- from app.models.data_research import (
- DeviceAsset,
- DeviceAssetSourceMapping,
- DeviceEntityMatchCandidate,
- DeviceEntityMatchReview,
- DeviceEntityMergeEvent,
- DeviceEntityMergeRollback,
- DeviceQualityIssue,
- DeviceQualityProfile,
- DeviceQualityProfileVersion,
- DeviceQualityRun,
- IngestionSource,
- )
- app = create_app()
- app.config.update(TESTING=True)
- suffix = uuid.uuid4().hex[:10]
- actor_uid = str(uuid.uuid4())
- now = datetime.now(UTC).replace(microsecond=0)
- domain_a = str(uuid.uuid4())
- domain_b = str(uuid.uuid4())
- source_uids = {
- "a": str(uuid.uuid4()),
- "b": str(uuid.uuid4()),
- "u": str(uuid.uuid4()),
- }
- asset_uids = {
- key: str(uuid.uuid4())
- for key in ("a1", "a2", "a3", "b", "u", "retired")
- }
- mapping_uids = {}
- candidate_uids = []
- merge_uids = []
- issue_uids = []
- profile_uid = str(uuid.uuid4())
- profile_version_uid = str(uuid.uuid4())
- run_uid = str(uuid.uuid4())
- baseline_admin = None
- try:
- with app.app_context():
- baseline_admin = SqlAlchemyGovernanceMetricRepository(
- db.session
- ).summary_counts(GovernanceMetricAccess(global_access=True))
- db.session.execute(
- text(
- """
- INSERT INTO public.users (
- id, username, display_name, password_hash, status
- ) VALUES (
- CAST(:id AS uuid), :username, :username,
- 'integration-test', 'active'
- )
- """
- ),
- {"id": actor_uid, "username": f"wp11-actor-{suffix}"},
- )
- for key, scope in (
- ("a", {"business_domains": [domain_a]}),
- ("b", {"business_domains": [domain_b]}),
- ("u", {}),
- ):
- db.session.add(
- IngestionSource(
- uid=source_uids[key],
- source_type="database",
- name=f"WP11 source {key} {suffix}",
- config={"password": f"source-secret-{key}"},
- permission_scope=scope,
- status="active",
- created_by=actor_uid,
- )
- )
- db.session.flush()
- for key, _source_key, complete, status in (
- ("a1", "a", True, "active"),
- ("a2", "a", False, "active"),
- ("a3", "a", True, "active"),
- ("b", "b", True, "active"),
- ("u", "u", True, "active"),
- ("retired", "a", True, "retired"),
- ):
- db.session.add(
- DeviceAsset(
- uid=asset_uids[key],
- asset_type="device",
- name=f"WP11设备-{suffix}-{key}",
- status=status,
- current_version=1,
- content_hash=(key[0] * 64),
- location=f"{key}车间",
- organization="设备部" if complete else " ",
- responsible_person="张工" if complete else None,
- attributes={"password": f"asset-secret-{key}"},
- created_by=actor_uid,
- updated_by=actor_uid,
- updated_at=now,
- )
- )
- db.session.flush()
- for key, source_key, _complete, _status in (
- ("a1", "a", True, "active"),
- ("a2", "a", False, "active"),
- ("a3", "a", True, "active"),
- ("b", "b", True, "active"),
- ("u", "u", True, "active"),
- ("retired", "a", True, "retired"),
- ):
- mapping_uid = str(uuid.uuid4())
- mapping_uids[key] = mapping_uid
- db.session.add(
- DeviceAssetSourceMapping(
- uid=mapping_uid,
- asset_uid=asset_uids[key],
- source_uid=source_uids[source_key],
- source_entity="asset.equipment",
- asset_type="device",
- source_code=f"EQ-WP11-{suffix}-{key}",
- source_updated_at=now,
- first_seen_at=now,
- last_seen_at=now,
- )
- )
- db.session.flush()
- def add_merge(left_key, right_key, *, rolled_back=False):
- candidate_uid = str(uuid.uuid4())
- review_uid = str(uuid.uuid4())
- merge_uid = str(uuid.uuid4())
- candidate_uids.append(candidate_uid)
- merge_uids.append(merge_uid)
- db.session.add(
- DeviceEntityMatchCandidate(
- uid=candidate_uid,
- left_asset_uid=asset_uids[left_key],
- right_asset_uid=asset_uids[right_key],
- canonical_asset_uid=asset_uids[left_key],
- status="rolled_back" if rolled_back else "merged",
- suggestion_source="manual",
- confidence=1,
- explanation=[],
- evidence_uids=[],
- current_version=2 if rolled_back else 1,
- created_by=actor_uid,
- )
- )
- db.session.flush()
- db.session.add(
- DeviceEntityMatchReview(
- uid=review_uid,
- candidate_uid=candidate_uid,
- version=1,
- decision="approve",
- reason="WP11 integration",
- actor_uid=actor_uid,
- )
- )
- db.session.flush()
- db.session.add(
- DeviceEntityMergeEvent(
- uid=merge_uid,
- candidate_uid=candidate_uid,
- canonical_asset_uid=asset_uids[left_key],
- member_asset_uid=asset_uids[right_key],
- review_uid=review_uid,
- snapshot={"password": "merge-secret"},
- actor_uid=actor_uid,
- )
- )
- db.session.flush()
- if rolled_back:
- db.session.add(
- DeviceEntityMergeRollback(
- uid=str(uuid.uuid4()),
- merge_uid=merge_uid,
- candidate_uid=candidate_uid,
- reason="WP11 rollback",
- snapshot={"password": "rollback-secret"},
- actor_uid=actor_uid,
- )
- )
- add_merge("a1", "a2")
- add_merge("a3", "b")
- add_merge("a1", "a3", rolled_back=True)
- db.session.add(
- DeviceQualityProfile(
- uid=profile_uid,
- code=f"wp11-{suffix}",
- name="WP11 integration",
- created_by=actor_uid,
- )
- )
- db.session.flush()
- db.session.add(
- DeviceQualityProfileVersion(
- uid=profile_version_uid,
- profile_uid=profile_uid,
- version=1,
- status="published",
- rules=[],
- content_hash=suffix.ljust(64, "0"),
- created_by=actor_uid,
- published_by=actor_uid,
- published_at=now,
- )
- )
- db.session.flush()
- db.session.add(
- DeviceQualityRun(
- uid=run_uid,
- policy_version_uid=profile_version_uid,
- policy_hash=suffix.ljust(64, "0"),
- source_uid=source_uids["a"],
- status="success",
- total_assets=3,
- total_violations=3,
- score=50,
- created_by=actor_uid,
- )
- )
- db.session.flush()
- for index, (asset_key, source_key, status, occurrence) in enumerate(
- (
- ("a1", "a", "closed", 1),
- ("a2", "a", "open", 2),
- ("b", "b", "closed", 1),
- ),
- start=1,
- ):
- issue_uid = str(uuid.uuid4())
- issue_uids.append(issue_uid)
- db.session.add(
- DeviceQualityIssue(
- uid=issue_uid,
- issue_code=f"W11{suffix[:6]}{index:02d}",
- source_violation_uid=str(uuid.uuid4()),
- source_run_uid=run_uid,
- rule_code="asset_context_complete",
- severity="error",
- priority="high",
- asset_uid=asset_uids[asset_key],
- field_name="organization",
- source_uid=source_uids[source_key],
- source_mapping_uid=mapping_uids[asset_key],
- message=f"issue-secret-{asset_key}",
- evidence={"password": f"evidence-secret-{asset_key}"},
- recurrence_key=(f"{suffix}-{asset_key}").ljust(64, "0"),
- occurrence_number=occurrence,
- status=status,
- assignee_uid=actor_uid,
- due_at=now - timedelta(days=1),
- current_version=1,
- created_by=actor_uid,
- updated_by=actor_uid,
- created_at=now,
- updated_at=now,
- closed_at=now if status == "closed" else None,
- )
- )
- db.session.commit()
- repository = SqlAlchemyGovernanceMetricRepository(db.session)
- admin = GovernanceMetricAccess(global_access=True)
- domain_viewer = GovernanceMetricAccess(
- global_access=False,
- business_domain_uids=(domain_a,),
- )
- assert baseline_admin is not None
- assert repository.summary_counts(admin) == {
- "asset_completeness": (
- baseline_admin["asset_completeness"][0] + 4,
- baseline_admin["asset_completeness"][1] + 5,
- ),
- "responsibility_coverage": (
- baseline_admin["responsibility_coverage"][0] + 4,
- baseline_admin["responsibility_coverage"][1] + 5,
- ),
- "entity_mapping": (
- baseline_admin["entity_mapping"][0] + 4,
- baseline_admin["entity_mapping"][1] + 5,
- ),
- "issue_closure": (
- baseline_admin["issue_closure"][0] + 2,
- baseline_admin["issue_closure"][1] + 3,
- ),
- "issue_recurrence": (
- baseline_admin["issue_recurrence"][0] + 1,
- baseline_admin["issue_recurrence"][1] + 3,
- ),
- }
- assert repository.summary_counts(domain_viewer) == {
- "asset_completeness": (2, 3),
- "responsibility_coverage": (2, 3),
- "entity_mapping": (2, 3),
- "issue_closure": (1, 2),
- "issue_recurrence": (1, 2),
- }
- incomplete, incomplete_total = repository.list_details(
- domain_viewer,
- metric="asset_completeness",
- state="incomplete",
- page=1,
- page_size=20,
- )
- mapped, mapped_total = repository.list_details(
- domain_viewer,
- metric="entity_mapping",
- state="mapped",
- page=1,
- page_size=20,
- )
- recurrent, recurrent_total = repository.list_details(
- domain_viewer,
- metric="issue_recurrence",
- state="recurrent",
- page=1,
- page_size=20,
- )
- assert incomplete_total == 1
- assert incomplete[0]["asset_uid"] == asset_uids["a2"]
- assert incomplete[0]["missing_fields"] == [
- "organization",
- "responsible_person",
- ]
- assert mapped_total == 2
- assert {item["asset_uid"] for item in mapped} == {
- asset_uids["a1"],
- asset_uids["a2"],
- }
- assert recurrent_total == 1
- assert recurrent[0]["issue_uid"] == issue_uids[1]
- assert recurrent[0]["overdue"] is True
- serialized = repr((incomplete, mapped, recurrent))
- for secret in (
- "source-secret",
- "asset-secret",
- "merge-secret",
- "issue-secret",
- "evidence-secret",
- "permission_scope",
- ):
- assert secret not in serialized
- finally:
- with app.app_context():
- for model, column, values in (
- (DeviceQualityIssue, DeviceQualityIssue.uid, issue_uids),
- (
- DeviceEntityMergeRollback,
- DeviceEntityMergeRollback.merge_uid,
- merge_uids,
- ),
- (
- DeviceEntityMergeEvent,
- DeviceEntityMergeEvent.uid,
- merge_uids,
- ),
- (
- DeviceEntityMatchReview,
- DeviceEntityMatchReview.candidate_uid,
- candidate_uids,
- ),
- (
- DeviceEntityMatchCandidate,
- DeviceEntityMatchCandidate.uid,
- candidate_uids,
- ),
- ):
- if values:
- db.session.query(model).filter(column.in_(values)).delete(
- synchronize_session=False
- )
- db.session.query(DeviceQualityRun).filter_by(uid=run_uid).delete()
- db.session.query(DeviceQualityProfileVersion).filter_by(
- uid=profile_version_uid
- ).delete()
- db.session.query(DeviceQualityProfile).filter_by(
- uid=profile_uid
- ).delete()
- if asset_uids:
- db.session.query(DeviceAssetSourceMapping).filter(
- DeviceAssetSourceMapping.asset_uid.in_(asset_uids.values())
- ).delete(synchronize_session=False)
- db.session.query(DeviceAsset).filter(
- DeviceAsset.uid.in_(asset_uids.values())
- ).delete(synchronize_session=False)
- db.session.query(IngestionSource).filter(
- IngestionSource.uid.in_(source_uids.values())
- ).delete(synchronize_session=False)
- db.session.execute(
- text(
- "DELETE FROM public.users "
- "WHERE id = CAST(:id AS uuid)"
- ),
- {"id": actor_uid},
- )
- db.session.commit()
|