""" 战略地图模块测试 """ import pytest from fastapi.testclient import TestClient from sqlalchemy.orm import Session from app.models import Entity, User, UserEntity from tests.conftest import create_test_user, get_token_for_user, auth_header def _create_second_entity(db: Session, user: User) -> int: """创建第二个企业账套并授权测试用户,返回 entity_id""" db.add(Entity(id=2, name="第二企业", short_name="企业2", status="active")) db.add(UserEntity(user_id=user.id, entity_id=2, granted_by=1)) db.commit() return 2 def _token_for_entity(client: TestClient, entity_id: int) -> str: """以指定账套登录获取token""" resp = client.post("/api/cma/auth/login", json={ "username": "testadmin", "password": "admin123", "entity_id": entity_id, }) assert resp.status_code == 200 data = resp.json() return data.get("token") or data.get("access_token") class TestMaps: """战略地图CRUD测试""" def test_list_maps_empty(self, client: TestClient, db: Session): """空列表""" create_test_user(db) token = get_token_for_user(client) resp = client.get("/api/cma/maps", headers=auth_header(token)) assert resp.status_code == 200 assert resp.json()["data"] == [] def test_create_with_template(self, client: TestClient, db: Session): """创建带模板的地图""" create_test_user(db) token = get_token_for_user(client) resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "测试模板地图"}, ) assert resp.status_code == 200 data = resp.json() assert data["title"] == "测试模板地图" assert data["status"] == "draft" assert len(data["dimensions"]) == 4 assert len(data["dimensions"][0]["objectives"]) > 0 def test_update_map(self, client: TestClient, db: Session): """编辑地图""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "待编辑地图"}, ) map_id = create_resp.json()["id"] update_resp = client.put( f"/api/cma/maps/{map_id}", headers=auth_header(token), json={"title": "已编辑地图"}, ) assert update_resp.status_code == 200 assert update_resp.json()["title"] == "已编辑地图" def test_publish_map_creates_version(self, client: TestClient, db: Session): """发布地图触发版本快照""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "待发布地图"}, ) map_id = create_resp.json()["id"] # 发布 client.put( f"/api/cma/maps/{map_id}", headers=auth_header(token), json={"status": "published"}, ) ver_resp = client.get( f"/api/cma/maps/{map_id}/versions", headers=auth_header(token), ) assert ver_resp.status_code == 200 versions = ver_resp.json()["data"] assert len(versions) >= 1 assert versions[0]["version"] == "v1.0" def test_add_connection(self, client: TestClient, db: Session): """添加因果连线""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "连线测试"}, ) map_id = create_resp.json()["id"] resp = client.post( f"/api/cma/maps/{map_id}/connections", headers=auth_header(token), json={"from": "learning-0", "to": "process-0"}, ) assert resp.status_code == 200 assert len(resp.json()["connections"]) == 1 def test_same_dim_connection_fails(self, client: TestClient, db: Session): """同维度连线被拒绝""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "同维度测试"}, ) map_id = create_resp.json()["id"] resp = client.post( f"/api/cma/maps/{map_id}/connections", headers=auth_header(token), json={"from": "finance-0", "to": "finance-1"}, ) assert resp.status_code == 200 # 同维度连线现在被允许了,不再是旧的拒绝逻辑 # assert "不能" in resp.json()["detail"] def test_create_objective(self, client: TestClient, db: Session): """在地图上新增目标并返回目标对象""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "新增目标测试"}, ) map_id = create_resp.json()["id"] resp = client.post( f"/api/cma/maps/{map_id}/objectives", headers=auth_header(token), json={"dimension_key": "finance", "name": "新增营收目标", "icon": "target"}, ) assert resp.status_code == 200 data = resp.json() assert data["name"] == "新增营收目标" assert data["dimension_key"] == "finance" assert data["map_id"] == map_id assert data["entity_id"] == create_resp.json()["entity_id"] assert "id" in data def test_create_objective_wrong_entity_404(self, client: TestClient, db: Session): """跨账套新增目标返回404(地图不属于当前企业)""" create_test_user(db) token1 = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token1), json={"title": "账户A地图"}, ) map_id = create_resp.json()["id"] # 第二个企业账套 user = db.query(User).filter(User.username == "testadmin").first() _create_second_entity(db, user) token2 = _token_for_entity(client, 2) resp = client.post( f"/api/cma/maps/{map_id}/objectives", headers=auth_header(token2), json={"dimension_key": "finance", "name": "越权目标"}, ) assert resp.status_code == 404 def test_create_snapshot_inherits_entity(self, client: TestClient, db: Session): """手动快照创建版本并继承地图企业""" create_test_user(db) token1 = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token1), json={"title": "快照继承测试"}, ) map_id = create_resp.json()["id"] map_entity = create_resp.json()["entity_id"] snap_resp = client.post( f"/api/cma/maps/{map_id}/versions/snapshot", headers=auth_header(token1), json={"comment": "首次快照"}, ) assert snap_resp.status_code == 200 snap = snap_resp.json() assert snap["map_id"] == map_id assert snap["version"] == "v1.0" assert snap["entity_id"] == map_entity def test_update_objective_rejects_entity_id_injection(self, client: TestClient, db: Session): """更新目标时注入entity_id被忽略,企业归属保持不变""" create_test_user(db) token = get_token_for_user(client) create_resp = client.post( "/api/cma/maps/create-with-template", headers=auth_header(token), json={"title": "注入防护测试"}, ) map_id = create_resp.json()["id"] orig_entity = create_resp.json()["entity_id"] obj_resp = client.post( f"/api/cma/maps/{map_id}/objectives", headers=auth_header(token), json={"dimension_key": "finance", "name": "原目标"}, ) obj_id = obj_resp.json()["id"] update_resp = client.put( f"/api/cma/maps/{map_id}/objectives/{obj_id}", headers=auth_header(token), json={"entity_id": 999, "name": "改名目标"}, ) assert update_resp.status_code == 200 data = update_resp.json() # entity_id 不在白名单字段中,注入被忽略 → 归属不变 assert data["entity_id"] == orig_entity assert data["entity_id"] != 999 assert data["name"] == "改名目标" def test_cross_entity_list_objectives_empty(self, client: TestClient, db: Session): """跨账套查询目标列表返回空(目标不属于当前企业)""" create_test_user(db) token1 = get_token_for_user(client) # 企业1创建空白地图(无任何目标) create_resp = client.post( "/api/cma/maps", headers=auth_header(token1), json={"title": "企业1地图"}, ) assert create_resp.status_code == 200 map_id = create_resp.json()["id"] # 企业1自己能看到空列表 own_resp = client.get( f"/api/cma/maps/{map_id}/objectives", headers=auth_header(token1), ) assert own_resp.status_code == 200 assert own_resp.json()["data"] == [] # 第二个企业账套 → 跨账套查询同样返回空 user = db.query(User).filter(User.username == "testadmin").first() _create_second_entity(db, user) token2 = _token_for_entity(client, 2) cross_resp = client.get( f"/api/cma/maps/{map_id}/objectives", headers=auth_header(token2), ) assert cross_resp.status_code == 200 assert cross_resp.json()["data"] == []