Files
leaudit-platform-backend/tests/release/conftest.py
T

480 lines
16 KiB
Python

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)