480 lines
16 KiB
Python
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)
|