from __future__ import annotations import hashlib from pathlib import Path import pytest from docx import Document as WordDocument def auth(token: str) -> dict[str, str]: return {"Authorization": f"Bearer {token}"} def data(response): return response.get_json()["data"] def by_name(items, name: str): return next(item for item in items if item["documentName"] == name) def test_documents_require_authentication(b4_client): response = b4_client.get("/api/v1/documents") assert response.status_code == 401 assert response.get_json()["code"] == "TOKEN_INVALID" def test_admin_sees_all_plan_statuses_but_not_attachments( b4_client, token_for ): response = b4_client.get( "/api/v1/documents?pageSize=100", headers=auth(token_for("admin")) ) assert response.status_code == 200 items = data(response)["items"] assert any(item["status"] == "DRAFT" for item in items) assert all(item["documentType"] != "ATTACHMENT" for item in items) def test_user_list_filters_status_security_and_acl(b4_client, token_for): response = b4_client.get( "/api/v1/documents?pageSize=100", headers=auth(token_for("user")) ) assert response.status_code == 200 names = {item["documentName"] for item in data(response)["items"]} assert "B4_TEST_全部主案" in names assert "B4_TEST_组织主案" in names assert "B4_TEST_自定义主案" in names assert "B4_TEST_动态继承子案" in names assert "B4_TEST_草稿主案" not in names assert "B4_TEST_绝密主案" not in names assert "B4_TEST_无授权主案" not in names def test_auditor_plan_list_is_forbidden(b4_client, token_for): response = b4_client.get( "/api/v1/documents", headers=auth(token_for("auditor")) ) assert response.status_code == 403 assert response.get_json()["code"] == "DOCUMENT_VIEW_FORBIDDEN" @pytest.mark.parametrize( ("query", "expected"), [ ("documentType=ATTACHMENT", 400), ("documentType=MAIN,ATTACHMENT", 400), ("sortBy=fileRelativePath", 400), ("sortDirection=sideways", 400), ("pageSize=101", 400), ("categoryId=not-an-id", 400), ("status=INVALID", 400), ], ) def test_document_invalid_filters(b4_client, token_for, query, expected): response = b4_client.get( f"/api/v1/documents?{query}", headers=auth(token_for("admin")) ) assert response.status_code == expected assert response.get_json()["code"] == "INVALID_ARGUMENT" @pytest.mark.parametrize( "sort_by", ["documentName", "createdAt", "updatedAt", "viewCount", "downloadCount"], ) def test_document_sort_whitelist_accepts_contract_fields( b4_client, token_for, sort_by ): response = b4_client.get( f"/api/v1/documents?sortBy={sort_by}&sortDirection=asc", headers=auth(token_for("admin")), ) assert response.status_code == 200 def test_dynamic_sub_plan_inherits_root_visibility(b4_client, token_for): response = b4_client.get( "/api/v1/documents?documentType=SUB_PLAN", headers=auth(token_for("user")), ) item = by_name(data(response)["items"], "B4_TEST_动态继承子案") assert item["visibilityType"] == "CUSTOM" assert item["rootDocumentId"].isdecimal() assert item["allowedActions"] == ["VIEW", "DOWNLOAD"] def test_document_detail_has_contract_fields_and_no_storage_path( b4_app, b4_client, token_for ): document_id = b4_app.config["B4_IDS"]["main_all"] response = b4_client.get( f"/api/v1/documents/{document_id}", headers=auth(token_for("user")) ) assert response.status_code == 200 item = data(response) assert item["id"] == str(document_id) assert item["fileSize"] == 128 assert item["permissionSummary"]["inheritedFromMainPlan"] is False assert "fileRelativePath" not in item assert "updatedByName" not in item assert isinstance(item["tags"], list) def test_document_detail_increments_view_and_audits( b4_app, b4_client, token_for ): from dms.extensions import db from dms.models import AuditLog, Document document_id = b4_app.config["B4_IDS"]["main_all"] response = b4_client.get( f"/api/v1/documents/{document_id}", headers=auth(token_for("user")) ) assert response.status_code == 200 with b4_app.app_context(): document = db.session.get(Document, document_id) audits = db.session.scalars( db.select(AuditLog).where( AuditLog.target_id == document_id, AuditLog.action_type == "VIEW_DOCUMENT", ) ).all() assert document.view_count == 1 assert len(audits) == 1 assert audits[0].target_type == "DOCUMENT" def test_attachment_cannot_use_document_detail( b4_app, b4_client, token_for ): attachment_id = b4_app.config["B4_IDS"]["attachment_one"] response = b4_client.get( f"/api/v1/documents/{attachment_id}", headers=auth(token_for("admin")) ) assert response.status_code == 404 assert response.get_json()["code"] == "RESOURCE_NOT_FOUND" def test_user_security_failure_is_specific(b4_client, token_for): response = b4_client.get( "/api/v1/documents?pageSize=100", headers=auth(token_for("admin")) ) top_secret = by_name(data(response)["items"], "B4_TEST_绝密主案") forbidden = b4_client.get( f"/api/v1/documents/{top_secret['id']}", headers=auth(token_for("user")), ) assert forbidden.status_code == 403 assert forbidden.get_json()["code"] == "SECURITY_LEVEL_FORBIDDEN" def test_user_acl_failure_is_specific(b4_client, token_for): response = b4_client.get( "/api/v1/documents?pageSize=100", headers=auth(token_for("admin")) ) hidden = by_name(data(response)["items"], "B4_TEST_无授权主案") forbidden = b4_client.get( f"/api/v1/documents/{hidden['id']}", headers=auth(token_for("user")), ) assert forbidden.status_code == 403 assert forbidden.get_json()["code"] == "DOCUMENT_VIEW_FORBIDDEN" def test_sub_plan_page_and_main_type_validation(b4_app, b4_client, token_for): main_id = b4_app.config["B4_IDS"]["main_custom"] response = b4_client.get( f"/api/v1/main-plans/{main_id}/sub-plans?keyword=动态&pageSize=1", headers=auth(token_for("user")), ) assert response.status_code == 200 assert data(response)["total"] == 1 sub_id = b4_app.config["B4_IDS"]["sub"] invalid = b4_client.get( f"/api/v1/main-plans/{sub_id}/sub-plans", headers=auth(token_for("admin")), ) assert invalid.status_code == 404 assert invalid.get_json()["code"] == "MAIN_PLAN_NOT_FOUND" def test_all_roles_can_list_attachments(b4_client, token_for): for username in ("user", "admin", "auditor"): response = b4_client.get( "/api/v1/attachments?pageSize=100", headers=auth(token_for(username)), ) assert response.status_code == 200 assert data(response)["total"] == 2 assert all(item["securityLevel"] == "PUBLIC" for item in data(response)["items"]) def test_attachment_filters_and_real_mount_count(b4_client, token_for): response = b4_client.get( "/api/v1/attachments?attachmentType=WORK_STANDARD&fileExtension=docx", headers=auth(token_for("user")), ) assert response.status_code == 200 assert data(response)["total"] == 1 assert data(response)["items"][0]["mountedPlanCount"] == 2 def test_attachment_detail_increments_view_and_attachment_audit( b4_app, b4_client, token_for ): from dms.extensions import db from dms.models import AuditLog, Document attachment_id = b4_app.config["B4_IDS"]["attachment_one"] response = b4_client.get( f"/api/v1/attachments/{attachment_id}", headers=auth(token_for("auditor")), ) assert response.status_code == 200 item = data(response) assert item["categoryId"] is None assert item["permissionSummary"] == { "organizationCount": 0, "userCount": 0, "inheritedFromMainPlan": False, } assert "fileRelativePath" not in item with b4_app.app_context(): assert db.session.get(Document, attachment_id).view_count == 1 audit = db.session.scalar( db.select(AuditLog).where(AuditLog.target_id == attachment_id) ) assert audit.target_type == "ATTACHMENT" def test_non_attachment_returns_not_found(b4_app, b4_client, token_for): main_id = b4_app.config["B4_IDS"]["main_all"] response = b4_client.get( f"/api/v1/attachments/{main_id}", headers=auth(token_for("admin")) ) assert response.status_code == 404 def test_attachment_main_plans_respect_plan_access( b4_app, b4_client, token_for ): attachment_id = b4_app.config["B4_IDS"]["attachment_one"] user_response = b4_client.get( f"/api/v1/attachments/{attachment_id}/main-plans", headers=auth(token_for("user")), ) assert data(user_response)["total"] == 2 auditor_response = b4_client.get( f"/api/v1/attachments/{attachment_id}/main-plans", headers=auth(token_for("auditor")), ) assert data(auditor_response) == {"items": [], "total": 0} def test_main_plan_attachments_stable_binding_order( b4_app, b4_client, token_for ): main_id = b4_app.config["B4_IDS"]["main_all"] response = b4_client.get( f"/api/v1/main-plans/{main_id}/attachments", headers=auth(token_for("user")), ) assert response.status_code == 200 items = data(response)["items"] assert [item["bindingSortNo"] for item in items] == [10, 20] assert all(item["bindingId"].isdecimal() for item in items) def test_allowed_actions_are_stable(b4_app, b4_client, token_for): main_id = b4_app.config["B4_IDS"]["main_all"] admin = b4_client.get( f"/api/v1/documents/{main_id}", headers=auth(token_for("admin")) ) assert data(admin)["allowedActions"] == [ "VIEW", "DOWNLOAD", "EDIT", "CONFIG_PERMISSION", "DELETE", "BIND_ATTACHMENT", "UNBIND_ATTACHMENT", ] attachment_id = b4_app.config["B4_IDS"]["attachment_one"] auditor = b4_client.get( f"/api/v1/attachments/{attachment_id}", headers=auth(token_for("auditor")), ) assert data(auditor)["allowedActions"] == ["VIEW", "DOWNLOAD"] def test_seed_documents_is_idempotent_and_creates_valid_files( b3_app, tmp_path: Path ): from dms.extensions import db from dms.models import Category, Document from dms.seed_documents import seed_documents with b3_app.app_context(): # B3测试分类编码与开发初始化编码不同,补齐seed的必要依赖。 for code, name in ( ("SCENE_A", "B4_TEST_场景A"), ("SCENE_B", "B4_TEST_场景B"), ("SCENE_C", "B4_TEST_场景C"), ): db.session.add( Category( category_code=code, category_name=name, category_type="SCENE", category_path=f"/{name}", sort_no=10, document_count=0, status="ENABLED", ) ) db.session.commit() first = seed_documents(tmp_path / "storage") second = seed_documents(tmp_path / "storage") assert first == (9, 0, 0) assert second == (0, 9, 0) documents = db.session.scalars( db.select(Document).where( Document.document_name.in_( ["2024年度综合应急预案", "装备保障工作规范"] ) ) ).all() assert len(documents) == 2 for document in documents: path = tmp_path / "storage" / Path(document.file_relative_path) assert path.is_file() WordDocument(path) assert hashlib.sha256(path.read_bytes()).hexdigest() == document.file_hash