"""数据分类分级 API 测试 — 重要数据标记 + 资产清单 + 导出""" import hashlib import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import Subject, KPIDefinition, Entity from tests.conftest import create_test_user, get_token_for_user, auth_header def create_test_subject(db: Session, **kwargs) -> Subject: """创建测试科目""" defaults = { "subject_code": "1001", "subject_name": "库存现金", "level": 1, "is_active": 1, } defaults.update(kwargs) s = Subject(**defaults) db.add(s) db.commit() db.refresh(s) return s def create_test_kpi(db: Session, **kwargs) -> KPIDefinition: """创建测试KPI(带entity_id)""" defaults = { "entity_id": 1, "kpi_code": "F_TEST_REVENUE", "kpi_name": "营业收入(万元)", "dimension": "finance", "status": "active", } defaults.update(kwargs) k = KPIDefinition(**defaults) db.add(k) db.commit() db.refresh(k) return k class TestInventory: def test_inventory_empty(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", headers=auth_header(token)) assert r.status_code == 200 data = r.json() assert data["total"] == 0 assert data["stats"]["marked"] == 0 def test_inventory_contains_subject_and_kpi(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", headers=auth_header(token)) assert r.status_code == 200 data = r.json() types = {i["type"] for i in data["items"]} assert types == {"subject", "kpi"} assert data["total"] == 2 # 未标记时级别默认 general assert data["stats"]["by_level"]["general"] == 2 def test_filter_by_data_type(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", params={"data_type": "kpi"}, headers=auth_header(token)) data = r.json() assert data["total"] == 1 assert data["items"][0]["type"] == "kpi" def test_filter_by_level(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db, data_level="important", important_flag=1) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", params={"data_level": "important"}, headers=auth_header(token)) data = r.json() assert data["total"] == 1 assert data["items"][0]["name"] == "库存现金" def test_filter_important_only(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db, data_level="important", important_flag=1) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", params={"important": 1}, headers=auth_header(token)) data = r.json() assert data["total"] == 1 def test_invalid_level_400(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/inventory", params={"data_level": "secret"}, headers=auth_header(token)) assert r.status_code == 400 class TestMarkSubject: def test_mark_subject(self, client: TestClient, db: Session): create_test_user(db) s = create_test_subject(db) token = get_token_for_user(client) r = client.put(f"/api/cma/data-classification/subjects/{s.id}", params={"data_level": "important", "data_category": "财务数据"}, headers=auth_header(token)) assert r.status_code == 200 body = r.json() # 重要级别自动视为重要数据 assert body["important_flag"] == 1 assert body["data_level"] == "important" assert body["data_category"] == "财务数据" def test_mark_subject_general_clears(self, client: TestClient, db: Session): create_test_user(db) s = create_test_subject(db, data_level="important", important_flag=1) token = get_token_for_user(client) r = client.put(f"/api/cma/data-classification/subjects/{s.id}", params={"data_level": "general", "important_flag": 0}, headers=auth_header(token)) assert r.status_code == 200 assert r.json()["data_level"] == "general" assert r.json()["important_flag"] == 0 def test_mark_subject_not_found(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) r = client.put("/api/cma/data-classification/subjects/9999", params={"data_level": "important"}, headers=auth_header(token)) assert r.status_code == 404 class TestMarkKpi: def test_mark_kpi(self, client: TestClient, db: Session): create_test_user(db) k = create_test_kpi(db, entity_id=1) token = get_token_for_user(client) r = client.put(f"/api/cma/data-classification/kpis/{k.id}", params={"data_level": "core", "data_category": "财务数据"}, headers=auth_header(token)) assert r.status_code == 200 assert r.json()["important_flag"] == 1 assert r.json()["data_level"] == "core" def test_kpi_entity_isolation(self, client: TestClient, db: Session): """多租户隔离:entity 2 的KPI,entity 1 的token不可见、不可标记""" create_test_user(db) k2 = create_test_kpi(db, entity_id=2, kpi_code="F_OTHER_ENTITY") token = get_token_for_user(client) # token 绑定 entity 1 # 不可标记 r = client.put(f"/api/cma/data-classification/kpis/{k2.id}", params={"data_level": "important"}, headers=auth_header(token)) assert r.status_code == 404 # 清单中不可见 r2 = client.get("/api/cma/data-classification/inventory", params={"data_type": "kpi"}, headers=auth_header(token)) data = r2.json() assert all(i["code"] != "F_OTHER_ENTITY" for i in data["items"]) class TestBatch: def test_batch_mark_subjects(self, client: TestClient, db: Session): create_test_user(db) s1 = create_test_subject(db, subject_code="1001", subject_name="库存现金") s2 = create_test_subject(db, subject_code="1002", subject_name="银行存款") token = get_token_for_user(client) r = client.put("/api/cma/data-classification/batch", params={"data_type": "subject", "ids": [s1.id, s2.id], "data_level": "important", "data_category": "财务数据"}, headers=auth_header(token)) assert r.status_code == 200 assert r.json()["updated_count"] == 2 # 回查确认生效 db.expire_all() assert db.query(Subject).get(s1.id).important_flag == 1 assert db.query(Subject).get(s2.id).data_level == "important" def test_batch_invalid_type(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) r = client.put("/api/cma/data-classification/batch", params={"data_type": "bad", "ids": [1]}, headers=auth_header(token)) assert r.status_code == 400 class TestExport: def test_export_csv(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db, data_level="important", important_flag=1) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/export", headers=auth_header(token)) assert r.status_code == 200 assert "text/csv" in r.headers["content-type"] text = r.text # 表头 + 类型 + 中文标签 assert "类型" in text and "数据级别" in text assert "库存现金" in text assert "科目" in text and "KPI" in text assert "重要数据" in text def test_export_filtered(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db, data_level="important", important_flag=1) create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/export", params={"important": 1}, headers=auth_header(token)) text = r.text assert "库存现金" in text assert "营业收入(万元)" not in text class TestStats: def test_stats(self, client: TestClient, db: Session): create_test_user(db) create_test_subject(db, data_level="important", important_flag=1) create_test_subject(db, subject_code="1002", subject_name="银行存款") create_test_kpi(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/stats", headers=auth_header(token)) assert r.status_code == 200 data = r.json() assert data["subjects"]["total"] == 2 assert data["subjects"]["marked"] == 1 assert data["kpis"]["total"] == 1 assert data["marked_total"] == 1 assert "policy_note" in data def test_industry_reference(self, client: TestClient, db: Session): create_test_user(db) token = get_token_for_user(client) r = client.get("/api/cma/data-classification/industry-reference", headers=auth_header(token)) assert r.status_code == 200 items = r.json()["items"] assert len(items) >= 5 industries = {i["industry"] for i in items} assert "金融" in industries and "医疗" in industries