2025-10-11 01:07:45 +08:00
|
|
|
|
"""
|
2026-05-26 19:38:44 +08:00
|
|
|
|
Integration tests for knowledge router endpoints.
|
2025-10-11 01:07:45 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
2025-11-06 19:47:22 +08:00
|
|
|
|
import uuid
|
2026-03-18 19:25:23 +08:00
|
|
|
|
from pathlib import Path
|
2025-11-06 19:47:22 +08:00
|
|
|
|
|
2025-10-11 01:07:45 +08:00
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
|
|
pytestmark = [pytest.mark.asyncio, pytest.mark.integration]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
def _assert_forbidden_response(response):
|
|
|
|
|
|
"""验证 403 禁止访问响应的格式"""
|
|
|
|
|
|
assert response.status_code == 403
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert "detail" in payload
|
|
|
|
|
|
assert isinstance(payload["detail"], str)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 21:32:10 +08:00
|
|
|
|
async def _create_test_department(test_client, admin_headers, prefix="pytest_dept"):
|
|
|
|
|
|
suffix = uuid.uuid4().hex[:8]
|
|
|
|
|
|
admin_uid = f"deptadmin_{suffix}"
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/departments",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"name": f"{prefix}_{suffix}",
|
|
|
|
|
|
"description": "pytest department",
|
|
|
|
|
|
"admin_uid": admin_uid,
|
|
|
|
|
|
"admin_password": f"Pw!{suffix}",
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert response.status_code == 201, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
payload["admin_uid"] = admin_uid
|
|
|
|
|
|
return payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_test_user(test_client, admin_headers, department_id):
|
|
|
|
|
|
suffix = uuid.uuid4().hex[:8]
|
|
|
|
|
|
password = f"Pw!{suffix}"
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/auth/users",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"username": f"pytest_user_{suffix}",
|
|
|
|
|
|
"password": password,
|
|
|
|
|
|
"role": "user",
|
|
|
|
|
|
"department_id": department_id,
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
user = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
login_response = await test_client.post(
|
|
|
|
|
|
"/api/auth/token",
|
|
|
|
|
|
data={"username": user["uid"], "password": password},
|
|
|
|
|
|
)
|
|
|
|
|
|
assert login_response.status_code == 200, login_response.text
|
|
|
|
|
|
return {"user": user, "headers": {"Authorization": f"Bearer {login_response.json()['access_token']}"}}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _delete_user_by_id(test_client, admin_headers, user_id):
|
|
|
|
|
|
response = await test_client.delete(f"/api/auth/users/{user_id}", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code in (200, 404), response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _find_user_id_by_uid(test_client, admin_headers, uid):
|
|
|
|
|
|
response = await test_client.get("/api/auth/users", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
for user in response.json():
|
|
|
|
|
|
if user["uid"] == uid:
|
|
|
|
|
|
return user["id"]
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _delete_department_with_admin(test_client, admin_headers, department):
|
|
|
|
|
|
admin_user_id = await _find_user_id_by_uid(test_client, admin_headers, department["admin_uid"])
|
|
|
|
|
|
if admin_user_id:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, admin_user_id)
|
|
|
|
|
|
response = await test_client.delete(f"/api/departments/{department['id']}", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code in (200, 404), response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _create_test_database(test_client, admin_headers, share_config=None):
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/knowledge/databases",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"database_name": f"pytest_acl_{uuid.uuid4().hex[:8]}",
|
|
|
|
|
|
"description": "Knowledge permission test",
|
|
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
|
|
|
|
|
"kb_type": "milvus",
|
|
|
|
|
|
"additional_params": {},
|
|
|
|
|
|
"share_config": share_config,
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
return response.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
async def _accessible_slugs(test_client, headers):
|
2026-05-18 21:32:10 +08:00
|
|
|
|
response = await test_client.get("/api/knowledge/databases/accessible", headers=headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
2026-05-21 19:32:36 +08:00
|
|
|
|
return {item["slug"] for item in response.json().get("databases", [])}
|
2026-05-18 21:32:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
2025-10-11 01:07:45 +08:00
|
|
|
|
async def test_admin_can_manage_knowledge_databases(test_client, admin_headers, knowledge_database):
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
|
|
|
|
|
list_response = await test_client.get("/api/knowledge/databases", headers=admin_headers)
|
|
|
|
|
|
assert list_response.status_code == 200, list_response.text
|
|
|
|
|
|
databases = list_response.json().get("databases", [])
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert any(entry["slug"] == slug for entry in databases)
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
get_response = await test_client.get(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2025-10-11 01:07:45 +08:00
|
|
|
|
assert get_response.status_code == 200, get_response.text
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert get_response.json()["slug"] == slug
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
|
|
|
|
|
update_response = await test_client.put(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}",
|
2025-10-11 01:07:45 +08:00
|
|
|
|
json={"name": knowledge_database["name"], "description": "Updated by pytest"},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert update_response.status_code == 200, update_response.text
|
|
|
|
|
|
assert update_response.json()["database"]["description"] == "Updated by pytest"
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-20 19:35:43 +08:00
|
|
|
|
async def test_create_database_with_chunk_preset(test_client, admin_headers):
|
|
|
|
|
|
db_name = f"pytest_chunk_preset_{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Chunk preset create test",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
2026-02-20 19:35:43 +08:00
|
|
|
|
"kb_type": "milvus",
|
|
|
|
|
|
"additional_params": {"chunk_preset_id": "book"},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert create_response.status_code == 200, create_response.text
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = create_response.json()["slug"]
|
2026-02-20 19:35:43 +08:00
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
info_response = await test_client.get(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2026-02-20 19:35:43 +08:00
|
|
|
|
assert info_response.status_code == 200, info_response.text
|
|
|
|
|
|
assert info_response.json()["additional_params"]["chunk_preset_id"] == "book"
|
|
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
delete_response = await test_client.delete(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2026-02-20 19:35:43 +08:00
|
|
|
|
assert delete_response.status_code == 200, delete_response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_update_database_additional_params_merge_keeps_chunk_preset(
|
|
|
|
|
|
test_client, admin_headers, knowledge_database
|
|
|
|
|
|
):
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-02-20 19:35:43 +08:00
|
|
|
|
|
|
|
|
|
|
first_update = await test_client.put(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}",
|
2026-02-20 19:35:43 +08:00
|
|
|
|
json={
|
|
|
|
|
|
"name": knowledge_database["name"],
|
|
|
|
|
|
"description": "update with chunk preset",
|
|
|
|
|
|
"additional_params": {"chunk_preset_id": "qa"},
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert first_update.status_code == 200, first_update.text
|
|
|
|
|
|
|
|
|
|
|
|
second_update = await test_client.put(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}",
|
2026-02-20 19:35:43 +08:00
|
|
|
|
json={
|
|
|
|
|
|
"name": knowledge_database["name"],
|
|
|
|
|
|
"description": "update without additional params",
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert second_update.status_code == 200, second_update.text
|
|
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
info_response = await test_client.get(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2026-02-20 19:35:43 +08:00
|
|
|
|
assert info_response.status_code == 200, info_response.text
|
|
|
|
|
|
assert info_response.json()["additional_params"]["chunk_preset_id"] == "qa"
|
|
|
|
|
|
|
|
|
|
|
|
|
2025-10-11 01:07:45 +08:00
|
|
|
|
async def test_knowledge_routes_enforce_permissions(test_client, standard_user, knowledge_database):
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
|
|
|
|
|
forbidden_create = await test_client.post(
|
|
|
|
|
|
"/api/knowledge/databases",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"database_name": "unauthorized_db",
|
|
|
|
|
|
"description": "Should not succeed",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
2025-10-11 01:07:45 +08:00
|
|
|
|
},
|
|
|
|
|
|
headers=standard_user["headers"],
|
|
|
|
|
|
)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
_assert_forbidden_response(forbidden_create)
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
|
|
|
|
|
forbidden_list = await test_client.get("/api/knowledge/databases", headers=standard_user["headers"])
|
2026-01-21 15:15:52 +08:00
|
|
|
|
_assert_forbidden_response(forbidden_list)
|
2025-10-11 01:07:45 +08:00
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
forbidden_get = await test_client.get(f"/api/knowledge/databases/{slug}", headers=standard_user["headers"])
|
2026-01-21 15:15:52 +08:00
|
|
|
|
_assert_forbidden_response(forbidden_get)
|
2025-11-06 19:47:22 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_admin_can_create_vector_db_with_reranker(test_client, admin_headers):
|
2026-01-21 15:15:52 +08:00
|
|
|
|
"""测试创建向量库并配置 reranker 参数(通过 query_params.options)
|
|
|
|
|
|
|
|
|
|
|
|
注意:数据库清理由 conftest.py 中的 session fixture 自动处理。
|
|
|
|
|
|
"""
|
2025-11-06 19:47:22 +08:00
|
|
|
|
db_name = f"pytest_rerank_{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Vector DB with reranker",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
2026-01-02 14:44:00 +08:00
|
|
|
|
"kb_type": "milvus",
|
2026-01-09 01:21:41 +08:00
|
|
|
|
"additional_params": {},
|
2025-11-06 19:47:22 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert create_response.status_code == 200, create_response.text
|
|
|
|
|
|
|
|
|
|
|
|
db_payload = create_response.json()
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = db_payload["slug"]
|
2025-11-06 19:47:22 +08:00
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
# 获取查询参数配置
|
2026-05-21 19:32:36 +08:00
|
|
|
|
params_response = await test_client.get(f"/api/knowledge/databases/{slug}/query-params", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert params_response.status_code == 200, params_response.text
|
|
|
|
|
|
|
|
|
|
|
|
params_payload = params_response.json()
|
|
|
|
|
|
options = params_payload.get("params", {}).get("options", [])
|
|
|
|
|
|
option_keys = {option.get("key") for option in options}
|
|
|
|
|
|
|
|
|
|
|
|
# 验证新的参数名称
|
|
|
|
|
|
assert "final_top_k" in option_keys
|
|
|
|
|
|
assert "use_reranker" in option_keys
|
|
|
|
|
|
assert "recall_top_k" in option_keys
|
|
|
|
|
|
assert "reranker_model" in option_keys
|
|
|
|
|
|
|
|
|
|
|
|
# 验证参数配置
|
|
|
|
|
|
final_top_k_option = next((opt for opt in options if opt.get("key") == "final_top_k"), None)
|
|
|
|
|
|
assert final_top_k_option is not None
|
|
|
|
|
|
assert final_top_k_option.get("default") == 10
|
|
|
|
|
|
|
|
|
|
|
|
use_reranker_option = next((opt for opt in options if opt.get("key") == "use_reranker"), None)
|
|
|
|
|
|
assert use_reranker_option is not None
|
|
|
|
|
|
assert use_reranker_option.get("default") is False
|
|
|
|
|
|
|
|
|
|
|
|
# 保存查询参数(模拟前端配置)
|
|
|
|
|
|
update_params = {
|
|
|
|
|
|
"final_top_k": 5,
|
|
|
|
|
|
"use_reranker": True,
|
|
|
|
|
|
"recall_top_k": 20,
|
|
|
|
|
|
}
|
|
|
|
|
|
update_response = await test_client.put(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/query-params", json=update_params, headers=admin_headers
|
2026-01-21 15:15:52 +08:00
|
|
|
|
)
|
|
|
|
|
|
assert update_response.status_code == 200, update_response.text
|
|
|
|
|
|
|
|
|
|
|
|
# 再次获取参数,验证保存成功
|
2026-05-21 19:32:36 +08:00
|
|
|
|
params_response2 = await test_client.get(f"/api/knowledge/databases/{slug}/query-params", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert params_response2.status_code == 200, params_response2.text
|
|
|
|
|
|
|
|
|
|
|
|
params_payload2 = params_response2.json()
|
|
|
|
|
|
options2 = params_payload2.get("params", {}).get("options", [])
|
|
|
|
|
|
|
|
|
|
|
|
# 验证保存的值
|
|
|
|
|
|
final_top_k_option2 = next((opt for opt in options2 if opt.get("key") == "final_top_k"), None)
|
|
|
|
|
|
assert final_top_k_option2 is not None
|
|
|
|
|
|
assert final_top_k_option2.get("default") == 5 # 保存的值
|
|
|
|
|
|
|
|
|
|
|
|
use_reranker_option2 = next((opt for opt in options2 if opt.get("key") == "use_reranker"), None)
|
|
|
|
|
|
assert use_reranker_option2 is not None
|
|
|
|
|
|
assert use_reranker_option2.get("default") is True # 保存的值
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-02-24 12:12:02 +08:00
|
|
|
|
async def test_create_dify_database_success(test_client, admin_headers):
|
|
|
|
|
|
db_name = f"pytest_dify_{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Dify KB create test",
|
|
|
|
|
|
"kb_type": "dify",
|
|
|
|
|
|
"additional_params": {
|
|
|
|
|
|
"dify_api_url": "https://api.dify.ai/v1",
|
|
|
|
|
|
"dify_token": "test-token",
|
|
|
|
|
|
"dify_dataset_id": "dataset-123",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert create_response.status_code == 200, create_response.text
|
2026-05-18 09:41:24 +08:00
|
|
|
|
created_payload = create_response.json()
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = created_payload["slug"]
|
2026-05-18 09:41:24 +08:00
|
|
|
|
assert created_payload["embedding_model_spec"] is None
|
|
|
|
|
|
assert "chunk_preset_id" not in created_payload["metadata"]
|
2026-02-24 12:12:02 +08:00
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
info_response = await test_client.get(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2026-02-24 12:12:02 +08:00
|
|
|
|
assert info_response.status_code == 200, info_response.text
|
|
|
|
|
|
additional_params = info_response.json()["additional_params"]
|
|
|
|
|
|
assert additional_params["dify_api_url"] == "https://api.dify.ai/v1"
|
|
|
|
|
|
assert additional_params["dify_token"] == "test-token"
|
|
|
|
|
|
assert additional_params["dify_dataset_id"] == "dataset-123"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_create_dify_database_missing_params_failed(test_client, admin_headers):
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": f"pytest_dify_missing_{uuid.uuid4().hex[:6]}",
|
|
|
|
|
|
"description": "Dify KB missing params",
|
|
|
|
|
|
"kb_type": "dify",
|
|
|
|
|
|
"additional_params": {
|
|
|
|
|
|
"dify_api_url": "https://api.dify.ai/v1",
|
|
|
|
|
|
"dify_token": "",
|
|
|
|
|
|
"dify_dataset_id": "",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 400, response.text
|
|
|
|
|
|
assert "Dify 参数缺失" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_create_dify_database_invalid_api_url_failed(test_client, admin_headers):
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": f"pytest_dify_bad_url_{uuid.uuid4().hex[:6]}",
|
|
|
|
|
|
"description": "Dify KB invalid api url",
|
|
|
|
|
|
"kb_type": "dify",
|
|
|
|
|
|
"additional_params": {
|
|
|
|
|
|
"dify_api_url": "https://api.dify.ai",
|
|
|
|
|
|
"dify_token": "test-token",
|
|
|
|
|
|
"dify_dataset_id": "dataset-123",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 400, response.text
|
|
|
|
|
|
assert "/v1" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_dify_query_params_and_documents_readonly(test_client, admin_headers):
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": f"pytest_dify_ro_{uuid.uuid4().hex[:6]}",
|
|
|
|
|
|
"description": "Dify readonly routes",
|
|
|
|
|
|
"kb_type": "dify",
|
|
|
|
|
|
"additional_params": {
|
|
|
|
|
|
"dify_api_url": "https://api.dify.ai/v1",
|
|
|
|
|
|
"dify_token": "test-token",
|
|
|
|
|
|
"dify_dataset_id": "dataset-123",
|
|
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert create_response.status_code == 200, create_response.text
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = create_response.json()["slug"]
|
2026-02-24 12:12:02 +08:00
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
params_response = await test_client.get(f"/api/knowledge/databases/{slug}/query-params", headers=admin_headers)
|
2026-02-24 12:12:02 +08:00
|
|
|
|
assert params_response.status_code == 200, params_response.text
|
|
|
|
|
|
options = params_response.json().get("params", {}).get("options", [])
|
|
|
|
|
|
option_keys = {item.get("key") for item in options}
|
|
|
|
|
|
assert option_keys == {"search_mode", "final_top_k", "score_threshold_enabled", "similarity_threshold"}
|
|
|
|
|
|
|
|
|
|
|
|
add_response = await test_client.post(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/documents",
|
2026-02-24 12:12:02 +08:00
|
|
|
|
json={"items": ["/tmp/demo.txt"], "params": {"content_type": "file"}},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert add_response.status_code == 400, add_response.text
|
|
|
|
|
|
assert "只支持检索" in add_response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
parse_response = await test_client.post(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/documents/parse",
|
2026-02-24 12:12:02 +08:00
|
|
|
|
json=["file_id_1"],
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert parse_response.status_code == 400, parse_response.text
|
|
|
|
|
|
assert "只支持检索" in parse_response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
index_response = await test_client.post(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/documents/index",
|
2026-02-24 12:12:02 +08:00
|
|
|
|
json={"file_ids": ["file_id_1"], "params": {}},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert index_response.status_code == 400, index_response.text
|
|
|
|
|
|
assert "只支持检索" in index_response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
# =============================================================================
|
2026-05-26 19:38:44 +08:00
|
|
|
|
# === Mindmap Tests ===
|
2026-01-21 15:15:52 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_databases_overview(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试获取所有知识库概览"""
|
2026-05-26 19:38:44 +08:00
|
|
|
|
response = await test_client.get("/api/knowledge/mindmap/databases", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
|
|
|
|
|
assert "databases" in payload
|
|
|
|
|
|
assert "total" in payload
|
|
|
|
|
|
|
|
|
|
|
|
# 验证知识库在列表中
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slugs = [db["slug"] for db in payload["databases"]]
|
|
|
|
|
|
assert knowledge_database["slug"] in slugs
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_database_files(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试获取知识库文件列表"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-05-26 19:38:44 +08:00
|
|
|
|
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap/files", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert payload["slug"] == slug
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert "files" in payload
|
|
|
|
|
|
assert "total" in payload
|
|
|
|
|
|
assert payload["db_name"] == knowledge_database["name"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_database_files_not_found(test_client, admin_headers):
|
|
|
|
|
|
"""测试获取不存在的知识库文件列表"""
|
2026-05-26 19:38:44 +08:00
|
|
|
|
response = await test_client.get("/api/knowledge/databases/nonexistent_slug/mindmap/files", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert response.status_code == 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_generate_mindmap_empty_files(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试空文件列表生成思维导图"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-01-21 15:15:52 +08:00
|
|
|
|
response = await test_client.post(
|
2026-05-26 19:38:44 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
|
|
|
|
|
json={"file_ids": [], "user_prompt": ""},
|
2026-01-21 15:15:52 +08:00
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
# 空文件应该返回400错误
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
assert "中没有文件" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_database_mindmap_not_exists(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试获取不存在的思维导图"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-05-26 19:38:44 +08:00
|
|
|
|
response = await test_client.get(f"/api/knowledge/databases/{slug}/mindmap", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert payload["slug"] == slug
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert payload["mindmap"] is None # 尚未生成思维导图
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_generate_and_get_mindmap(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试生成并获取思维导图
|
|
|
|
|
|
|
|
|
|
|
|
注意:此测试需要知识库中有文件才能完整测试核心功能。
|
|
|
|
|
|
由于没有前置的文件上传 fixture,测试会先验证空文件场景(预期400),
|
|
|
|
|
|
然后使用 xfail 标记等待后续完善。
|
|
|
|
|
|
"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
# 空文件场景 - 预期返回400错误
|
|
|
|
|
|
generate_response = await test_client.post(
|
2026-05-26 19:38:44 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
|
|
|
|
|
json={"file_ids": [], "user_prompt": ""},
|
2026-01-21 15:15:52 +08:00
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert generate_response.status_code == 400
|
|
|
|
|
|
assert "中没有文件" in generate_response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
# 标记此测试需要文件上传支持才能完整执行
|
|
|
|
|
|
pytest.skip("需要先上传文件才能完整测试思维导图生成功能")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# === Knowledge Router Additional Tests ===
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_accessible_databases(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试获取可访问的知识库列表"""
|
|
|
|
|
|
response = await test_client.get("/api/knowledge/databases/accessible", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert "databases" in payload
|
|
|
|
|
|
|
|
|
|
|
|
# 验证知识库在列表中
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slugs = [db["slug"] for db in payload["databases"]]
|
|
|
|
|
|
assert knowledge_database["slug"] in slugs
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-05-18 21:32:10 +08:00
|
|
|
|
async def test_create_database_defaults_to_global_share_config(test_client, admin_headers):
|
|
|
|
|
|
database = await _create_test_database(test_client, admin_headers)
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = database["slug"]
|
2026-05-18 21:32:10 +08:00
|
|
|
|
try:
|
|
|
|
|
|
assert database["share_config"] == {"access_level": "global", "department_ids": [], "user_uids": []}
|
|
|
|
|
|
finally:
|
2026-05-21 19:32:36 +08:00
|
|
|
|
await test_client.delete(f"/api/knowledge/databases/{slug}", headers=admin_headers)
|
2026-05-18 21:32:10 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_department_share_config_filters_accessible_databases(test_client, admin_headers):
|
|
|
|
|
|
department_a = await _create_test_department(test_client, admin_headers, "pytest_dept_a")
|
|
|
|
|
|
department_b = await _create_test_department(test_client, admin_headers, "pytest_dept_b")
|
|
|
|
|
|
user_a = user_b = None
|
|
|
|
|
|
database = None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
user_a = await _create_test_user(test_client, admin_headers, department_a["id"])
|
|
|
|
|
|
user_b = await _create_test_user(test_client, admin_headers, department_b["id"])
|
|
|
|
|
|
database = await _create_test_database(
|
|
|
|
|
|
test_client,
|
|
|
|
|
|
admin_headers,
|
|
|
|
|
|
{"access_level": "department", "department_ids": [department_a["id"]], "user_uids": []},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
saved_config = database["share_config"]
|
|
|
|
|
|
assert saved_config["access_level"] == "department"
|
|
|
|
|
|
assert department_a["id"] in saved_config["department_ids"]
|
|
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert database["slug"] in await _accessible_slugs(test_client, user_a["headers"])
|
|
|
|
|
|
assert database["slug"] not in await _accessible_slugs(test_client, user_b["headers"])
|
2026-05-18 21:32:10 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
if database:
|
2026-05-21 19:32:36 +08:00
|
|
|
|
await test_client.delete(f"/api/knowledge/databases/{database['slug']}", headers=admin_headers)
|
2026-05-18 21:32:10 +08:00
|
|
|
|
if user_a:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, user_a["user"]["id"])
|
|
|
|
|
|
if user_b:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, user_b["user"]["id"])
|
|
|
|
|
|
await _delete_department_with_admin(test_client, admin_headers, department_a)
|
|
|
|
|
|
await _delete_department_with_admin(test_client, admin_headers, department_b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_user_share_config_filters_accessible_databases(test_client, admin_headers):
|
|
|
|
|
|
department_a = await _create_test_department(test_client, admin_headers, "pytest_dept_a")
|
|
|
|
|
|
department_b = await _create_test_department(test_client, admin_headers, "pytest_dept_b")
|
|
|
|
|
|
user_a = user_b = None
|
|
|
|
|
|
database = None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
user_a = await _create_test_user(test_client, admin_headers, department_a["id"])
|
|
|
|
|
|
user_b = await _create_test_user(test_client, admin_headers, department_b["id"])
|
|
|
|
|
|
database = await _create_test_database(
|
|
|
|
|
|
test_client,
|
|
|
|
|
|
admin_headers,
|
|
|
|
|
|
{"access_level": "user", "department_ids": [], "user_uids": [user_a["user"]["uid"]]},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
saved_config = database["share_config"]
|
|
|
|
|
|
assert saved_config["access_level"] == "user"
|
|
|
|
|
|
assert user_a["user"]["uid"] in saved_config["user_uids"]
|
|
|
|
|
|
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert database["slug"] in await _accessible_slugs(test_client, user_a["headers"])
|
|
|
|
|
|
assert database["slug"] not in await _accessible_slugs(test_client, user_b["headers"])
|
2026-05-18 21:32:10 +08:00
|
|
|
|
finally:
|
|
|
|
|
|
if database:
|
2026-05-21 19:32:36 +08:00
|
|
|
|
await test_client.delete(f"/api/knowledge/databases/{database['slug']}", headers=admin_headers)
|
2026-05-18 21:32:10 +08:00
|
|
|
|
if user_a:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, user_a["user"]["id"])
|
|
|
|
|
|
if user_b:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, user_b["user"]["id"])
|
|
|
|
|
|
await _delete_department_with_admin(test_client, admin_headers, department_a)
|
|
|
|
|
|
await _delete_department_with_admin(test_client, admin_headers, department_b)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_user_access_options_include_all_departments_for_admin(test_client, admin_headers):
|
|
|
|
|
|
department = await _create_test_department(test_client, admin_headers, "pytest_access_options")
|
|
|
|
|
|
user = None
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
user = await _create_test_user(test_client, admin_headers, department["id"])
|
|
|
|
|
|
response = await test_client.get("/api/auth/users/access-options", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
uids = {item["uid"] for item in response.json()}
|
|
|
|
|
|
assert user["user"]["uid"] in uids
|
|
|
|
|
|
assert department["admin_uid"] in uids
|
|
|
|
|
|
finally:
|
|
|
|
|
|
if user:
|
|
|
|
|
|
await _delete_user_by_id(test_client, admin_headers, user["user"]["id"])
|
|
|
|
|
|
await _delete_department_with_admin(test_client, admin_headers, department)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
async def test_get_knowledge_base_types(test_client, admin_headers):
|
|
|
|
|
|
"""测试获取支持的知识库类型"""
|
|
|
|
|
|
response = await test_client.get("/api/knowledge/types", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
|
|
|
|
|
assert "kb_types" in payload
|
2026-05-18 18:07:09 +08:00
|
|
|
|
assert "default_config" not in payload["kb_types"]["dify"]
|
|
|
|
|
|
assert payload["kb_types"]["dify"]["name"] == "Dify"
|
|
|
|
|
|
assert payload["kb_types"]["dify"]["description"] == "连接 Dify Dataset 的只读检索知识库"
|
2026-05-18 09:41:24 +08:00
|
|
|
|
assert payload["kb_types"]["dify"]["requires_embedding_model"] is False
|
|
|
|
|
|
assert payload["kb_types"]["dify"]["supports_documents"] is False
|
|
|
|
|
|
assert [option["key"] for option in payload["kb_types"]["dify"]["create_params"]["options"]] == [
|
|
|
|
|
|
"dify_api_url",
|
|
|
|
|
|
"dify_token",
|
|
|
|
|
|
"dify_dataset_id",
|
|
|
|
|
|
]
|
2026-05-18 18:07:09 +08:00
|
|
|
|
assert "default_config" not in payload["kb_types"]["notion"]
|
|
|
|
|
|
assert payload["kb_types"]["notion"]["name"] == "Notion"
|
|
|
|
|
|
assert (
|
|
|
|
|
|
payload["kb_types"]["notion"]["description"]
|
|
|
|
|
|
== "连接 Notion Data Source 的只读知识库,支持检索、打开页面和页内查找"
|
|
|
|
|
|
)
|
2026-05-18 16:02:21 +08:00
|
|
|
|
assert payload["kb_types"]["notion"]["requires_embedding_model"] is False
|
|
|
|
|
|
assert payload["kb_types"]["notion"]["supports_documents"] is False
|
|
|
|
|
|
assert [option["key"] for option in payload["kb_types"]["notion"]["create_params"]["options"]] == [
|
|
|
|
|
|
"notion_token",
|
|
|
|
|
|
"notion_data_source_id",
|
|
|
|
|
|
"notion_version",
|
|
|
|
|
|
]
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_knowledge_base_statistics(test_client, admin_headers):
|
|
|
|
|
|
"""测试获取知识库统计信息"""
|
|
|
|
|
|
response = await test_client.get("/api/knowledge/stats", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
|
|
|
|
|
assert "stats" in payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_get_supported_file_types(test_client, admin_headers):
|
|
|
|
|
|
"""测试获取支持的文件类型"""
|
|
|
|
|
|
response = await test_client.get("/api/knowledge/files/supported-types", headers=admin_headers)
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
|
|
|
|
|
assert "file_types" in payload
|
|
|
|
|
|
assert isinstance(payload["file_types"], list)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-03-18 19:25:23 +08:00
|
|
|
|
async def test_markdown_endpoint_parses_uploaded_text_file(test_client, admin_headers):
|
|
|
|
|
|
"""测试 /files/markdown 能解析上传文件并返回 markdown。"""
|
2026-03-30 15:24:47 +08:00
|
|
|
|
data_dir = Path(__file__).resolve().parents[2] / "data"
|
2026-03-18 19:25:23 +08:00
|
|
|
|
test_file = data_dir / "A_Dream_of_Red_Mansions_10hui.txt"
|
|
|
|
|
|
|
|
|
|
|
|
assert test_file.exists(), f"测试文件不存在: {test_file}"
|
|
|
|
|
|
|
|
|
|
|
|
with test_file.open("rb") as f:
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/knowledge/files/markdown",
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
files={"file": (test_file.name, f, "text/plain")},
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
assert response.status_code == 200, response.text
|
|
|
|
|
|
payload = response.json()
|
|
|
|
|
|
assert payload["message"] == "success"
|
|
|
|
|
|
assert isinstance(payload.get("markdown_content"), str)
|
|
|
|
|
|
assert payload["markdown_content"].strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
async def test_duplicate_database_name(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试重复创建同名知识库"""
|
|
|
|
|
|
db_name = knowledge_database["name"]
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/knowledge/databases",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Duplicate name test",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
|
|
|
|
|
"kb_type": "milvus",
|
2026-01-21 15:15:52 +08:00
|
|
|
|
"additional_params": {},
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
assert "已存在" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-05-17 13:06:25 +08:00
|
|
|
|
async def test_create_lightrag_knowledge_base_is_unsupported(test_client, admin_headers):
|
|
|
|
|
|
db_name = f"pytest_lightrag_{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
response = await test_client.post(
|
|
|
|
|
|
"/api/knowledge/databases",
|
|
|
|
|
|
json={
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Unsupported LightRAG knowledge base",
|
|
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
|
|
|
|
|
"kb_type": "lightrag",
|
|
|
|
|
|
"additional_params": {},
|
|
|
|
|
|
},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
assert "Unsupported knowledge base type: lightrag" in response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-01-21 15:15:52 +08:00
|
|
|
|
async def test_create_milvus_knowledge_base(test_client, admin_headers):
|
|
|
|
|
|
"""测试创建 Milvus 知识库
|
|
|
|
|
|
|
|
|
|
|
|
注意:数据库清理由 conftest.py 中的 session fixture 自动处理。
|
|
|
|
|
|
"""
|
|
|
|
|
|
db_name = f"pytest_milvus_{uuid.uuid4().hex[:6]}"
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"database_name": db_name,
|
|
|
|
|
|
"description": "Pytest Milvus knowledge base",
|
2026-05-17 13:06:25 +08:00
|
|
|
|
"embedding_model_spec": "siliconflow-cn:Pro/BAAI/bge-m3",
|
2026-01-21 15:15:52 +08:00
|
|
|
|
"kb_type": "milvus",
|
|
|
|
|
|
"additional_params": {},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
create_response = await test_client.post("/api/knowledge/databases", json=payload, headers=admin_headers)
|
|
|
|
|
|
assert create_response.status_code == 200, create_response.text
|
|
|
|
|
|
|
|
|
|
|
|
db_payload = create_response.json()
|
|
|
|
|
|
assert db_payload["kb_type"] == "milvus"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_sample_questions_endpoints(test_client, admin_headers, knowledge_database):
|
|
|
|
|
|
"""测试示例问题接口(空文件时预期返回400)"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取示例问题(空知识库应该返回空列表)
|
2026-05-21 19:32:36 +08:00
|
|
|
|
get_response = await test_client.get(f"/api/knowledge/databases/{slug}/sample-questions", headers=admin_headers)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert get_response.status_code == 200, get_response.text
|
|
|
|
|
|
get_payload = get_response.json()
|
2026-05-21 19:32:36 +08:00
|
|
|
|
assert get_payload["slug"] == slug
|
2026-01-21 15:15:52 +08:00
|
|
|
|
assert "questions" in get_payload
|
|
|
|
|
|
assert get_payload["count"] == 0 # 空知识库没有问题
|
|
|
|
|
|
|
|
|
|
|
|
# 生成示例问题(空知识库应该返回400)
|
|
|
|
|
|
generate_response = await test_client.post(
|
2026-05-21 19:32:36 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/sample-questions",
|
2026-01-21 15:15:52 +08:00
|
|
|
|
json={"count": 5},
|
|
|
|
|
|
headers=admin_headers,
|
|
|
|
|
|
)
|
|
|
|
|
|
assert generate_response.status_code == 400
|
|
|
|
|
|
assert "中没有文件" in generate_response.json()["detail"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_mindmap_permissions(test_client, standard_user, knowledge_database):
|
|
|
|
|
|
"""测试思维导图接口的权限控制"""
|
2026-05-21 19:32:36 +08:00
|
|
|
|
slug = knowledge_database["slug"]
|
2026-01-21 15:15:52 +08:00
|
|
|
|
|
|
|
|
|
|
# 普通用户应该无法访问
|
2026-05-26 19:38:44 +08:00
|
|
|
|
forbidden_list = await test_client.get("/api/knowledge/mindmap/databases", headers=standard_user["headers"])
|
2026-01-21 15:15:52 +08:00
|
|
|
|
_assert_forbidden_response(forbidden_list)
|
|
|
|
|
|
|
2026-05-26 19:38:44 +08:00
|
|
|
|
forbidden_files = await test_client.get(
|
|
|
|
|
|
f"/api/knowledge/databases/{slug}/mindmap/files", headers=standard_user["headers"]
|
|
|
|
|
|
)
|
2026-01-21 15:15:52 +08:00
|
|
|
|
_assert_forbidden_response(forbidden_files)
|
|
|
|
|
|
|
|
|
|
|
|
forbidden_generate = await test_client.post(
|
2026-05-26 19:38:44 +08:00
|
|
|
|
f"/api/knowledge/databases/{slug}/mindmap/generate",
|
|
|
|
|
|
json={"file_ids": []},
|
2026-01-21 15:15:52 +08:00
|
|
|
|
headers=standard_user["headers"],
|
|
|
|
|
|
)
|
|
|
|
|
|
_assert_forbidden_response(forbidden_generate)
|