| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381 |
- 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_动态继承子案" not in names
- assert "B4_TEST_草稿主案" not in names
- assert "B4_TEST_绝密主案" not in names
- assert "B4_TEST_无授权主案" not in names
- def test_historical_auditor_cannot_login(b4_client, b2_password):
- response = b4_client.post(
- "/api/v1/auth/login",
- json={
- "username": "auditor",
- "password": b2_password,
- "keepSignedIn": False,
- },
- )
- assert response.status_code == 401
- assert response.get_json()["code"] == "INVALID_CREDENTIALS"
- @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_only_admin_can_list_attachment_library(b4_client, token_for):
- response = b4_client.get(
- "/api/v1/attachments?pageSize=100",
- headers=auth(token_for("admin")),
- )
- assert response.status_code == 200
- assert data(response)["total"] == 2
- assert all(
- item["securityLevel"] == "PUBLIC" for item in data(response)["items"]
- )
- denied = b4_client.get(
- "/api/v1/attachments?pageSize=100",
- headers=auth(token_for("user")),
- )
- assert denied.status_code == 403
- 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("admin")),
- )
- 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("user")),
- )
- 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
- admin_response = b4_client.get(
- f"/api/v1/attachments/{attachment_id}/main-plans",
- headers=auth(token_for("admin")),
- )
- assert data(admin_response)["total"] == 2
- 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"]
- user = b4_client.get(
- f"/api/v1/attachments/{attachment_id}",
- headers=auth(token_for("user")),
- )
- assert data(user)["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()
- scene_a = db.session.scalar(
- db.select(Category).where(Category.category_code == "SCENE_A")
- )
- db.session.add(
- Category(
- category_code="STYLE_A1",
- category_name="B4_TEST_样式A1",
- category_type="STYLE",
- parent_id=scene_a.id,
- category_path=f"{scene_a.category_path}/B4_TEST_样式A1",
- 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
|