154 lines
4.6 KiB
Python
154 lines
4.6 KiB
Python
"""API 端点集成测试."""
|
|
import os
|
|
import tempfile
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
"""创建测试客户端(使用临时目录 + 本地嵌入模型)."""
|
|
tmpdir = tempfile.mkdtemp()
|
|
# 设置环境变量使 app 使用测试配置
|
|
os.environ["MD_VECTOR_DB_DATA_DIR"] = tmpdir
|
|
os.environ["MD_VECTOR_DB_COLLECTION"] = "test_api"
|
|
# 离线模式: 模型已缓存, 无需联网
|
|
os.environ["TRANSFORMERS_OFFLINE"] = "1"
|
|
os.environ["HF_HUB_OFFLINE"] = "1"
|
|
|
|
from src.server.app import app
|
|
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
|
|
class TestHealthEndpoint:
|
|
"""健康检查."""
|
|
|
|
def test_health_returns_ok(self, client):
|
|
response = client.get("/api/v1/health")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
|
|
|
|
class TestCollectionEndpoint:
|
|
"""Collection 端点."""
|
|
|
|
def test_list_collections(self, client):
|
|
response = client.get("/api/v1/collections")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "collections" in data
|
|
|
|
|
|
class TestSearchEndpoint:
|
|
"""搜索端点."""
|
|
|
|
def test_search_requires_query(self, client):
|
|
response = client.post("/api/v1/search", json={})
|
|
assert response.status_code == 422 # FastAPI 自动校验
|
|
|
|
def test_search_empty_collection(self, client):
|
|
response = client.post(
|
|
"/api/v1/search",
|
|
json={"query": "test", "top_k": 5},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "results" in data
|
|
assert data["results"] == []
|
|
|
|
|
|
class TestIngestEndpoint:
|
|
"""入库端点."""
|
|
|
|
def test_ingest_content(self, client):
|
|
response = client.post(
|
|
"/api/v1/ingest",
|
|
json={
|
|
"content": "# Test\nHello world.",
|
|
"file_name": "test.md",
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["status"] == "ok"
|
|
assert data["chunks"] > 0
|
|
|
|
def test_ingest_then_search(self, client):
|
|
"""入库后能检索到."""
|
|
client.post(
|
|
"/api/v1/ingest",
|
|
json={"content": "# 配置说明\nChromaDB 配置很简单。", "file_name": "config.md"},
|
|
)
|
|
response = client.post(
|
|
"/api/v1/search",
|
|
json={"query": "配置", "top_k": 3},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["results"]) > 0
|
|
|
|
|
|
class TestSecurity:
|
|
"""安全测试."""
|
|
|
|
def test_ingest_rejects_path_traversal(self, client):
|
|
"""拒绝路径遍历攻击."""
|
|
response = client.post(
|
|
"/api/v1/ingest",
|
|
json={"file_path": "../../../etc/passwd"},
|
|
)
|
|
assert response.status_code in (400, 403)
|
|
|
|
def test_ingest_rejects_absolute_path(self, client):
|
|
"""拒绝绝对路径."""
|
|
response = client.post(
|
|
"/api/v1/ingest",
|
|
json={"file_path": "C:\\Windows\\System32\\config\\SAM"},
|
|
)
|
|
assert response.status_code in (400, 403)
|
|
|
|
|
|
class TestDeleteEndpoint:
|
|
"""删除端点."""
|
|
|
|
def test_delete_nonexistent(self, client):
|
|
"""删除不存在的文件返回 404."""
|
|
response = client.delete("/api/v1/documents/nonexistent.md")
|
|
assert response.status_code == 404
|
|
|
|
def test_delete_ingested(self, client):
|
|
"""删除已入库文件后搜索不再返回."""
|
|
# 入库
|
|
ingest_resp = client.post(
|
|
"/api/v1/ingest",
|
|
json={"content": "# Test Delete\nHello.", "file_name": "delete-test.md"},
|
|
)
|
|
assert ingest_resp.status_code == 200, f"ingest failed: {ingest_resp.json()}"
|
|
# 删除
|
|
response = client.delete("/api/v1/documents/delete-test.md")
|
|
assert response.status_code == 200, f"delete failed: {response.json()}"
|
|
# 搜索验证已删除
|
|
search_resp = client.post(
|
|
"/api/v1/search", json={"query": "Test Delete", "top_k": 3}
|
|
)
|
|
results = search_resp.json()["results"]
|
|
sources = [r["source_file"] for r in results]
|
|
assert "delete-test.md" not in sources
|
|
|
|
|
|
def test_admin_ui_served(client):
|
|
"""管理界面可访问."""
|
|
response = client.get("/admin")
|
|
assert response.status_code == 200
|
|
|
|
|
|
def test_health_skips_rate_limit(client):
|
|
"""健康检查多次访问不触发速率限制."""
|
|
for _ in range(5):
|
|
resp = client.get("/api/v1/health")
|
|
assert resp.status_code == 200
|