feat: add tenant-scoped rule and permission management
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""Test package root."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Release acceptance tests."""
|
||||
@@ -0,0 +1,479 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Callable
|
||||
|
||||
import pytest
|
||||
|
||||
from .helpers import ReleaseApiClient, ReleaseTestConfig
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TenantSeed:
|
||||
tenant_code: str
|
||||
tenant_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SeededUser:
|
||||
user_id: int
|
||||
sub: str
|
||||
username: str
|
||||
nickname: str
|
||||
role_key: str
|
||||
tenant_code: str
|
||||
token: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DocumentTypeSeed:
|
||||
type_id: int
|
||||
type_code: str
|
||||
type_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseDocumentSeed:
|
||||
document_id: int
|
||||
file_id: int | None
|
||||
type_id: int
|
||||
type_code: str
|
||||
tenant_code: str
|
||||
tenant_name: str
|
||||
file_name: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseDatasetSeed:
|
||||
dataset_id: int
|
||||
dataset_name: str
|
||||
tenant_code: str
|
||||
tenant_name: str
|
||||
app_id: int | None
|
||||
app_name: str | None
|
||||
|
||||
|
||||
def _env(name: str, default: str) -> str:
|
||||
return str(os.getenv(name, default)).strip()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def release_config() -> ReleaseTestConfig:
|
||||
return ReleaseTestConfig(
|
||||
base_url=_env("LEAUDIT_TEST_BASE_URL", "http://127.0.0.1:8096").rstrip("/"),
|
||||
admin_username=_env("LEAUDIT_TEST_ADMIN_USERNAME", "000"),
|
||||
admin_password=_env("LEAUDIT_TEST_ADMIN_PASSWORD", "admin06111"),
|
||||
timeout_seconds=float(_env("LEAUDIT_TEST_TIMEOUT_SECONDS", "30")),
|
||||
tenant_a_code=_env("LEAUDIT_TEST_TENANT_A_CODE", "PTA01"),
|
||||
tenant_a_name=_env("LEAUDIT_TEST_TENANT_A_NAME", "Pytest租户A"),
|
||||
tenant_b_code=_env("LEAUDIT_TEST_TENANT_B_CODE", "PTB01"),
|
||||
tenant_b_name=_env("LEAUDIT_TEST_TENANT_B_NAME", "Pytest租户B"),
|
||||
module_name=_env("LEAUDIT_TEST_MODULE_NAME", f"Pytest发布验收入口模块-{time.time_ns()}"),
|
||||
module_path=_env("LEAUDIT_TEST_MODULE_PATH", "/documents"),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def anonymous_client(release_config: ReleaseTestConfig) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def admin_auth(release_config: ReleaseTestConfig, anonymous_client: ReleaseApiClient) -> dict[str, Any]:
|
||||
return anonymous_client.login_password(release_config.admin_username, release_config.admin_password)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def admin_client(release_config: ReleaseTestConfig, admin_auth: dict[str, Any]) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config, token=str(admin_auth["access_token"]))
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def role_map(admin_client: ReleaseApiClient) -> dict[str, int]:
|
||||
response = admin_client.get("/api/v3/rbac/roles?page=1&page_size=200")
|
||||
items = ReleaseApiClient.json_data(response)["items"]
|
||||
mapping = {str(item["role_key"]): int(item["id"]) for item in items}
|
||||
for role_key in ("super_admin", "provincial_admin", "admin", "common"):
|
||||
assert role_key in mapping, f"缺少角色种子: {role_key}"
|
||||
return mapping
|
||||
|
||||
|
||||
def _ensure_tenant(admin_client: ReleaseApiClient, tenant_code: str, tenant_name: str) -> TenantSeed:
|
||||
detail_response = admin_client.get(f"/api/v3/tenants/{tenant_code}", expected_status=None)
|
||||
body = detail_response.json()
|
||||
desired_payload = {
|
||||
"tenant_name": tenant_name,
|
||||
"tenant_short_name": tenant_name,
|
||||
"tenant_type": "CUSTOM",
|
||||
"parent_tenant_code": None,
|
||||
"display_order": 500,
|
||||
"is_public": False,
|
||||
"can_host_entry_module": True,
|
||||
"can_host_documents": True,
|
||||
"can_host_rag": True,
|
||||
"can_host_templates": True,
|
||||
"feature_keys": ["home.entry_module", "documents.upload", "rag.dataset"],
|
||||
"alias_values": [tenant_name],
|
||||
"ext": {"created_by": "pytest-release"},
|
||||
}
|
||||
if detail_response.status_code == 404:
|
||||
create_payload = {
|
||||
"tenant_code": tenant_code,
|
||||
"is_enabled": True,
|
||||
**desired_payload,
|
||||
}
|
||||
admin_client.post("/api/v3/tenants", json=create_payload, expected_status=200)
|
||||
else:
|
||||
assert detail_response.status_code == 200, body
|
||||
admin_client.put(f"/api/v3/tenants/{tenant_code}", json=desired_payload, expected_status=200)
|
||||
admin_client.patch(f"/api/v3/tenants/{tenant_code}/status", json={"is_enabled": True}, expected_status=200)
|
||||
return TenantSeed(tenant_code=tenant_code, tenant_name=tenant_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_a(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
|
||||
return _ensure_tenant(admin_client, release_config.tenant_a_code, release_config.tenant_a_name)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_b(admin_client: ReleaseApiClient, release_config: ReleaseTestConfig) -> TenantSeed:
|
||||
return _ensure_tenant(admin_client, release_config.tenant_b_code, release_config.tenant_b_name)
|
||||
|
||||
|
||||
def _ensure_user(
|
||||
*,
|
||||
anonymous_client: ReleaseApiClient,
|
||||
admin_client: ReleaseApiClient,
|
||||
role_map: dict[str, int],
|
||||
sub: str,
|
||||
username: str,
|
||||
nickname: str,
|
||||
role_key: str,
|
||||
tenant: TenantSeed,
|
||||
) -> SeededUser:
|
||||
login_data = anonymous_client.login_oauth(
|
||||
sub=sub,
|
||||
username=username,
|
||||
nickname=nickname,
|
||||
ou_id=f"pytest-{tenant.tenant_code.lower()}",
|
||||
ou_name=f"{tenant.tenant_name}测试组织",
|
||||
area=tenant.tenant_name,
|
||||
)
|
||||
user_info = login_data["user_info"]
|
||||
user_id = int(user_info["user_id"])
|
||||
admin_client.post(
|
||||
f"/api/v3/rbac/users/{user_id}/roles",
|
||||
json={"role_ids": [role_map[role_key]]},
|
||||
expected_status=200,
|
||||
)
|
||||
admin_client.put(
|
||||
f"/api/v3/rbac/users/{user_id}/tenant",
|
||||
json={"tenant_code": tenant.tenant_code},
|
||||
expected_status=200,
|
||||
)
|
||||
refreshed = anonymous_client.login_oauth(
|
||||
sub=sub,
|
||||
username=username,
|
||||
nickname=nickname,
|
||||
ou_id=f"pytest-{tenant.tenant_code.lower()}",
|
||||
ou_name=f"{tenant.tenant_name}测试组织",
|
||||
area=tenant.tenant_name,
|
||||
)
|
||||
refreshed_user = refreshed["user_info"]
|
||||
assert str(refreshed_user.get("tenant_code") or "") == tenant.tenant_code
|
||||
return SeededUser(
|
||||
user_id=user_id,
|
||||
sub=sub,
|
||||
username=username,
|
||||
nickname=nickname,
|
||||
role_key=role_key,
|
||||
tenant_code=tenant.tenant_code,
|
||||
token=str(refreshed["access_token"]),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_admin_user(
|
||||
anonymous_client: ReleaseApiClient,
|
||||
admin_client: ReleaseApiClient,
|
||||
role_map: dict[str, int],
|
||||
tenant_a: TenantSeed,
|
||||
) -> SeededUser:
|
||||
return _ensure_user(
|
||||
anonymous_client=anonymous_client,
|
||||
admin_client=admin_client,
|
||||
role_map=role_map,
|
||||
sub="pytest-admin-pta01",
|
||||
username="pytest_admin_pta01",
|
||||
nickname="Pytest租户A管理员",
|
||||
role_key="admin",
|
||||
tenant=tenant_a,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_admin_user_b(
|
||||
anonymous_client: ReleaseApiClient,
|
||||
admin_client: ReleaseApiClient,
|
||||
role_map: dict[str, int],
|
||||
tenant_b: TenantSeed,
|
||||
) -> SeededUser:
|
||||
return _ensure_user(
|
||||
anonymous_client=anonymous_client,
|
||||
admin_client=admin_client,
|
||||
role_map=role_map,
|
||||
sub="pytest-admin-ptb01",
|
||||
username="pytest_admin_ptb01",
|
||||
nickname="Pytest租户B管理员",
|
||||
role_key="admin",
|
||||
tenant=tenant_b,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_common_user_a(
|
||||
anonymous_client: ReleaseApiClient,
|
||||
admin_client: ReleaseApiClient,
|
||||
role_map: dict[str, int],
|
||||
tenant_a: TenantSeed,
|
||||
) -> SeededUser:
|
||||
return _ensure_user(
|
||||
anonymous_client=anonymous_client,
|
||||
admin_client=admin_client,
|
||||
role_map=role_map,
|
||||
sub="pytest-common-pta01",
|
||||
username="pytest_common_pta01",
|
||||
nickname="Pytest租户A普通用户",
|
||||
role_key="common",
|
||||
tenant=tenant_a,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tenant_common_user_b(
|
||||
anonymous_client: ReleaseApiClient,
|
||||
admin_client: ReleaseApiClient,
|
||||
role_map: dict[str, int],
|
||||
tenant_b: TenantSeed,
|
||||
) -> SeededUser:
|
||||
return _ensure_user(
|
||||
anonymous_client=anonymous_client,
|
||||
admin_client=admin_client,
|
||||
role_map=role_map,
|
||||
sub="pytest-common-ptb01",
|
||||
username="pytest_common_ptb01",
|
||||
nickname="Pytest租户B普通用户",
|
||||
role_key="common",
|
||||
tenant=tenant_b,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_admin_api(release_config: ReleaseTestConfig, tenant_admin_user: SeededUser) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config, token=tenant_admin_user.token)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tenant_admin_api_b(release_config: ReleaseTestConfig, tenant_admin_user_b: SeededUser) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config, token=tenant_admin_user_b.token)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def common_api_a(release_config: ReleaseTestConfig, tenant_common_user_a: SeededUser) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config, token=tenant_common_user_a.token)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def common_api_b(release_config: ReleaseTestConfig, tenant_common_user_b: SeededUser) -> ReleaseApiClient:
|
||||
client = ReleaseApiClient(release_config, token=tenant_common_user_b.token)
|
||||
yield client
|
||||
client.close()
|
||||
|
||||
|
||||
def _find_module_by_name(admin_client: ReleaseApiClient, module_name: str) -> dict[str, Any] | None:
|
||||
response = admin_client.get(f"/api/v3/entry-modules?page=1&page_size=200&name={module_name}".replace("page_size", "page_size"), expected_status=None)
|
||||
items = ReleaseApiClient.json_data(response)["items"]
|
||||
for item in items:
|
||||
if str(item.get("name") or "") == module_name:
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def release_entry_module(
|
||||
admin_client: ReleaseApiClient,
|
||||
release_config: ReleaseTestConfig,
|
||||
tenant_b: TenantSeed,
|
||||
) -> dict[str, Any]:
|
||||
module_name = release_config.module_name
|
||||
existing = _find_module_by_name(admin_client, module_name)
|
||||
payload = {
|
||||
"name": module_name,
|
||||
"description": "pytest release acceptance only",
|
||||
"path": release_config.module_path,
|
||||
"route_path": release_config.module_path,
|
||||
"tenants": [
|
||||
{
|
||||
"tenant_code": tenant_b.tenant_code,
|
||||
"tenant_name": tenant_b.tenant_name,
|
||||
"enabled": True,
|
||||
"sort_order": 1,
|
||||
}
|
||||
],
|
||||
}
|
||||
created = False
|
||||
if existing:
|
||||
module_id = int(existing["id"])
|
||||
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
|
||||
else:
|
||||
response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
|
||||
if response.status_code == 200:
|
||||
created = True
|
||||
else:
|
||||
retried = False
|
||||
for attempt in range(1, 4):
|
||||
module_name = f"{release_config.module_name}-{attempt}"
|
||||
payload["name"] = module_name
|
||||
existing = _find_module_by_name(admin_client, module_name)
|
||||
if existing:
|
||||
module_id = int(existing["id"])
|
||||
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
|
||||
retried = True
|
||||
break
|
||||
response = admin_client.post("/api/v3/entry-modules", json=payload, expected_status=None)
|
||||
if response.status_code == 200:
|
||||
created = True
|
||||
retried = True
|
||||
break
|
||||
assert retried, response.text
|
||||
module = ReleaseApiClient.json_data(response)
|
||||
try:
|
||||
yield module
|
||||
finally:
|
||||
if created:
|
||||
admin_client.delete(f"/api/v3/entry-modules/{int(module['id'])}", expected_status=200)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def release_document_type(admin_client: ReleaseApiClient) -> DocumentTypeSeed:
|
||||
response = admin_client.get("/api/document-types")
|
||||
items = ReleaseApiClient.json_data(response)
|
||||
assert isinstance(items, list), items
|
||||
for item in items:
|
||||
type_id = int(item.get("id") or 0)
|
||||
type_code = str(item.get("code") or "").strip()
|
||||
type_name = str(item.get("name") or "").strip()
|
||||
if type_id > 0 and type_code:
|
||||
return DocumentTypeSeed(type_id=type_id, type_code=type_code, type_name=type_name or type_code)
|
||||
pytest.skip("当前环境没有可用于发布验收的文档类型")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_release_document(
|
||||
admin_client: ReleaseApiClient,
|
||||
release_document_type: DocumentTypeSeed,
|
||||
) -> Callable[..., ReleaseDocumentSeed]:
|
||||
created_document_ids: list[int] = []
|
||||
|
||||
def _make_document(
|
||||
*,
|
||||
client: ReleaseApiClient,
|
||||
tenant: TenantSeed,
|
||||
file_name: str,
|
||||
content: bytes | None = None,
|
||||
content_type: str = "text/plain",
|
||||
) -> ReleaseDocumentSeed:
|
||||
payload_bytes = content or f"pytest release document for {tenant.tenant_code} / {file_name}\n".encode("utf-8")
|
||||
response = client.post(
|
||||
"/api/upload",
|
||||
data={
|
||||
"typeId": str(release_document_type.type_id),
|
||||
"typeCode": release_document_type.type_code,
|
||||
"tenant_code": tenant.tenant_code,
|
||||
"region": tenant.tenant_name,
|
||||
"fileRole": "primary",
|
||||
"autoRun": "false",
|
||||
"speed": "normal",
|
||||
},
|
||||
files={
|
||||
"file": (file_name, payload_bytes, content_type),
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
data = ReleaseApiClient.json_data(response)
|
||||
document_id = int(data["documentId"])
|
||||
created_document_ids.append(document_id)
|
||||
return ReleaseDocumentSeed(
|
||||
document_id=document_id,
|
||||
file_id=int(data["fileId"]) if data.get("fileId") is not None else None,
|
||||
type_id=release_document_type.type_id,
|
||||
type_code=release_document_type.type_code,
|
||||
tenant_code=tenant.tenant_code,
|
||||
tenant_name=tenant.tenant_name,
|
||||
file_name=file_name,
|
||||
)
|
||||
|
||||
try:
|
||||
yield _make_document
|
||||
finally:
|
||||
for document_id in reversed(created_document_ids):
|
||||
admin_client.delete(f"/api/documents/{document_id}", expected_status=None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_release_dataset(
|
||||
admin_client: ReleaseApiClient,
|
||||
) -> Callable[..., ReleaseDatasetSeed]:
|
||||
created_dataset_ids: list[int] = []
|
||||
|
||||
def _make_dataset(
|
||||
*,
|
||||
tenant: TenantSeed,
|
||||
dataset_name: str,
|
||||
is_public: bool = False,
|
||||
is_default: bool = False,
|
||||
) -> ReleaseDatasetSeed:
|
||||
response = admin_client.post(
|
||||
"/api/v3/rag/datasets/admin",
|
||||
json={
|
||||
"tenant_code": tenant.tenant_code,
|
||||
"tenant_name": tenant.tenant_name,
|
||||
"area": tenant.tenant_name,
|
||||
"name": dataset_name,
|
||||
"description": f"{dataset_name} - pytest release",
|
||||
"is_public": is_public,
|
||||
"is_default": is_default,
|
||||
"status": 1,
|
||||
"sort_order": 0,
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
data = ReleaseApiClient.json_data(response)
|
||||
dataset_id = int(data["id"])
|
||||
created_dataset_ids.append(dataset_id)
|
||||
return ReleaseDatasetSeed(
|
||||
dataset_id=dataset_id,
|
||||
dataset_name=str(data.get("name") or dataset_name),
|
||||
tenant_code=str(data.get("tenantCode") or tenant.tenant_code),
|
||||
tenant_name=str(data.get("tenantName") or tenant.tenant_name),
|
||||
app_id=int(data["appId"]) if data.get("appId") is not None else None,
|
||||
app_name=str(data.get("appName") or "") or None,
|
||||
)
|
||||
|
||||
try:
|
||||
yield _make_dataset
|
||||
finally:
|
||||
for dataset_id in reversed(created_dataset_ids):
|
||||
admin_client.delete(f"/api/v3/rag/datasets/admin/{dataset_id}", expected_status=None)
|
||||
@@ -0,0 +1,134 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseTestConfig:
|
||||
base_url: str
|
||||
admin_username: str
|
||||
admin_password: str
|
||||
timeout_seconds: float
|
||||
tenant_a_code: str
|
||||
tenant_a_name: str
|
||||
tenant_b_code: str
|
||||
tenant_b_name: str
|
||||
module_name: str
|
||||
module_path: str
|
||||
|
||||
|
||||
class ReleaseApiClient:
|
||||
def __init__(self, config: ReleaseTestConfig, token: str | None = None) -> None:
|
||||
self.config = config
|
||||
self.token = token
|
||||
self._client = httpx.Client(base_url=config.base_url, timeout=config.timeout_seconds)
|
||||
|
||||
def close(self) -> None:
|
||||
self._client.close()
|
||||
|
||||
def with_token(self, token: str) -> "ReleaseApiClient":
|
||||
return ReleaseApiClient(self.config, token=token)
|
||||
|
||||
def request(self, method: str, path: str, *, expected_status: int | None = 200, **kwargs: Any) -> httpx.Response:
|
||||
headers = dict(kwargs.pop("headers", {}) or {})
|
||||
if self.token:
|
||||
headers["Authorization"] = f"Bearer {self.token}"
|
||||
response = self._client.request(method, path, headers=headers, **kwargs)
|
||||
if expected_status is not None:
|
||||
assert response.status_code == expected_status, self._format_error(response)
|
||||
return response
|
||||
|
||||
def get(self, path: str, **kwargs: Any) -> httpx.Response:
|
||||
return self.request("GET", path, **kwargs)
|
||||
|
||||
def post(self, path: str, **kwargs: Any) -> httpx.Response:
|
||||
return self.request("POST", path, **kwargs)
|
||||
|
||||
def put(self, path: str, **kwargs: Any) -> httpx.Response:
|
||||
return self.request("PUT", path, **kwargs)
|
||||
|
||||
def patch(self, path: str, **kwargs: Any) -> httpx.Response:
|
||||
return self.request("PATCH", path, **kwargs)
|
||||
|
||||
def delete(self, path: str, **kwargs: Any) -> httpx.Response:
|
||||
return self.request("DELETE", path, **kwargs)
|
||||
|
||||
def login_password(self, username: str, password: str) -> dict[str, Any]:
|
||||
response = self.post(
|
||||
"/api/auth/login",
|
||||
json={"username": username, "password": password},
|
||||
expected_status=200,
|
||||
)
|
||||
payload = response.json()
|
||||
assert payload.get("success") is True, payload
|
||||
data = payload.get("data") or {}
|
||||
assert data.get("access_token"), payload
|
||||
return data
|
||||
|
||||
def login_oauth(
|
||||
self,
|
||||
*,
|
||||
sub: str,
|
||||
username: str,
|
||||
nickname: str,
|
||||
ou_id: str,
|
||||
ou_name: str,
|
||||
area: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
response = self.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"userInfo": {
|
||||
"sub": sub,
|
||||
"username": username,
|
||||
"nickname": nickname,
|
||||
"email": f"{username}@pytest.local",
|
||||
"phone_number": "13800000000",
|
||||
"ou_id": ou_id,
|
||||
"ou_name": ou_name,
|
||||
"is_leader": False,
|
||||
},
|
||||
"area": area,
|
||||
"expiresIn": 3600,
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
payload = response.json()
|
||||
assert payload.get("success") is True, payload
|
||||
data = payload.get("data") or {}
|
||||
assert data.get("access_token"), payload
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def json_data(response: httpx.Response) -> Any:
|
||||
payload = response.json()
|
||||
if isinstance(payload, dict) and "data" in payload:
|
||||
return payload["data"]
|
||||
return payload
|
||||
|
||||
@staticmethod
|
||||
def _format_error(response: httpx.Response) -> str:
|
||||
try:
|
||||
body = response.json()
|
||||
except Exception:
|
||||
body = response.text
|
||||
return f"{response.request.method} {response.request.url} -> {response.status_code}: {body}"
|
||||
|
||||
|
||||
def flatten_route_paths(routes: list[dict[str, Any]]) -> set[str]:
|
||||
paths: set[str] = set()
|
||||
|
||||
def collect(items: list[dict[str, Any]]) -> None:
|
||||
for item in items:
|
||||
route_path = str(item.get("route_path") or "")
|
||||
if route_path:
|
||||
paths.add(route_path)
|
||||
children = item.get("children") or []
|
||||
if isinstance(children, list):
|
||||
collect(children)
|
||||
|
||||
collect(routes)
|
||||
return paths
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from .helpers import ReleaseApiClient, flatten_route_paths
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g1_admin_auth_and_rbac_context(admin_client: ReleaseApiClient) -> None:
|
||||
me_response = admin_client.get("/api/auth/me")
|
||||
me = ReleaseApiClient.json_data(me_response)
|
||||
assert int(me["user_id"]) > 0
|
||||
assert isinstance(me.get("roles"), list) and me["roles"]
|
||||
assert isinstance(me.get("permissions"), list)
|
||||
|
||||
routes_response = admin_client.get("/api/rbac/user/routes")
|
||||
routes_data = ReleaseApiClient.json_data(routes_response)
|
||||
route_paths = flatten_route_paths(routes_data["routes"])
|
||||
assert "/home" in route_paths
|
||||
assert "/settings" in route_paths
|
||||
assert "/role-permissions" in route_paths
|
||||
|
||||
users_response = admin_client.get("/api/v3/rbac/users?page=1&page_size=20")
|
||||
users_data = ReleaseApiClient.json_data(users_response)
|
||||
assert users_data["total"] >= 1
|
||||
assert isinstance(users_data["items"], list)
|
||||
|
||||
org_response = admin_client.get("/api/admin/users/organizations/tree?include_users=false")
|
||||
org_data = ReleaseApiClient.json_data(org_response)
|
||||
assert "organizations" in org_data
|
||||
assert org_data["total_organizations"] >= 1
|
||||
|
||||
tenant_option_response = admin_client.get("/api/v3/tenants/options?feature_key=home.entry_module")
|
||||
tenant_option_data = ReleaseApiClient.json_data(tenant_option_response)
|
||||
assert tenant_option_data["total"] >= 1
|
||||
@@ -0,0 +1,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from .conftest import SeededUser, TenantSeed
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
def _set_module_tenants(admin_client: ReleaseApiClient, module_id: int, module_name: str, tenants: list[TenantSeed], path: str) -> dict:
|
||||
payload = {
|
||||
"name": module_name,
|
||||
"description": "pytest release acceptance only",
|
||||
"path": path,
|
||||
"route_path": path,
|
||||
"tenants": [
|
||||
{
|
||||
"tenant_code": tenant.tenant_code,
|
||||
"tenant_name": tenant.tenant_name,
|
||||
"enabled": True,
|
||||
"sort_order": index,
|
||||
}
|
||||
for index, tenant in enumerate(tenants, start=1)
|
||||
],
|
||||
}
|
||||
response = admin_client.put(f"/api/v3/entry-modules/{module_id}", json=payload, expected_status=200)
|
||||
return ReleaseApiClient.json_data(response)
|
||||
|
||||
|
||||
def _home_module_names(client: ReleaseApiClient) -> list[str]:
|
||||
response = client.get("/api/home/entry-modules")
|
||||
modules = ReleaseApiClient.json_data(response)
|
||||
return [str(item.get("name") or "") for item in modules]
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g2_tenant_detail_and_user_tenant_switch(
|
||||
admin_client: ReleaseApiClient,
|
||||
anonymous_client: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_common_user_a: SeededUser,
|
||||
) -> None:
|
||||
detail_response = admin_client.get(f"/api/v3/tenants/{tenant_a.tenant_code}")
|
||||
detail = ReleaseApiClient.json_data(detail_response)
|
||||
assert detail["tenant_code"] == tenant_a.tenant_code
|
||||
assert detail["tenant_name"] == tenant_a.tenant_name
|
||||
assert "home.entry_module" in detail["feature_keys"]
|
||||
|
||||
switch_response = admin_client.put(
|
||||
f"/api/v3/rbac/users/{tenant_common_user_a.user_id}/tenant",
|
||||
json={"tenant_code": tenant_a.tenant_code},
|
||||
expected_status=200,
|
||||
)
|
||||
switch_data = ReleaseApiClient.json_data(switch_response)
|
||||
assert switch_data["tenant_code"] == tenant_a.tenant_code
|
||||
|
||||
relogin = anonymous_client.login_oauth(
|
||||
sub=tenant_common_user_a.sub,
|
||||
username=tenant_common_user_a.username,
|
||||
nickname=tenant_common_user_a.nickname,
|
||||
ou_id=f"pytest-{tenant_a.tenant_code.lower()}",
|
||||
ou_name=f"{tenant_a.tenant_name}测试组织",
|
||||
area=tenant_a.tenant_name,
|
||||
)
|
||||
assert relogin["user_info"]["tenant_code"] == tenant_a.tenant_code
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g3_entry_module_visibility_follows_tenant_assignment(
|
||||
admin_client: ReleaseApiClient,
|
||||
release_entry_module: dict,
|
||||
release_config,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
common_api_a: ReleaseApiClient,
|
||||
common_api_b: ReleaseApiClient,
|
||||
) -> None:
|
||||
module_id = int(release_entry_module["id"])
|
||||
module_name = str(release_entry_module["name"])
|
||||
|
||||
_set_module_tenants(admin_client, module_id, module_name, [tenant_b], release_config.module_path)
|
||||
names_a = _home_module_names(common_api_a)
|
||||
names_b = _home_module_names(common_api_b)
|
||||
assert module_name not in names_a
|
||||
assert module_name in names_b
|
||||
|
||||
_set_module_tenants(admin_client, module_id, module_name, [tenant_a, tenant_b], release_config.module_path)
|
||||
names_a = _home_module_names(common_api_a)
|
||||
names_b = _home_module_names(common_api_b)
|
||||
assert module_name in names_a
|
||||
assert module_name in names_b
|
||||
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import fitz
|
||||
import pytest
|
||||
|
||||
from .conftest import TenantSeed
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
def _document_ids(items: list[dict]) -> set[int]:
|
||||
return {int(item["documentId"]) for item in items if item.get("documentId") is not None}
|
||||
|
||||
|
||||
def _sample_pdf_bytes(text: str) -> bytes:
|
||||
document = fitz.open()
|
||||
page = document.new_page()
|
||||
page.insert_text((72, 72), text)
|
||||
return document.tobytes()
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g4_document_list_and_detail_respect_tenant_boundary(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_admin_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
make_release_document,
|
||||
) -> None:
|
||||
own_doc = make_release_document(
|
||||
client=tenant_admin_api,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-g4-a-{int(time.time())}.txt",
|
||||
)
|
||||
other_doc = make_release_document(
|
||||
client=tenant_admin_api_b,
|
||||
tenant=tenant_b,
|
||||
file_name=f"pytest-g4-b-{int(time.time())}.txt",
|
||||
)
|
||||
|
||||
own_list = ReleaseApiClient.json_data(tenant_admin_api.get("/api/documents/list?page=1&pageSize=100"))
|
||||
own_ids = _document_ids(own_list["documents"])
|
||||
assert own_doc.document_id in own_ids
|
||||
assert other_doc.document_id not in own_ids
|
||||
|
||||
own_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/documents/{own_doc.document_id}"))
|
||||
assert int(own_detail["documentId"]) == own_doc.document_id
|
||||
assert str(own_detail.get("tenantCode") or "") == tenant_a.tenant_code
|
||||
|
||||
cross_detail = tenant_admin_api.get(f"/api/documents/{other_doc.document_id}", expected_status=404)
|
||||
assert "无权访问" in cross_detail.text or "不存在" in cross_detail.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g4_document_update_append_and_delete_reject_cross_tenant_access(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_admin_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
make_release_document,
|
||||
) -> None:
|
||||
own_doc = make_release_document(
|
||||
client=tenant_admin_api,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-g4-own-{int(time.time())}.pdf",
|
||||
content=_sample_pdf_bytes("pytest g4 own document"),
|
||||
content_type="application/pdf",
|
||||
)
|
||||
|
||||
update_response = tenant_admin_api.put(
|
||||
f"/api/documents/{own_doc.document_id}",
|
||||
json={"remark": "pytest g4 own update"},
|
||||
expected_status=200,
|
||||
)
|
||||
update_data = ReleaseApiClient.json_data(update_response)
|
||||
assert int(update_data["documentId"]) == own_doc.document_id
|
||||
|
||||
append_response = tenant_admin_api.post(
|
||||
f"/api/documents/{own_doc.document_id}/attachments",
|
||||
data={"mergeMode": "new", "remark": "pytest attachment"},
|
||||
files=[("files", ("attachment.pdf", _sample_pdf_bytes("pytest attachment"), "application/pdf"))],
|
||||
expected_status=200,
|
||||
)
|
||||
append_data = ReleaseApiClient.json_data(append_response)
|
||||
new_document_id = int(append_data["documentId"])
|
||||
assert new_document_id != own_doc.document_id
|
||||
assert str(append_data.get("tenantCode") or "") == tenant_a.tenant_code
|
||||
assert int(append_data.get("previousVersionId") or 0) == own_doc.document_id
|
||||
|
||||
cross_update = tenant_admin_api_b.put(
|
||||
f"/api/documents/{new_document_id}",
|
||||
json={"remark": "should fail"},
|
||||
expected_status=404,
|
||||
)
|
||||
assert "无权访问" in cross_update.text or "不存在" in cross_update.text
|
||||
|
||||
cross_append = tenant_admin_api_b.post(
|
||||
f"/api/documents/{new_document_id}/attachments",
|
||||
data={"mergeMode": "new", "remark": "should fail"},
|
||||
files=[("files", ("cross.pdf", _sample_pdf_bytes("cross tenant"), "application/pdf"))],
|
||||
expected_status=404,
|
||||
)
|
||||
assert "无权访问" in cross_append.text or "不存在" in cross_append.text
|
||||
|
||||
cross_delete = tenant_admin_api_b.delete(f"/api/documents/{new_document_id}", expected_status=404)
|
||||
assert "无权访问" in cross_delete.text or "不存在" in cross_delete.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g4_common_user_only_sees_self_created_documents(
|
||||
common_api_a: ReleaseApiClient,
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
make_release_document,
|
||||
) -> None:
|
||||
user_doc = make_release_document(
|
||||
client=common_api_a,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-g4-common-self-{int(time.time())}.txt",
|
||||
)
|
||||
admin_doc = make_release_document(
|
||||
client=tenant_admin_api,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-g4-common-other-{int(time.time())}.txt",
|
||||
)
|
||||
|
||||
own_list = ReleaseApiClient.json_data(common_api_a.get("/api/documents/list?page=1&pageSize=100"))
|
||||
own_ids = _document_ids(own_list["documents"])
|
||||
assert user_doc.document_id in own_ids
|
||||
assert admin_doc.document_id not in own_ids
|
||||
|
||||
own_detail = ReleaseApiClient.json_data(common_api_a.get(f"/api/documents/{user_doc.document_id}"))
|
||||
assert int(own_detail["documentId"]) == user_doc.document_id
|
||||
|
||||
hidden_detail = common_api_a.get(f"/api/documents/{admin_doc.document_id}", expected_status=404)
|
||||
assert "无权访问" in hidden_detail.text or "不存在" in hidden_detail.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g4_govdoc_list_endpoint_does_not_fail_when_backfilling_version_groups(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
) -> None:
|
||||
response = tenant_admin_api.get("/api/govdoc/documents?page=1&pageSize=10", expected_status=200)
|
||||
payload = response.json()
|
||||
assert payload.get("code") in {0, 200}, payload
|
||||
assert "data" in payload, payload
|
||||
@@ -0,0 +1,175 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from .conftest import ReleaseDatasetSeed, TenantSeed
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
def _dataset_ids(items: list[dict]) -> set[int]:
|
||||
return {int(item["id"]) for item in items if item.get("id") is not None}
|
||||
|
||||
|
||||
def _app_ids(items: list[dict]) -> set[str]:
|
||||
return {str(item["appId"]) for item in items if item.get("appId") is not None}
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_rag_dataset_admin_scope_is_limited_to_own_tenant(
|
||||
admin_client: ReleaseApiClient,
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_admin_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
make_release_dataset,
|
||||
) -> None:
|
||||
dataset_a = make_release_dataset(
|
||||
tenant=tenant_a,
|
||||
dataset_name=f"Pytest G5 A {int(time.time())}",
|
||||
)
|
||||
dataset_b = make_release_dataset(
|
||||
tenant=tenant_b,
|
||||
dataset_name=f"Pytest G5 B {int(time.time())}",
|
||||
)
|
||||
|
||||
global_admin_list = ReleaseApiClient.json_data(admin_client.get("/api/v3/rag/datasets/admin?page=1&pageSize=100"))
|
||||
ids_a = _dataset_ids(global_admin_list["data"])
|
||||
assert dataset_a.dataset_id in ids_a
|
||||
assert dataset_b.dataset_id in ids_a
|
||||
|
||||
tenant_admin_list = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.get("/api/v3/rag/datasets/admin?page=1&pageSize=100")
|
||||
)
|
||||
tenant_admin_ids = _dataset_ids(tenant_admin_list["data"])
|
||||
assert dataset_a.dataset_id in tenant_admin_ids
|
||||
assert dataset_b.dataset_id not in tenant_admin_ids
|
||||
|
||||
forbidden_query = tenant_admin_api.get(
|
||||
f"/api/v3/rag/datasets/admin?page=1&pageSize=100&tenant_code={tenant_b.tenant_code}",
|
||||
expected_status=403,
|
||||
)
|
||||
assert "本地区知识库配置" in forbidden_query.text or "本租户知识库" in forbidden_query.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_rag_dataset_detail_and_update_respect_tenant_boundary(
|
||||
admin_client: ReleaseApiClient,
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
make_release_dataset,
|
||||
) -> None:
|
||||
dataset_a = make_release_dataset(
|
||||
tenant=tenant_a,
|
||||
dataset_name=f"Pytest G5 Detail A {int(time.time())}",
|
||||
)
|
||||
dataset_b = make_release_dataset(
|
||||
tenant=tenant_b,
|
||||
dataset_name=f"Pytest G5 Detail B {int(time.time())}",
|
||||
)
|
||||
|
||||
own_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/v3/rag/datasets/{dataset_a.dataset_id}"))
|
||||
assert int(own_detail["id"]) == dataset_a.dataset_id
|
||||
assert str(own_detail.get("tenantCode") or "") == tenant_a.tenant_code
|
||||
|
||||
hidden_detail = ReleaseApiClient.json_data(tenant_admin_api.get(f"/api/v3/rag/datasets/{dataset_b.dataset_id}"))
|
||||
assert hidden_detail is None
|
||||
|
||||
own_update = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.patch(
|
||||
f"/api/v3/rag/datasets/{dataset_a.dataset_id}",
|
||||
json={"name": f"{dataset_a.dataset_name}-tenant-updated"},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
assert str(own_update["name"]).endswith("-tenant-updated")
|
||||
|
||||
hidden_update = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.patch(
|
||||
f"/api/v3/rag/datasets/{dataset_b.dataset_id}",
|
||||
json={"name": "should-not-work"},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
assert hidden_update is None
|
||||
|
||||
admin_update = ReleaseApiClient.json_data(
|
||||
admin_client.patch(
|
||||
f"/api/v3/rag/datasets/{dataset_a.dataset_id}",
|
||||
json={"name": f"{dataset_a.dataset_name}-updated"},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
assert str(admin_update["name"]).endswith("-updated")
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_rag_apps_and_public_dataset_visibility(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_admin_api_b: ReleaseApiClient,
|
||||
common_api_a: ReleaseApiClient,
|
||||
common_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
make_release_dataset,
|
||||
) -> None:
|
||||
dataset_a = make_release_dataset(
|
||||
tenant=tenant_a,
|
||||
dataset_name=f"Pytest G5 App A {int(time.time())}",
|
||||
is_default=True,
|
||||
)
|
||||
dataset_b = make_release_dataset(
|
||||
tenant=tenant_b,
|
||||
dataset_name=f"Pytest G5 App B {int(time.time())}",
|
||||
is_default=True,
|
||||
)
|
||||
public_dataset = make_release_dataset(
|
||||
tenant=tenant_a,
|
||||
dataset_name=f"Pytest G5 Public {int(time.time())}",
|
||||
is_public=True,
|
||||
is_default=False,
|
||||
)
|
||||
|
||||
apps_a = ReleaseApiClient.json_data(common_api_a.get("/api/v3/rag/apps"))
|
||||
app_ids_a = _app_ids(apps_a["data"])
|
||||
assert dataset_a.app_id is not None
|
||||
assert str(dataset_a.app_id) in app_ids_a
|
||||
assert dataset_b.app_id is not None
|
||||
assert str(dataset_b.app_id) not in app_ids_a
|
||||
assert public_dataset.app_id is not None
|
||||
assert str(public_dataset.app_id) in app_ids_a
|
||||
|
||||
apps_b = ReleaseApiClient.json_data(common_api_b.get("/api/v3/rag/apps"))
|
||||
app_ids_b = _app_ids(apps_b["data"])
|
||||
assert str(dataset_b.app_id) in app_ids_b
|
||||
assert str(dataset_a.app_id) not in app_ids_b
|
||||
assert str(public_dataset.app_id) in app_ids_b
|
||||
|
||||
default_app_a = ReleaseApiClient.json_data(common_api_a.get("/api/v3/rag/apps/default"))
|
||||
assert default_app_a is not None
|
||||
assert str(default_app_a.get("tenantCode") or "") in {tenant_a.tenant_code, public_dataset.tenant_code}
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_common_user_cannot_access_rag_admin_endpoints(
|
||||
common_api_a: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
) -> None:
|
||||
forbidden_admin_list = common_api_a.get("/api/v3/rag/datasets/admin?page=1&pageSize=20", expected_status=403)
|
||||
assert "管理知识库权限" in forbidden_admin_list.text
|
||||
|
||||
forbidden_create = common_api_a.post(
|
||||
"/api/v3/rag/datasets/admin",
|
||||
json={
|
||||
"tenant_code": tenant_a.tenant_code,
|
||||
"tenant_name": tenant_a.tenant_name,
|
||||
"area": tenant_a.tenant_name,
|
||||
"name": f"should-forbid-{int(time.time())}",
|
||||
"description": "forbidden",
|
||||
"status": 1,
|
||||
},
|
||||
expected_status=403,
|
||||
)
|
||||
assert "创建知识库权限" in forbidden_create.text
|
||||
@@ -0,0 +1,222 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from .conftest import SeededUser, TenantSeed
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_rule_sets_are_globally_readable_but_bindings_and_groups_follow_tenant_scope(
|
||||
admin_client: ReleaseApiClient,
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
common_api_a: ReleaseApiClient,
|
||||
) -> None:
|
||||
admin_rule_sets = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets"))
|
||||
tenant_rule_sets = ReleaseApiClient.json_data(tenant_admin_api.get("/api/rule-sets"))
|
||||
common_rule_sets = ReleaseApiClient.json_data(common_api_a.get("/api/rule-sets"))
|
||||
|
||||
assert len(admin_rule_sets) > 0
|
||||
assert len(tenant_rule_sets) == len(admin_rule_sets)
|
||||
assert len(common_rule_sets) == len(admin_rule_sets)
|
||||
|
||||
admin_bindings = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets/bindings"))
|
||||
tenant_bindings = ReleaseApiClient.json_data(tenant_admin_api.get("/api/rule-sets/bindings"))
|
||||
common_bindings = ReleaseApiClient.json_data(common_api_a.get("/api/rule-sets/bindings"))
|
||||
assert isinstance(admin_bindings, list)
|
||||
assert len(admin_bindings) > 0
|
||||
assert tenant_bindings == []
|
||||
assert common_bindings == []
|
||||
|
||||
admin_groups = admin_client.get("/api/v3/evaluation-point-groups/all")
|
||||
tenant_groups = tenant_admin_api.get("/api/v3/evaluation-point-groups/all")
|
||||
common_groups = common_api_a.get("/api/v3/evaluation-point-groups/all")
|
||||
assert ReleaseApiClient.json_data(admin_groups) != []
|
||||
assert ReleaseApiClient.json_data(tenant_groups) == []
|
||||
assert ReleaseApiClient.json_data(common_groups) == []
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_group_detail_and_children_follow_same_scope_boundary(
|
||||
admin_client: ReleaseApiClient,
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
common_api_a: ReleaseApiClient,
|
||||
) -> None:
|
||||
admin_groups = ReleaseApiClient.json_data(admin_client.get("/api/v3/evaluation-point-groups/all"))
|
||||
assert admin_groups != []
|
||||
|
||||
target_group = admin_groups[0]
|
||||
target_group_id = int(target_group["id"])
|
||||
|
||||
admin_detail = ReleaseApiClient.json_data(
|
||||
admin_client.get(f"/api/v3/evaluation-point-groups/{target_group_id}")
|
||||
)
|
||||
assert int(admin_detail["id"]) == target_group_id
|
||||
|
||||
admin_children = admin_client.get(
|
||||
f"/api/v3/evaluation-point-groups/{target_group_id}/children?page=1&page_size=20"
|
||||
).json()
|
||||
assert isinstance(admin_children, dict)
|
||||
assert "data" in admin_children
|
||||
|
||||
tenant_detail = tenant_admin_api.get(
|
||||
f"/api/v3/evaluation-point-groups/{target_group_id}",
|
||||
expected_status=404,
|
||||
)
|
||||
assert "规则分组不存在" in tenant_detail.text
|
||||
|
||||
tenant_children = tenant_admin_api.get(
|
||||
f"/api/v3/evaluation-point-groups/{target_group_id}/children?page=1&page_size=20",
|
||||
expected_status=404,
|
||||
)
|
||||
assert "规则分组不存在" in tenant_children.text
|
||||
|
||||
common_detail = common_api_a.get(
|
||||
f"/api/v3/evaluation-point-groups/{target_group_id}",
|
||||
expected_status=404,
|
||||
)
|
||||
assert "规则分组不存在" in common_detail.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_cross_review_rejects_cross_tenant_documents_and_members(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
common_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
tenant_common_user_a: SeededUser,
|
||||
tenant_common_user_b: SeededUser,
|
||||
make_release_document,
|
||||
) -> None:
|
||||
doc_a = make_release_document(
|
||||
client=tenant_admin_api,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-cross-a-{int(time.time())}.txt",
|
||||
)
|
||||
doc_b = make_release_document(
|
||||
client=common_api_b,
|
||||
tenant=tenant_b,
|
||||
file_name=f"pytest-cross-b-{int(time.time())}.txt",
|
||||
)
|
||||
|
||||
same_tenant = tenant_admin_api.post(
|
||||
"/api/v3/cross-review/tasks",
|
||||
json={
|
||||
"taskName": f"Pytest Cross Same {int(time.time())}",
|
||||
"taskType": "CITY",
|
||||
"memberUserIds": [tenant_common_user_a.user_id],
|
||||
"principalUserIds": [],
|
||||
"documentIds": [doc_a.document_id],
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
same_tenant_data = ReleaseApiClient.json_data(same_tenant)
|
||||
assert int(same_tenant_data["documentCount"]) == 1
|
||||
|
||||
cross_doc = tenant_admin_api.post(
|
||||
"/api/v3/cross-review/tasks",
|
||||
json={
|
||||
"taskName": f"Pytest Cross Doc {int(time.time())}",
|
||||
"taskType": "CITY",
|
||||
"memberUserIds": [tenant_common_user_a.user_id],
|
||||
"principalUserIds": [],
|
||||
"documentIds": [doc_b.document_id],
|
||||
},
|
||||
expected_status=403,
|
||||
)
|
||||
assert "其他租户文档" in cross_doc.text
|
||||
|
||||
cross_member = tenant_admin_api.post(
|
||||
"/api/v3/cross-review/tasks",
|
||||
json={
|
||||
"taskName": f"Pytest Cross Member {int(time.time())}",
|
||||
"taskType": "CITY",
|
||||
"memberUserIds": [tenant_common_user_b.user_id],
|
||||
"principalUserIds": [],
|
||||
"documentIds": [doc_a.document_id],
|
||||
},
|
||||
expected_status=403,
|
||||
)
|
||||
assert "其他租户用户" in cross_member.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g5_cross_review_task_visibility_progress_and_documents_follow_member_scope(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_admin_api_b: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_common_user_a: SeededUser,
|
||||
make_release_document,
|
||||
) -> None:
|
||||
doc_a = make_release_document(
|
||||
client=tenant_admin_api,
|
||||
tenant=tenant_a,
|
||||
file_name=f"pytest-cross-query-{int(time.time())}.txt",
|
||||
)
|
||||
created = tenant_admin_api.post(
|
||||
"/api/v3/cross-review/tasks",
|
||||
json={
|
||||
"taskName": f"Pytest Query Task {int(time.time())}",
|
||||
"taskType": "CITY",
|
||||
"memberUserIds": [tenant_common_user_a.user_id],
|
||||
"principalUserIds": [],
|
||||
"documentIds": [doc_a.document_id],
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
created_data = ReleaseApiClient.json_data(created)
|
||||
task_id = int(created_data["taskId"])
|
||||
|
||||
queried = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.post(
|
||||
"/api/v3/cross-review/tasks/query",
|
||||
json={"page": 1, "pageSize": 50, "keyword": "Pytest Query Task"},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
task_ids = {int(item["taskId"]) for item in queried["items"]}
|
||||
assert task_id in task_ids
|
||||
|
||||
created_item = next(item for item in queried["items"] if int(item["taskId"]) == task_id)
|
||||
evaluation_tenant_codes = {str(item.get("tenantCode") or "") for item in created_item.get("evaluationTenants") or []}
|
||||
assert tenant_a.tenant_code in evaluation_tenant_codes
|
||||
|
||||
progress = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.get(f"/api/v3/cross-review/tasks/{task_id}/progress", expected_status=200)
|
||||
)
|
||||
assert int(progress["taskId"]) == task_id
|
||||
assert int(progress["totalDocuments"]) == 1
|
||||
|
||||
documents = ReleaseApiClient.json_data(
|
||||
tenant_admin_api.get(
|
||||
f"/api/v3/cross-review/tasks/{task_id}/documents?page=1&pageSize=20",
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
assert int(documents["taskId"]) == task_id
|
||||
returned_document_ids = {int(item["documentId"]) for item in documents["items"]}
|
||||
assert doc_a.document_id in returned_document_ids
|
||||
|
||||
cross_tenant_query = ReleaseApiClient.json_data(
|
||||
tenant_admin_api_b.post(
|
||||
"/api/v3/cross-review/tasks/query",
|
||||
json={"page": 1, "pageSize": 50, "keyword": "Pytest Query Task"},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
cross_tenant_task_ids = {int(item["taskId"]) for item in cross_tenant_query["items"]}
|
||||
assert task_id not in cross_tenant_task_ids
|
||||
|
||||
cross_tenant_progress = tenant_admin_api_b.get(
|
||||
f"/api/v3/cross-review/tasks/{task_id}/progress",
|
||||
expected_status=403,
|
||||
)
|
||||
assert "当前用户不是交叉评查任务成员" in cross_tenant_progress.text
|
||||
|
||||
cross_tenant_documents = tenant_admin_api_b.get(
|
||||
f"/api/v3/cross-review/tasks/{task_id}/documents?page=1&pageSize=20",
|
||||
expected_status=403,
|
||||
)
|
||||
assert "当前用户不是交叉评查任务成员" in cross_tenant_documents.text
|
||||
@@ -0,0 +1,159 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
def _select_rule_set(client: ReleaseApiClient) -> dict[str, Any]:
|
||||
preferred_rule_type = os.getenv("LEAUDIT_TEST_RULE_TYPE", "contract.entrust").strip()
|
||||
rule_sets = ReleaseApiClient.json_data(client.get("/api/rule-sets"))
|
||||
assert isinstance(rule_sets, list)
|
||||
assert rule_sets, "当前环境没有规则集,无法测试版本管理"
|
||||
|
||||
for item in rule_sets:
|
||||
if str(item.get("ruleType") or "") == preferred_rule_type and item.get("currentVersionId"):
|
||||
return item
|
||||
|
||||
for item in rule_sets:
|
||||
if item.get("currentVersionId"):
|
||||
return item
|
||||
|
||||
pytest.skip("当前环境没有带 currentVersionId 的规则集,无法测试发布/回滚闭环")
|
||||
|
||||
|
||||
def _set_metadata_version(yaml_text: str, version_no: str) -> str:
|
||||
lines = yaml_text.splitlines()
|
||||
metadata_index: int | None = None
|
||||
version_pattern = re.compile(r"^(\s*)version\s*:")
|
||||
|
||||
for index, line in enumerate(lines):
|
||||
if line.strip() == "metadata:":
|
||||
metadata_index = index
|
||||
continue
|
||||
if metadata_index is None:
|
||||
continue
|
||||
if line and not line.startswith((" ", "\t")):
|
||||
break
|
||||
match = version_pattern.match(line)
|
||||
if match:
|
||||
lines[index] = f"{match.group(1)}version: '{version_no}'"
|
||||
return "\n".join(lines) + ("\n" if yaml_text.endswith("\n") else "")
|
||||
|
||||
if metadata_index is None:
|
||||
return f"metadata:\n version: '{version_no}'\n{yaml_text}"
|
||||
|
||||
lines.insert(metadata_index + 1, f" version: '{version_no}'")
|
||||
return "\n".join(lines) + ("\n" if yaml_text.endswith("\n") else "")
|
||||
|
||||
|
||||
def _rule_set_by_type(client: ReleaseApiClient, rule_type: str) -> dict[str, Any]:
|
||||
rule_sets = ReleaseApiClient.json_data(client.get("/api/rule-sets"))
|
||||
for item in rule_sets:
|
||||
if str(item.get("ruleType") or "") == rule_type:
|
||||
return item
|
||||
raise AssertionError(f"规则集不存在: {rule_type}")
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g6_rule_detail_version_management_save_publish_and_rollback(admin_client: ReleaseApiClient) -> None:
|
||||
"""覆盖规则配置详情页的版本管理闭环。
|
||||
|
||||
该用例会创建一个真实历史版本并短暂发布,最后回滚到执行前的 currentVersionId。
|
||||
"""
|
||||
|
||||
target_rule_set = _select_rule_set(admin_client)
|
||||
rule_type = str(target_rule_set["ruleType"])
|
||||
original_rule_set_id = int(target_rule_set["id"])
|
||||
original_current_version_id = int(target_rule_set["currentVersionId"])
|
||||
original_tenant_code = str(target_rule_set.get("effectiveTenantCode") or "")
|
||||
|
||||
versions_before = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/{rule_type}/versions"))
|
||||
assert isinstance(versions_before, list)
|
||||
assert {int(item["ruleSetId"]) for item in versions_before} == {original_rule_set_id}
|
||||
assert original_current_version_id in {int(item["id"]) for item in versions_before}
|
||||
|
||||
content = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/versions/{original_current_version_id}/content"))
|
||||
assert int(content["ruleSetId"]) == original_rule_set_id
|
||||
assert str(content["ruleType"]) == rule_type
|
||||
assert str(content["yamlText"]).strip()
|
||||
|
||||
new_version_no = f"pytest-vm-{int(time.time())}"
|
||||
yaml_text = _set_metadata_version(str(content["yamlText"]), new_version_no)
|
||||
created_version_id: int | None = None
|
||||
|
||||
try:
|
||||
created = ReleaseApiClient.json_data(
|
||||
admin_client.post(
|
||||
f"/api/rule-sets/{rule_type}/versions",
|
||||
json={
|
||||
"yamlText": yaml_text,
|
||||
"changeNote": f"pytest rule detail version management smoke {new_version_no}",
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
created_version_id = int(created["id"])
|
||||
assert int(created["ruleSetId"]) == original_rule_set_id
|
||||
assert str(created["versionNo"]) == new_version_no
|
||||
assert str(created["status"]) == "draft"
|
||||
|
||||
versions_after_create = ReleaseApiClient.json_data(admin_client.get(f"/api/rule-sets/{rule_type}/versions"))
|
||||
assert created_version_id in {int(item["id"]) for item in versions_after_create}
|
||||
assert {int(item["ruleSetId"]) for item in versions_after_create} == {original_rule_set_id}
|
||||
|
||||
published = ReleaseApiClient.json_data(
|
||||
admin_client.post(
|
||||
f"/api/rule-sets/{rule_type}/publish",
|
||||
json={"versionId": created_version_id},
|
||||
expected_status=200,
|
||||
)
|
||||
)
|
||||
assert int(published["id"]) == created_version_id
|
||||
assert int(published["ruleSetId"]) == original_rule_set_id
|
||||
assert str(published["status"]) == "published"
|
||||
|
||||
current_after_publish = _rule_set_by_type(admin_client, rule_type)
|
||||
assert int(current_after_publish["id"]) == original_rule_set_id
|
||||
assert int(current_after_publish["currentVersionId"]) == created_version_id
|
||||
assert str(current_after_publish.get("effectiveTenantCode") or "") == original_tenant_code
|
||||
|
||||
finally:
|
||||
if created_version_id:
|
||||
admin_client.post(
|
||||
f"/api/rule-sets/{rule_type}/rollback",
|
||||
json={"versionId": original_current_version_id},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
restored = _rule_set_by_type(admin_client, rule_type)
|
||||
assert int(restored["id"]) == original_rule_set_id
|
||||
assert int(restored["currentVersionId"]) == original_current_version_id
|
||||
assert str(restored.get("effectiveTenantCode") or "") == original_tenant_code
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_g6_rule_detail_version_management_rejects_cross_rule_set_publish(admin_client: ReleaseApiClient) -> None:
|
||||
"""发布接口必须拒绝把其他规则集的版本发布到当前规则类型。"""
|
||||
|
||||
rule_sets = ReleaseApiClient.json_data(admin_client.get("/api/rule-sets"))
|
||||
candidates = [item for item in rule_sets if item.get("currentVersionId")]
|
||||
if len(candidates) < 2:
|
||||
pytest.skip("当前环境少于两个带 currentVersionId 的规则集,无法测试跨规则集发布拦截")
|
||||
|
||||
left = candidates[0]
|
||||
right = next((item for item in candidates[1:] if str(item["ruleType"]) != str(left["ruleType"])), None)
|
||||
if right is None:
|
||||
pytest.skip("当前环境没有可用于跨规则类型发布拦截的第二个规则集")
|
||||
|
||||
response = admin_client.post(
|
||||
f"/api/rule-sets/{left['ruleType']}/publish",
|
||||
json={"versionId": int(right["currentVersionId"])},
|
||||
expected_status=403,
|
||||
)
|
||||
assert "当前租户不能发布或回滚其他租户的规则版本" in response.text
|
||||
@@ -0,0 +1,105 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from .conftest import SeededUser, TenantSeed
|
||||
from .helpers import ReleaseApiClient
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_global_admin_can_query_cross_tenant_scope(
|
||||
admin_client: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
) -> None:
|
||||
data_a = ReleaseApiClient.json_data(
|
||||
admin_client.get(f"/api/v3/rbac/users?page=1&page_size=100&tenant_code={tenant_a.tenant_code}")
|
||||
)
|
||||
data_b = ReleaseApiClient.json_data(
|
||||
admin_client.get(f"/api/v3/rbac/users?page=1&page_size=100&tenant_code={tenant_b.tenant_code}")
|
||||
)
|
||||
assert isinstance(data_a["items"], list)
|
||||
assert isinstance(data_b["items"], list)
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_tenant_admin_is_limited_to_own_tenant_scope(
|
||||
tenant_admin_api: ReleaseApiClient,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
tenant_common_user_a: SeededUser,
|
||||
tenant_common_user_b: SeededUser,
|
||||
) -> None:
|
||||
own_scope = ReleaseApiClient.json_data(tenant_admin_api.get("/api/v3/rbac/users?page=1&page_size=100"))
|
||||
tenant_codes = {str(item.get("tenant_code") or "") for item in own_scope["items"]}
|
||||
assert tenant_a.tenant_code in tenant_codes
|
||||
assert tenant_b.tenant_code not in tenant_codes
|
||||
|
||||
forbidden_query = tenant_admin_api.get(
|
||||
f"/api/v3/rbac/users?page=1&page_size=20&tenant_code={tenant_b.tenant_code}",
|
||||
expected_status=403,
|
||||
)
|
||||
assert "不能查询其他租户用户" in forbidden_query.text
|
||||
|
||||
same_tenant_update = tenant_admin_api.put(
|
||||
f"/api/v3/rbac/users/{tenant_common_user_a.user_id}/tenant",
|
||||
json={"tenant_code": tenant_a.tenant_code},
|
||||
expected_status=200,
|
||||
)
|
||||
same_tenant_data = ReleaseApiClient.json_data(same_tenant_update)
|
||||
assert same_tenant_data["tenant_code"] == tenant_a.tenant_code
|
||||
|
||||
cross_tenant_update = tenant_admin_api.put(
|
||||
f"/api/v3/rbac/users/{tenant_common_user_b.user_id}/tenant",
|
||||
json={"tenant_code": tenant_a.tenant_code},
|
||||
expected_status=403,
|
||||
)
|
||||
assert "不能修改其他租户用户" in cross_tenant_update.text
|
||||
|
||||
|
||||
@pytest.mark.release
|
||||
def test_common_user_cannot_access_management_but_keeps_business_entry(
|
||||
common_api_a: ReleaseApiClient,
|
||||
release_entry_module: dict,
|
||||
admin_client: ReleaseApiClient,
|
||||
release_config,
|
||||
tenant_a: TenantSeed,
|
||||
tenant_b: TenantSeed,
|
||||
) -> None:
|
||||
module_id = int(release_entry_module["id"])
|
||||
module_name = str(release_entry_module["name"])
|
||||
admin_client.put(
|
||||
f"/api/v3/entry-modules/{module_id}",
|
||||
json={
|
||||
"name": module_name,
|
||||
"description": "pytest release acceptance only",
|
||||
"path": release_config.module_path,
|
||||
"route_path": release_config.module_path,
|
||||
"tenants": [
|
||||
{
|
||||
"tenant_code": tenant_a.tenant_code,
|
||||
"tenant_name": tenant_a.tenant_name,
|
||||
"enabled": True,
|
||||
"sort_order": 1,
|
||||
},
|
||||
{
|
||||
"tenant_code": tenant_b.tenant_code,
|
||||
"tenant_name": tenant_b.tenant_name,
|
||||
"enabled": True,
|
||||
"sort_order": 2,
|
||||
},
|
||||
],
|
||||
},
|
||||
expected_status=200,
|
||||
)
|
||||
|
||||
users_response = common_api_a.get("/api/v3/rbac/users?page=1&page_size=20", expected_status=403)
|
||||
assert "系统设置管理权限" in users_response.text
|
||||
|
||||
tenants_response = common_api_a.get("/api/v3/tenants", expected_status=403)
|
||||
assert "租户" in tenants_response.text
|
||||
|
||||
home_response = common_api_a.get("/api/home/entry-modules")
|
||||
home_modules = ReleaseApiClient.json_data(home_response)
|
||||
home_names = [str(item.get("name") or "") for item in home_modules]
|
||||
assert module_name in home_names
|
||||
@@ -0,0 +1,174 @@
|
||||
import asyncio
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleConfigServiceImpl import RuleConfigServiceImpl
|
||||
|
||||
|
||||
def test_rule_config_source_fields_are_backfilled_from_binding_and_rule_set_meta():
|
||||
service = RuleConfigServiceImpl()
|
||||
|
||||
tenant_binding = {"id": 101, "group_id": 11, "rule_set_id": 201, "tenant_code": "MZ"}
|
||||
provincial_binding = {"id": 102, "group_id": 12, "rule_set_id": 202, "tenant_code": "PROVINCIAL"}
|
||||
public_binding = {"id": 103, "group_id": 13, "rule_set_id": 203, "tenant_code": "PUBLIC"}
|
||||
rule_set_map = {
|
||||
201: {
|
||||
"rule_type": "contract",
|
||||
"rule_name": "Tenant Rule",
|
||||
"current_version_id": 301,
|
||||
"fallback_version_id": None,
|
||||
"has_usable_version": True,
|
||||
"usable_rule_count": 2,
|
||||
"source_rule_set_id": 901,
|
||||
},
|
||||
202: {
|
||||
"rule_type": "contract",
|
||||
"rule_name": "Provincial Rule",
|
||||
"current_version_id": 302,
|
||||
"fallback_version_id": None,
|
||||
"has_usable_version": True,
|
||||
"usable_rule_count": 1,
|
||||
"source_rule_set_id": 902,
|
||||
},
|
||||
203: {
|
||||
"rule_type": "contract",
|
||||
"rule_name": "Public Rule",
|
||||
"current_version_id": 303,
|
||||
"fallback_version_id": None,
|
||||
"has_usable_version": True,
|
||||
"usable_rule_count": 1,
|
||||
"source_rule_set_id": 903,
|
||||
},
|
||||
}
|
||||
|
||||
row = {
|
||||
"group_id": 11,
|
||||
"root_group_id": 1,
|
||||
"document_type_id": 21,
|
||||
"document_type_name": "合同",
|
||||
"main_type": "主类",
|
||||
"subtype": "子类",
|
||||
"entry_module_name": "模块",
|
||||
}
|
||||
|
||||
async def run():
|
||||
service._load_effective_binding = fake_load_effective_binding
|
||||
service._load_yaml_text_by_version_id = fake_load_yaml_text_by_version_id
|
||||
service._load_latest_version_id = fake_load_latest_version_id
|
||||
service._load_current_user = fake_load_current_user
|
||||
|
||||
tenant_pack = await service._build_pack_vo(row, rule_set_map, CurrentUserId=1)
|
||||
provincial_pack = await service._build_pack_vo({**row, "group_id": 12}, rule_set_map, CurrentUserId=1)
|
||||
public_pack = await service._build_pack_vo({**row, "group_id": 13}, rule_set_map, CurrentUserId=1)
|
||||
|
||||
tenant_summary = service._build_pack_summary_item(
|
||||
row={**row, "group_id": 11},
|
||||
binding=tenant_binding,
|
||||
rule_set_map=rule_set_map,
|
||||
latest_version_map={},
|
||||
current_user={"tenant_code": "MZ", "is_global": False},
|
||||
)
|
||||
provincial_summary = service._build_pack_summary_item(
|
||||
row={**row, "group_id": 12},
|
||||
binding=provincial_binding,
|
||||
rule_set_map=rule_set_map,
|
||||
latest_version_map={},
|
||||
current_user={"tenant_code": "MZ", "is_global": False},
|
||||
)
|
||||
public_summary = service._build_pack_summary_item(
|
||||
row={**row, "group_id": 13},
|
||||
binding=public_binding,
|
||||
rule_set_map=rule_set_map,
|
||||
latest_version_map={},
|
||||
current_user={"tenant_code": "MZ", "is_global": False},
|
||||
)
|
||||
return tenant_pack, provincial_pack, public_pack, tenant_summary, provincial_summary, public_summary
|
||||
|
||||
async def fake_load_effective_binding(group_id: int, CurrentUserId=None):
|
||||
return {
|
||||
11: tenant_binding,
|
||||
12: provincial_binding,
|
||||
13: public_binding,
|
||||
}[group_id]
|
||||
|
||||
async def fake_load_yaml_text_by_version_id(version_id: int):
|
||||
return f"yaml-{version_id}"
|
||||
|
||||
async def fake_load_latest_version_id(rule_set_id: int):
|
||||
return None
|
||||
|
||||
async def fake_load_current_user(CurrentUserId=None):
|
||||
return {"tenant_code": "MZ", "is_global": False}
|
||||
|
||||
tenant_pack, provincial_pack, public_pack, tenant_summary, provincial_summary, public_summary = asyncio.run(run())
|
||||
|
||||
assert tenant_pack.effectiveTenantCode == "MZ"
|
||||
assert tenant_pack.effectiveScopeType == "TENANT"
|
||||
assert tenant_pack.isInherited is False
|
||||
assert tenant_pack.sourceRuleSetId == 901
|
||||
|
||||
assert provincial_pack.effectiveTenantCode == "PROVINCIAL"
|
||||
assert provincial_pack.effectiveScopeType == "PROVINCIAL"
|
||||
assert provincial_pack.isInherited is True
|
||||
assert provincial_pack.sourceRuleSetId == 902
|
||||
|
||||
assert public_pack.effectiveTenantCode == "PUBLIC"
|
||||
assert public_pack.effectiveScopeType == "PUBLIC"
|
||||
assert public_pack.isInherited is True
|
||||
assert public_pack.sourceRuleSetId == 903
|
||||
|
||||
assert tenant_summary["effectiveTenantCode"] == "MZ"
|
||||
assert tenant_summary["effectiveScopeType"] == "TENANT"
|
||||
assert tenant_summary["isInherited"] is False
|
||||
assert tenant_summary["sourceRuleSetId"] == 901
|
||||
|
||||
assert provincial_summary["effectiveTenantCode"] == "PROVINCIAL"
|
||||
assert provincial_summary["effectiveScopeType"] == "PROVINCIAL"
|
||||
assert provincial_summary["isInherited"] is True
|
||||
assert provincial_summary["sourceRuleSetId"] == 902
|
||||
|
||||
assert public_summary["effectiveTenantCode"] == "PUBLIC"
|
||||
assert public_summary["effectiveScopeType"] == "PUBLIC"
|
||||
assert public_summary["isInherited"] is True
|
||||
assert public_summary["sourceRuleSetId"] == 903
|
||||
|
||||
|
||||
def test_rule_config_pack_does_not_fallback_to_cross_tenant_rule_set_latest_version():
|
||||
service = RuleConfigServiceImpl()
|
||||
row = {
|
||||
"group_id": 11,
|
||||
"root_group_id": 1,
|
||||
"document_type_id": 21,
|
||||
"document_type_name": "合同",
|
||||
"main_type": "主类",
|
||||
"subtype": "子类",
|
||||
"entry_module_name": "模块",
|
||||
}
|
||||
cross_tenant_binding = {"id": 101, "group_id": 11, "rule_set_id": 999, "tenant_code": "JY"}
|
||||
latest_version_calls: list[int] = []
|
||||
|
||||
async def run():
|
||||
service._load_effective_binding = fake_load_effective_binding
|
||||
service._load_yaml_text_by_version_id = fake_load_yaml_text_by_version_id
|
||||
service._load_latest_version_id = fake_load_latest_version_id
|
||||
service._load_current_user = fake_load_current_user
|
||||
|
||||
return await service._build_pack_vo(row, {}, CurrentUserId=1)
|
||||
|
||||
async def fake_load_effective_binding(group_id: int, CurrentUserId=None):
|
||||
return cross_tenant_binding
|
||||
|
||||
async def fake_load_yaml_text_by_version_id(version_id: int):
|
||||
return f"yaml-{version_id}"
|
||||
|
||||
async def fake_load_latest_version_id(rule_set_id: int):
|
||||
latest_version_calls.append(rule_set_id)
|
||||
return 10010
|
||||
|
||||
async def fake_load_current_user(CurrentUserId=None):
|
||||
return {"tenant_code": "MZ", "is_global": False}
|
||||
|
||||
pack = asyncio.run(run())
|
||||
|
||||
assert pack.ruleSetId is None
|
||||
assert pack.resolvedVersionId is None
|
||||
assert pack.yamlText == ""
|
||||
assert latest_version_calls == []
|
||||
@@ -0,0 +1,65 @@
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.evaluationPointGroupServiceImpl import (
|
||||
EvaluationPointGroupServiceImpl,
|
||||
)
|
||||
|
||||
|
||||
def test_binding_scope_payload_for_tenant_user_uses_exact_tenant():
|
||||
service = EvaluationPointGroupServiceImpl()
|
||||
|
||||
payload = service._build_binding_scope_payload(
|
||||
current_user={"tenant_code": "MZ", "tenant_name": "梅州", "is_global": False},
|
||||
rule_set_meta={"effective_tenant_code": "PROVINCIAL", "effective_scope_type": "PROVINCIAL"},
|
||||
)
|
||||
|
||||
assert payload == {
|
||||
"tenant_code": "MZ",
|
||||
"scope_type": "TENANT",
|
||||
"tenant_name_snapshot": "梅州",
|
||||
}
|
||||
|
||||
|
||||
def test_binding_scope_payload_for_global_user_inherits_rule_set_scope():
|
||||
service = EvaluationPointGroupServiceImpl()
|
||||
|
||||
payload = service._build_binding_scope_payload(
|
||||
current_user={"tenant_code": None, "tenant_name": None, "is_global": True},
|
||||
rule_set_meta={"effective_tenant_code": "PUBLIC", "effective_scope_type": "PUBLIC"},
|
||||
)
|
||||
|
||||
assert payload == {
|
||||
"tenant_code": "PUBLIC",
|
||||
"scope_type": "PUBLIC",
|
||||
"tenant_name_snapshot": None,
|
||||
}
|
||||
|
||||
|
||||
def test_binding_scope_payload_rejects_tenant_user_binding_public_asset():
|
||||
service = EvaluationPointGroupServiceImpl()
|
||||
|
||||
try:
|
||||
service._build_binding_scope_payload(
|
||||
current_user={"tenant_code": "MZ", "tenant_name": "梅州", "is_global": False},
|
||||
rule_set_meta={"effective_tenant_code": "PUBLIC", "effective_scope_type": "PUBLIC"},
|
||||
)
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_binding_inheritance_state_marks_tenant_user_using_provincial_binding_as_inherited():
|
||||
service = EvaluationPointGroupServiceImpl()
|
||||
|
||||
state = service._build_binding_scope_state(
|
||||
binding_row={"tenant_code": "PROVINCIAL", "scope_type": "PROVINCIAL"},
|
||||
current_user={"tenant_code": "MZ", "is_global": False},
|
||||
rule_set_meta={"source_rule_set_id": 88},
|
||||
)
|
||||
|
||||
assert state["effective_tenant_code"] == "PROVINCIAL"
|
||||
assert state["effective_scope_type"] == "PROVINCIAL"
|
||||
assert state["is_inherited"] is True
|
||||
assert state["source_rule_set_id"] == 88
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleTenantScope import (
|
||||
candidate_scope_tenant_codes,
|
||||
normalize_scoped_tenant_code,
|
||||
pick_effective_scoped_row,
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_scope_tenant_codes_prefers_exact_then_public_then_legacy_provincial():
|
||||
assert candidate_scope_tenant_codes("MZ") == ["MZ", "PUBLIC", "PROVINCIAL"]
|
||||
|
||||
|
||||
def test_normalize_scoped_tenant_code_treats_empty_as_provincial_by_default():
|
||||
assert normalize_scoped_tenant_code("") == "PROVINCIAL"
|
||||
assert normalize_scoped_tenant_code(None) == "PROVINCIAL"
|
||||
|
||||
|
||||
def test_pick_effective_scoped_row_prefers_exact_tenant():
|
||||
rows = [
|
||||
{"tenant_code": "PROVINCIAL", "id": 2},
|
||||
{"tenant_code": "MZ", "id": 1},
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
|
||||
result = pick_effective_scoped_row(rows, "MZ")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == 1
|
||||
|
||||
|
||||
def test_pick_effective_scoped_row_prefers_public_over_legacy_empty_provincial_fallback():
|
||||
rows = [
|
||||
{"tenant_code": "", "id": 9},
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
|
||||
result = pick_effective_scoped_row(rows, "MZ")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == 3
|
||||
|
||||
|
||||
def test_pick_effective_scoped_row_falls_back_to_public():
|
||||
rows = [
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
|
||||
result = pick_effective_scoped_row(rows, "MZ")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == 3
|
||||
@@ -0,0 +1,44 @@
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleTenantMaterializer import RuleTenantMaterializer
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.tenantServiceImpl import TenantServiceImpl
|
||||
|
||||
|
||||
def test_rule_tenant_materializer_excludes_platform_template_tenants():
|
||||
materializer = RuleTenantMaterializer()
|
||||
|
||||
tenants = [
|
||||
{"tenant_code": "PUBLIC", "is_enabled": True},
|
||||
{"tenant_code": "PROVINCIAL", "is_enabled": True},
|
||||
{"tenant_code": "MZ", "is_enabled": True},
|
||||
{"tenant_code": "JY", "is_enabled": False},
|
||||
{"tenant_code": "GZ", "is_enabled": True},
|
||||
]
|
||||
|
||||
assert materializer._filter_materializable_tenants(tenants) == ["MZ", "GZ"]
|
||||
|
||||
|
||||
def test_rule_tenant_materializer_uses_public_as_template_source_before_legacy_provincial():
|
||||
materializer = RuleTenantMaterializer()
|
||||
|
||||
assert materializer._template_source_order() == ["PUBLIC", "PROVINCIAL"]
|
||||
|
||||
|
||||
def test_rule_tenant_materializer_builds_tenant_region_from_tenant_code():
|
||||
materializer = RuleTenantMaterializer()
|
||||
|
||||
assert materializer._legacy_region_for_tenant("MZ") == "MZ"
|
||||
assert materializer._legacy_region_for_tenant("gz") == "GZ"
|
||||
|
||||
|
||||
def test_tenant_service_exposes_rule_materializer_hook():
|
||||
service = TenantServiceImpl()
|
||||
|
||||
assert service.RuleTenantMaterializer is not None
|
||||
|
||||
|
||||
def test_rule_tenant_materializer_only_copies_versions_into_empty_tenant_rule_set():
|
||||
materializer = RuleTenantMaterializer()
|
||||
|
||||
sql_text = str(materializer._materialize_versions.__code__.co_consts)
|
||||
|
||||
assert "tenant_existing.rule_set_id = tenant_rs.id" in sql_text
|
||||
assert "existing.version_seq = src_v.version_seq" in sql_text
|
||||
@@ -0,0 +1,55 @@
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.auditServiceImpl import (
|
||||
_candidate_binding_tenant_codes,
|
||||
_pick_effective_binding,
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_binding_tenant_codes_prefers_tenant_then_public_then_legacy_provincial():
|
||||
assert _candidate_binding_tenant_codes("MZ") == ["MZ", "PUBLIC", "PROVINCIAL"]
|
||||
|
||||
|
||||
def test_candidate_binding_tenant_codes_deduplicates_special_tenant_values():
|
||||
assert _candidate_binding_tenant_codes("PROVINCIAL") == ["PUBLIC", "PROVINCIAL"]
|
||||
assert _candidate_binding_tenant_codes("PUBLIC") == ["PUBLIC"]
|
||||
|
||||
|
||||
def test_pick_effective_binding_prefers_exact_tenant_binding():
|
||||
bindings = [
|
||||
{"tenant_code": "PROVINCIAL", "id": 2},
|
||||
{"tenant_code": "MZ", "id": 1},
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
|
||||
result = _pick_effective_binding(bindings, "MZ")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == 1
|
||||
|
||||
|
||||
def test_pick_effective_binding_falls_back_to_public_then_legacy_provincial():
|
||||
provincial_only = [
|
||||
{"tenant_code": "PROVINCIAL", "id": 2},
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
public_only = [
|
||||
{"tenant_code": "PUBLIC", "id": 3},
|
||||
]
|
||||
|
||||
provincial_result = _pick_effective_binding(provincial_only, "MZ")
|
||||
public_result = _pick_effective_binding(public_only, "MZ")
|
||||
|
||||
assert provincial_result is not None
|
||||
assert provincial_result["id"] == 3
|
||||
assert public_result is not None
|
||||
assert public_result["id"] == 3
|
||||
|
||||
|
||||
def test_pick_effective_binding_handles_legacy_empty_tenant_as_global_fallback():
|
||||
bindings = [
|
||||
{"tenant_code": "", "id": 9},
|
||||
]
|
||||
|
||||
result = _pick_effective_binding(bindings, "MZ")
|
||||
|
||||
assert result is not None
|
||||
assert result["id"] == 9
|
||||
@@ -0,0 +1,35 @@
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl
|
||||
|
||||
|
||||
def test_build_version_snapshot_uses_explicit_source_version():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
snapshot = service._build_version_scope_snapshot(
|
||||
writable_tenant_code="MZ",
|
||||
writable_scope_type="TENANT",
|
||||
rule_set_row={"tenant_code": "MZ"},
|
||||
source_version_id=321,
|
||||
)
|
||||
|
||||
assert snapshot == {
|
||||
"tenant_code_snapshot": "MZ",
|
||||
"scope_type_snapshot": "TENANT",
|
||||
"source_version_id": 321,
|
||||
}
|
||||
|
||||
|
||||
def test_build_version_snapshot_defaults_to_rule_set_scope_when_tenant_missing():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
snapshot = service._build_version_scope_snapshot(
|
||||
writable_tenant_code=None,
|
||||
writable_scope_type="PROVINCIAL",
|
||||
rule_set_row={"tenant_code": "PUBLIC"},
|
||||
source_version_id=None,
|
||||
)
|
||||
|
||||
assert snapshot == {
|
||||
"tenant_code_snapshot": "PUBLIC",
|
||||
"scope_type_snapshot": "PROVINCIAL",
|
||||
"source_version_id": None,
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
from fastapi_common.fastapi_common_web.domain.responses import StatusCodeEnum
|
||||
from fastapi_common.fastapi_common_web.exception.LeauditException import LeauditException
|
||||
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.rbacAdminServiceImpl import RbacAdminServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.rbacServiceImpl import RbacServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.permissionServiceImpl import PermissionServiceImpl
|
||||
from fastapi_modules.fastapi_leaudit.services.impl.ruleServiceImpl import RuleServiceImpl
|
||||
|
||||
|
||||
def test_pick_writable_rule_set_prefers_exact_tenant():
|
||||
service = RuleServiceImpl()
|
||||
rows = [
|
||||
{"id": 10, "tenant_code": "PROVINCIAL"},
|
||||
{"id": 11, "tenant_code": "MZ"},
|
||||
{"id": 12, "tenant_code": "PUBLIC"},
|
||||
]
|
||||
|
||||
result = service._pick_writable_rule_set_row(rows, current_user={"tenant_code": "MZ", "is_global": False})
|
||||
|
||||
assert result is not None
|
||||
assert int(result["id"]) == 11
|
||||
|
||||
|
||||
def test_pick_writable_rule_set_prefers_user_tenant_even_for_global_role():
|
||||
service = RuleServiceImpl()
|
||||
rows = [
|
||||
{"id": 10, "tenant_code": "PROVINCIAL"},
|
||||
{"id": 11, "tenant_code": "MZ"},
|
||||
{"id": 12, "tenant_code": "PUBLIC"},
|
||||
]
|
||||
|
||||
result = service._pick_writable_rule_set_row(rows, current_user={"tenant_code": "MZ", "is_global": True})
|
||||
|
||||
assert result is not None
|
||||
assert int(result["id"]) == 11
|
||||
|
||||
|
||||
def test_pick_writable_rule_set_prefers_public_for_global_user_without_tenant():
|
||||
service = RuleServiceImpl()
|
||||
rows = [
|
||||
{"id": 10, "tenant_code": "PROVINCIAL"},
|
||||
{"id": 12, "tenant_code": "PUBLIC"},
|
||||
]
|
||||
|
||||
result = service._pick_writable_rule_set_row(rows, current_user={"tenant_code": None, "is_global": True})
|
||||
|
||||
assert result is not None
|
||||
assert int(result["id"]) == 12
|
||||
|
||||
|
||||
def test_pick_writable_rule_set_rejects_public_for_tenant_user():
|
||||
service = RuleServiceImpl()
|
||||
rows = [
|
||||
{"id": 12, "tenant_code": "PUBLIC"},
|
||||
]
|
||||
|
||||
try:
|
||||
service._pick_writable_rule_set_row(rows, current_user={"tenant_code": "MZ", "is_global": False})
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_assert_version_belongs_to_writable_rule_set_rejects_cross_tenant_publish():
|
||||
service = RuleServiceImpl()
|
||||
writable_rule_set = {"id": 11, "tenant_code": "MZ"}
|
||||
version_row = {"id": 1001, "rule_set_id": 10}
|
||||
|
||||
try:
|
||||
service._assert_version_belongs_to_writable_rule_set(version_row, writable_rule_set)
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_403_FORBIDDEN
|
||||
|
||||
|
||||
def test_assert_rollback_target_rejects_current_version():
|
||||
service = RuleServiceImpl()
|
||||
rule_set = {"id": 11, "current_version_id": 1001}
|
||||
version_row = {"id": 1001, "rule_set_id": 11, "status": "published"}
|
||||
|
||||
try:
|
||||
service._assert_rollback_target(version_row, rule_set)
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
def test_assert_rollback_target_rejects_draft_version():
|
||||
service = RuleServiceImpl()
|
||||
rule_set = {"id": 11, "current_version_id": 1001}
|
||||
version_row = {"id": 1002, "rule_set_id": 11, "status": "draft"}
|
||||
|
||||
try:
|
||||
service._assert_rollback_target(version_row, rule_set)
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
def test_assert_rollback_target_allows_previous_version():
|
||||
service = RuleServiceImpl()
|
||||
rule_set = {"id": 11, "current_version_id": 1002}
|
||||
version_row = {"id": 1001, "rule_set_id": 11, "status": "deprecated"}
|
||||
|
||||
service._assert_rollback_target(version_row, rule_set)
|
||||
|
||||
|
||||
def test_tenant_user_requires_rule_tenant_schema_before_write():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
try:
|
||||
service._assert_rule_tenant_schema_ready_for_write(
|
||||
use_tenant_scope=False,
|
||||
current_user={"tenant_code": "JY", "is_global": False},
|
||||
)
|
||||
assert False, "expected LeauditException"
|
||||
except LeauditException as exc:
|
||||
assert exc.status == StatusCodeEnum.HTTP_409_CONFLICT
|
||||
|
||||
|
||||
def test_global_user_can_write_when_rule_tenant_schema_missing_for_legacy_compatibility():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
service._assert_rule_tenant_schema_ready_for_write(
|
||||
use_tenant_scope=False,
|
||||
current_user={"tenant_code": None, "is_global": True},
|
||||
)
|
||||
|
||||
|
||||
def test_build_tenant_binding_clone_payload_uses_tenant_scope():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
payload = service._build_tenant_binding_clone_payload(
|
||||
current_user={"tenant_code": "JY", "tenant_name": "揭阳", "is_global": False},
|
||||
source_binding={"group_id": 3, "priority": 100, "note": "省级绑定"},
|
||||
tenant_rule_set_id=88,
|
||||
)
|
||||
|
||||
assert payload == {
|
||||
"group_id": 3,
|
||||
"rule_set_id": 88,
|
||||
"tenant_code": "JY",
|
||||
"scope_type": "TENANT",
|
||||
"tenant_name_snapshot": "揭阳",
|
||||
"priority": 100,
|
||||
"note": "由租户规则集派生自动补绑",
|
||||
}
|
||||
|
||||
|
||||
def test_legacy_region_for_tenant_scope_uses_tenant_code_to_avoid_old_unique_constraint():
|
||||
service = RuleServiceImpl()
|
||||
|
||||
assert service._legacy_region_for_scope("JY", "TENANT") == "JY"
|
||||
assert service._legacy_region_for_scope("PROVINCIAL", "PROVINCIAL") == "default"
|
||||
assert service._legacy_region_for_scope("PUBLIC", "PUBLIC") == "PUBLIC"
|
||||
|
||||
|
||||
def test_rbac_manageable_permissions_include_rule_version_lifecycle():
|
||||
permission_keys = {
|
||||
item["permission_key"]
|
||||
for item in RbacAdminServiceImpl._MANAGEABLE_PERMISSION_BLUEPRINTS
|
||||
if item["route_path"] == "/rules"
|
||||
}
|
||||
|
||||
assert "rules:list:read" in permission_keys
|
||||
assert "rules:version_list:read" in permission_keys
|
||||
assert "rules:content:read" in permission_keys
|
||||
assert "rules:validate:execute" in permission_keys
|
||||
assert "rules:version_create:write" in permission_keys
|
||||
assert "rules:publish:write" in permission_keys
|
||||
assert "rules:rollback:write" in permission_keys
|
||||
assert "rules:binding_list:read" in permission_keys
|
||||
assert "rules:binding_create:write" in permission_keys
|
||||
assert "rules:binding_update:write" in permission_keys
|
||||
assert "rules:binding_delete:delete" in permission_keys
|
||||
|
||||
|
||||
def test_rbac_rule_group_permissions_are_folded_into_rules_menu():
|
||||
route_paths = {item["route_path"] for item in RbacAdminServiceImpl._MANAGEABLE_ROUTE_BLUEPRINTS}
|
||||
group_permission_paths = {
|
||||
item["route_path"]
|
||||
for item in RbacAdminServiceImpl._MANAGEABLE_PERMISSION_BLUEPRINTS
|
||||
if item["permission_key"].startswith("evaluation_group:")
|
||||
}
|
||||
|
||||
assert "/rule-groups" not in route_paths
|
||||
assert group_permission_paths == {"/rules"}
|
||||
|
||||
|
||||
def test_user_route_compat_menu_does_not_expose_rule_groups():
|
||||
service = RbacServiceImpl()
|
||||
routes = service._buildCompatibilityRoutes(["admin"], {"evaluation_group:list:read", "rules:list:read"})
|
||||
paths = service._collectRoutePaths(routes)
|
||||
rules_route = next(route for route in routes if route.route_path == "/rules")
|
||||
|
||||
assert "/rule-groups" not in paths
|
||||
assert "evaluation_group:list:read" in rules_route.permissions
|
||||
|
||||
|
||||
def test_rbac_seed_cache_reuses_recent_route_map():
|
||||
service = RbacAdminServiceImpl()
|
||||
route_map = {
|
||||
str(item["route_path"]): index
|
||||
for index, item in enumerate(RbacAdminServiceImpl._MANAGEABLE_ROUTE_BLUEPRINTS, start=1)
|
||||
}
|
||||
|
||||
service._remember_admin_seed_route_map(route_map)
|
||||
|
||||
assert service._get_cached_admin_seed_route_map() == route_map
|
||||
|
||||
|
||||
def test_permission_cache_is_shared_and_can_invalidate_user():
|
||||
first = PermissionServiceImpl()
|
||||
second = PermissionServiceImpl()
|
||||
|
||||
first._permission_cache[12345] = (0.0, ({"rules:list:read"}, set()))
|
||||
|
||||
assert 12345 in second._permission_cache
|
||||
PermissionServiceImpl.InvalidateUser(12345)
|
||||
assert 12345 not in first._permission_cache
|
||||
assert 12345 not in second._permission_cache
|
||||
Reference in New Issue
Block a user