from __future__ import annotations import copy import json import traceback from pathlib import Path from types import MappingProxyType import pytest from sqlalchemy import event, func, select from dms.config import ( DEFAULT_UI_DICTIONARY_CONFIG_PATH, load_dms_config, ) from dms.services.ui_dictionary_service import ( DICTIONARY_ENUMS, UiDictionaryConfigError, load_ui_dictionary_config, ) def _default_payload() -> dict: return json.loads( DEFAULT_UI_DICTIONARY_CONFIG_PATH.read_text(encoding="utf-8") ) def _write_payload(path: Path, payload: dict) -> Path: path.write_text( json.dumps(payload, ensure_ascii=False), encoding="utf-8", ) return path def test_default_ui_dictionary_loads_all_enum_codes_and_chinese_labels(): config = load_ui_dictionary_config(DEFAULT_UI_DICTIONARY_CONFIG_PATH) assert config.version == "2026.1" assert config.locale == "zh-CN" assert isinstance(config.dictionaries, MappingProxyType) assert set(config.dictionaries) == set(DICTIONARY_ENUMS) for dictionary_name, enum_type in DICTIONARY_ENUMS.items(): options = config.dictionaries[dictionary_name] assert {option.code for option in options} == { member.value for member in enum_type } assert list(options) == sorted( options, key=lambda option: (option.sort_no, option.code) ) assert all(option.label for option in options) labels = { option.code: option.label for option in config.dictionaries["securityLevels"] } assert labels["INTERNAL"] == "内部" assert { option.code: option.label for option in config.dictionaries["visibilityTypes"] }["ALL_AUTHENTICATED"] == "全体已登录用户" assert { option.code: option.label for option in config.dictionaries["documentStatuses"] }["PUBLISHED"] == "已发布" def test_external_ui_dictionary_path_is_loaded_once( monkeypatch, tmp_path: Path ): payload = _default_payload() payload["dictionaries"]["securityLevels"][1]["label"] = "内部资料" path = _write_payload(tmp_path / "external.json", payload) monkeypatch.setenv("DMS_UI_DICTIONARY_CONFIG_PATH", str(path)) environment_config = load_dms_config() loaded = load_ui_dictionary_config( environment_config["DMS_UI_DICTIONARY_CONFIG_PATH"] ) path.write_text("{broken after startup", encoding="utf-8") response_data = loaded.to_dict() labels = { item["code"]: item["label"] for item in response_data["dictionaries"]["securityLevels"] } assert labels["INTERNAL"] == "内部资料" assert "DMS_UI_DICTIONARY_CONFIG_PATH" not in response_data def test_loaded_configuration_is_immutable(): config = load_ui_dictionary_config(DEFAULT_UI_DICTIONARY_CONFIG_PATH) with pytest.raises(TypeError): config.dictionaries["extra"] = () # type: ignore[index] with pytest.raises(Exception): config.dictionaries["roleCodes"][0].label = "变化" # type: ignore[misc] @pytest.mark.parametrize( "mutation", [ "missing_dictionary", "duplicate_code", "missing_code", "unknown_code", "empty_label", "negative_sort", "boolean_sort", "invalid_version", "invalid_locale", ], ) def test_invalid_ui_dictionary_config_is_rejected_without_path_leak( mutation: str, tmp_path: Path ): payload = copy.deepcopy(_default_payload()) roles = payload["dictionaries"]["roleCodes"] if mutation == "missing_dictionary": del payload["dictionaries"]["auditTargets"] elif mutation == "duplicate_code": roles.append(copy.deepcopy(roles[0])) elif mutation == "missing_code": roles.pop() elif mutation == "unknown_code": roles[0]["code"] = "UNKNOWN_ROLE" elif mutation == "empty_label": roles[0]["label"] = " " elif mutation == "negative_sort": roles[0]["sortNo"] = -1 elif mutation == "boolean_sort": roles[0]["sortNo"] = True elif mutation == "invalid_version": payload["version"] = "latest" elif mutation == "invalid_locale": payload["locale"] = "../zh-CN" path = _write_payload(tmp_path / "sensitive-name.json", payload) with pytest.raises(UiDictionaryConfigError) as captured: load_ui_dictionary_config(path) assert str(path) not in str(captured.value) assert str(tmp_path) not in str(captured.value) def test_missing_malformed_and_non_utf8_config_are_rejected(tmp_path: Path): paths = [ tmp_path / "missing.json", tmp_path / "malformed.json", tmp_path / "non-utf8.json", ] paths[1].write_text("{", encoding="utf-8") paths[2].write_bytes(b"\xff\xfe\x00") for path in paths: with pytest.raises(UiDictionaryConfigError) as captured: load_ui_dictionary_config(path) assert str(tmp_path) not in str(captured.value) rendered = "".join( traceback.format_exception( type(captured.value), captured.value, captured.value.__traceback__, ) ) assert str(tmp_path) not in rendered def test_application_startup_fails_for_invalid_external_config( monkeypatch, tmp_path: Path ): from flask import Flask from dms import init_dms path = tmp_path / "invalid.json" path.write_text("{", encoding="utf-8") monkeypatch.setenv("DMS_UI_DICTIONARY_CONFIG_PATH", str(path)) with pytest.raises(UiDictionaryConfigError): init_dms(Flask("invalid-ui-dictionary")) def test_ui_dictionary_requires_valid_token(b2_client): missing = b2_client.get("/api/v1/config/ui-dictionaries") invalid = b2_client.get( "/api/v1/config/ui-dictionaries", headers={"Authorization": "Bearer invalid-token"}, ) assert missing.status_code == 401 assert invalid.status_code == 401 for response, expected_code in ( (missing, "TOKEN_INVALID"), (invalid, "TOKEN_INVALID"), ): payload = response.get_json() assert payload["code"] == expected_code assert payload["requestId"] == response.headers["X-Request-Id"] def test_all_roles_receive_stable_dictionary_without_audit_or_business_query( b2_app, b2_client, token_for ): from dms.extensions import db from dms.models import AuditLog tokens = { username: token_for(username) for username in ("admin", "user") } with b2_app.app_context(): audit_count_before = db.session.scalar( select(func.count()).select_from(AuditLog) ) statements: list[str] = [] def record_statement( _connection, _cursor, statement, _parameters, _context, _executemany, ): statements.append(statement.lower()) event.listen(db.engine, "before_cursor_execute", record_statement) try: responses = [ b2_client.get( "/api/v1/config/ui-dictionaries", headers={ "Authorization": f"Bearer {token}", "Origin": "http://127.0.0.1:9346", }, ) for token in tokens.values() ] finally: event.remove( db.engine, "before_cursor_execute", record_statement ) audit_count_after = db.session.scalar( select(func.count()).select_from(AuditLog) ) assert all(response.status_code == 200 for response in responses) payloads = [response.get_json() for response in responses] assert all( payload["data"] == payloads[0]["data"] for payload in payloads[1:] ) for response, payload in zip(responses, payloads): assert payload["requestId"] == response.headers["X-Request-Id"] assert "X-Request-Id" in response.headers[ "Access-Control-Expose-Headers" ] assert response.headers["Cache-Control"] == "private, no-store" assert "configPath" not in payload["data"] assert "loadedAt" not in payload["data"] assert audit_count_after == audit_count_before assert len(statements) == len(tokens) assert all( statement.lstrip().startswith("select") and "sys_user" in statement for statement in statements ) def test_openapi_ui_dictionary_contract_and_no_write_operation(): import yaml backend_root = Path(__file__).resolve().parents[2] openapi_root = backend_root / "openapi" document = yaml.safe_load( (openapi_root / "openapi.yaml").read_text(encoding="utf-8") ) path_item = yaml.safe_load( (openapi_root / "paths" / "configuration.yaml").read_text( encoding="utf-8" ) )["UiDictionaries"] schemas = yaml.safe_load( (openapi_root / "components" / "schemas.yaml").read_text( encoding="utf-8" ) ) assert document["info"]["version"] == "1.5.0" assert set(path_item) == {"get"} assert path_item["get"]["operationId"] == "getUiDictionaries" assert {"200", "401", "500"} <= set(path_item["get"]["responses"]) assert set(schemas["UiDictionaryGroups"]["required"]) == set( DICTIONARY_ENUMS )