Files
Timeline_Generator/tests/integration/test_api.py
beabigegg 2d37d23bcf v9.5: 實作標籤完全不重疊算法
- 新增 _calculate_lane_conflicts_v2() 分開返回標籤重疊和線穿框分數
- 修改泳道選擇算法,優先選擇無標籤重疊的泳道
- 兩階段搜尋:優先側別無可用泳道則嘗試另一側
- 增強日誌輸出,顯示標籤範圍和詳細衝突分數

🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-06 11:35:29 +08:00

614 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
API 端點整合測試
對應 TDD.md - IT-API-01: API 端點整合測試
驗證所有 REST API 端點功能正常運作
Version: 1.0.0
DocID: TDD-IT-API-001
"""
import pytest
from datetime import datetime
from io import BytesIO
class TestHealthCheck:
"""健康檢查 API 測試"""
@pytest.mark.asyncio
async def test_health_check_success(self, client):
"""
IT-API-01-001: 測試健康檢查端點
預期結果:
- HTTP 200
- success = True
- 包含版本資訊
"""
response = await client.get("/health")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["message"] == "Service is healthy"
assert "version" in data["data"]
assert "timestamp" in data["data"]
class TestImportAPI:
"""匯入 API 測試"""
@pytest.mark.asyncio
async def test_import_csv_success(self, client, sample_csv_content):
"""
IT-API-02-001: 測試成功匯入 CSV
預期結果:
- HTTP 200
- success = True
- imported_count = 3
"""
# 清空事件
await client.delete("/api/events")
# 上傳 CSV
files = {"file": ("test.csv", BytesIO(sample_csv_content), "text/csv")}
response = await client.post("/api/import", files=files)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert data["imported_count"] == 3
assert len(data["events"]) == 3
assert data["events"][0]["title"] == "Event 1"
@pytest.mark.asyncio
async def test_import_invalid_file_type(self, client):
"""
IT-API-02-002: 測試不支援的檔案類型
預期結果:
- HTTP 400
- 錯誤訊息
"""
files = {"file": ("test.txt", BytesIO(b"invalid"), "text/plain")}
response = await client.post("/api/import", files=files)
assert response.status_code == 400
assert "不支援的檔案格式" in response.json()["detail"]
@pytest.mark.asyncio
async def test_import_no_filename(self, client):
"""
IT-API-02-003: 測試未提供檔案名稱
預期結果:
- HTTP 422 (FastAPI 驗證錯誤) 或 400
"""
files = {"file": ("", BytesIO(b"test"), "text/csv")}
response = await client.post("/api/import", files=files)
# FastAPI 會在更早的層級驗證並返回 422
assert response.status_code in [400, 422]
class TestEventsAPI:
"""事件管理 API 測試"""
@pytest.mark.asyncio
async def test_get_events_empty(self, client):
"""
IT-API-03-001: 測試取得空事件列表
預期結果:
- HTTP 200
- 空陣列
"""
# 先清空
await client.delete("/api/events")
response = await client.get("/api/events")
assert response.status_code == 200
assert response.json() == []
@pytest.mark.asyncio
async def test_add_event_success(self, client):
"""
IT-API-03-002: 測試新增事件
預期結果:
- HTTP 200
- 回傳新增的事件
"""
# 清空
await client.delete("/api/events")
event_data = {
"id": "test-001",
"title": "Integration Test Event",
"start": "2024-01-01T09:00:00",
"end": "2024-01-01T17:00:00",
"group": "Test",
"description": "Test description",
"color": "#3B82F6",
"event_type": "range"
}
response = await client.post("/api/events", json=event_data)
assert response.status_code == 200
data = response.json()
assert data["id"] == "test-001"
assert data["title"] == "Integration Test Event"
@pytest.mark.asyncio
async def test_get_events_after_add(self, client):
"""
IT-API-03-003: 測試新增後取得事件列表
預期結果:
- HTTP 200
- 包含新增的事件
"""
# 清空並新增
await client.delete("/api/events")
event_data = {
"id": "test-002",
"title": "Test Event 2",
"start": "2024-01-01T09:00:00"
}
await client.post("/api/events", json=event_data)
response = await client.get("/api/events")
assert response.status_code == 200
events = response.json()
assert len(events) >= 1
assert any(e["id"] == "test-002" for e in events)
@pytest.mark.asyncio
async def test_delete_event_success(self, client):
"""
IT-API-03-004: 測試刪除事件
預期結果:
- HTTP 200
- success = True
"""
# 先新增
await client.delete("/api/events")
event_data = {
"id": "test-delete",
"title": "To Be Deleted",
"start": "2024-01-01T09:00:00"
}
await client.post("/api/events", json=event_data)
# 刪除
response = await client.delete("/api/events/test-delete")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "成功刪除" in data["message"]
@pytest.mark.asyncio
async def test_delete_nonexistent_event(self, client):
"""
IT-API-03-005: 測試刪除不存在的事件
預期結果:
- HTTP 404
- 使用 APIResponse 格式回應
"""
response = await client.delete("/api/events/nonexistent-id")
assert response.status_code == 404
data = response.json()
# API 使用自訂 404 handler回應格式為 APIResponse
assert data["success"] is False
assert "找不到" in data["message"] or data["error_code"] == "NOT_FOUND"
@pytest.mark.asyncio
async def test_clear_events(self, client):
"""
IT-API-03-006: 測試清空所有事件
預期結果:
- HTTP 200
- 事件列表清空
"""
# 先新增一些事件
await client.post("/api/events", json={
"id": "clear-1",
"title": "Event 1",
"start": "2024-01-01T09:00:00"
})
# 清空
response = await client.delete("/api/events")
assert response.status_code == 200
data = response.json()
assert data["success"] is True
# 驗證已清空
events_response = await client.get("/api/events")
assert len(events_response.json()) == 0
class TestRenderAPI:
"""渲染 API 測試"""
@pytest.mark.asyncio
async def test_render_with_events(self, client):
"""
IT-API-04-001: 測試渲染時間軸
預期結果:
- HTTP 200
- success = True
- 包含 Plotly data 和 layout
"""
# 準備事件
events = [
{
"id": "render-1",
"title": "Event 1",
"start": "2024-01-01T09:00:00"
},
{
"id": "render-2",
"title": "Event 2",
"start": "2024-01-05T09:00:00"
}
]
request_data = {
"events": events,
"config": {
"direction": "horizontal",
"theme": "modern",
"show_grid": True
}
}
response = await client.post("/api/render", json=request_data)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
assert "data" in data
assert "layout" in data
assert "data" in data["data"]
@pytest.mark.asyncio
async def test_render_empty_events(self, client):
"""
IT-API-04-002: 測試渲染空事件列表
預期結果:
- HTTP 200
- 可以處理空事件
"""
request_data = {
"events": [],
"config": {
"direction": "horizontal",
"theme": "modern"
}
}
response = await client.post("/api/render", json=request_data)
assert response.status_code == 200
data = response.json()
assert data["success"] is True
@pytest.mark.asyncio
async def test_render_with_different_themes(self, client):
"""
IT-API-04-003: 測試不同主題渲染
預期結果:
- 所有主題都能正常渲染
"""
events = [{
"id": "theme-test",
"title": "Theme Test",
"start": "2024-01-01T09:00:00"
}]
themes = ["modern", "classic", "minimal", "corporate"]
for theme in themes:
request_data = {
"events": events,
"config": {"theme": theme}
}
response = await client.post("/api/render", json=request_data)
assert response.status_code == 200, f"Theme {theme} failed"
assert response.json()["success"] is True
@pytest.mark.asyncio
async def test_render_with_stored_events(self, client, sample_csv_content):
"""
IT-API-04-004: 測試使用已儲存的事件渲染
預期結果:
- 可以使用全域儲存的事件
"""
# 先匯入事件
await client.delete("/api/events")
files = {"file": ("test.csv", BytesIO(sample_csv_content), "text/csv")}
await client.post("/api/import", files=files)
# 渲染(不提供 events使用全域儲存
request_data = {
"config": {"direction": "horizontal"}
}
response = await client.post("/api/render", json=request_data)
assert response.status_code == 200
assert response.json()["success"] is True
class TestExportAPI:
"""匯出 API 測試"""
@pytest.mark.asyncio
async def test_export_pdf_success(self, client):
"""
IT-API-05-001: 測試匯出 PDF
預期結果:
- HTTP 200
- Content-Type = application/pdf
- 檔案內容正確
"""
# 先渲染
events = [{"id": "exp-1", "title": "Export Test", "start": "2024-01-01T09:00:00"}]
render_response = await client.post("/api/render", json={
"events": events,
"config": {}
})
render_data = render_response.json()
# 匯出
export_request = {
"plotly_data": render_data["data"],
"plotly_layout": render_data["layout"],
"options": {
"fmt": "pdf",
"dpi": 300,
"width": 1920,
"height": 1080
},
"filename": "test_export.pdf"
}
response = await client.post("/api/export", json=export_request)
assert response.status_code == 200
assert response.headers["content-type"] == "application/pdf"
assert len(response.content) > 0
# 檢查 PDF 檔案標記
assert response.content.startswith(b'%PDF-')
@pytest.mark.asyncio
async def test_export_png_success(self, client):
"""
IT-API-05-002: 測試匯出 PNG
預期結果:
- HTTP 200
- Content-Type = image/png
"""
# 渲染
events = [{"id": "png-1", "title": "PNG Test", "start": "2024-01-01T09:00:00"}]
render_response = await client.post("/api/render", json={"events": events})
render_data = render_response.json()
# 匯出 PNG
export_request = {
"plotly_data": render_data["data"],
"plotly_layout": render_data["layout"],
"options": {
"fmt": "png",
"dpi": 300
}
}
response = await client.post("/api/export", json=export_request)
assert response.status_code == 200
assert response.headers["content-type"] == "image/png"
# 檢查 PNG 檔案簽名
assert response.content.startswith(b'\x89PNG')
@pytest.mark.asyncio
async def test_export_svg_success(self, client):
"""
IT-API-05-003: 測試匯出 SVG
預期結果:
- HTTP 200
- Content-Type = image/svg+xml
"""
# 渲染
events = [{"id": "svg-1", "title": "SVG Test", "start": "2024-01-01T09:00:00"}]
render_response = await client.post("/api/render", json={"events": events})
render_data = render_response.json()
# 匯出 SVG
export_request = {
"plotly_data": render_data["data"],
"plotly_layout": render_data["layout"],
"options": {"fmt": "svg"}
}
response = await client.post("/api/export", json=export_request)
assert response.status_code == 200
assert response.headers["content-type"] == "image/svg+xml"
# SVG 是文字格式
assert b'<svg' in response.content or b'<?xml' in response.content
class TestThemesAPI:
"""主題 API 測試"""
@pytest.mark.asyncio
async def test_get_themes_success(self, client):
"""
IT-API-06-001: 測試取得主題列表
預期結果:
- HTTP 200
- 包含 4 個主題
- 每個主題包含必要欄位
"""
response = await client.get("/api/themes")
assert response.status_code == 200
themes = response.json()
assert len(themes) == 4
# 驗證主題結構
for theme in themes:
assert "name" in theme
assert "style" in theme
assert "primary_color" in theme
assert "background_color" in theme
assert "text_color" in theme
# 驗證顏色格式
assert theme["primary_color"].startswith("#")
assert len(theme["primary_color"]) == 7
@pytest.mark.asyncio
async def test_get_themes_includes_all_styles(self, client):
"""
IT-API-06-002: 測試主題包含所有樣式
預期結果:
- 包含 modern, classic, minimal, corporate
"""
response = await client.get("/api/themes")
themes = response.json()
styles = [theme["style"] for theme in themes]
assert "modern" in styles
assert "classic" in styles
assert "minimal" in styles
assert "corporate" in styles
class TestWorkflows:
"""完整工作流程測試"""
@pytest.mark.asyncio
async def test_complete_workflow(self, client, sample_csv_content):
"""
IT-API-07-001: 測試完整工作流程
流程:
1. 匯入 CSV
2. 取得事件列表
3. 渲染時間軸
4. 匯出 PDF
預期結果:
- 所有步驟成功
"""
# 1. 清空並匯入
await client.delete("/api/events")
import_response = await client.post(
"/api/import",
files={"file": ("test.csv", BytesIO(sample_csv_content), "text/csv")}
)
assert import_response.status_code == 200
assert import_response.json()["imported_count"] == 3
# 2. 取得事件
events_response = await client.get("/api/events")
assert events_response.status_code == 200
events = events_response.json()
assert len(events) == 3
# 3. 渲染
render_response = await client.post("/api/render", json={
"config": {"direction": "horizontal", "theme": "modern"}
})
assert render_response.status_code == 200
render_data = render_response.json()
assert render_data["success"] is True
# 4. 匯出
export_response = await client.post("/api/export", json={
"plotly_data": render_data["data"],
"plotly_layout": render_data["layout"],
"options": {"fmt": "pdf", "dpi": 300}
})
assert export_response.status_code == 200
assert export_response.headers["content-type"] == "application/pdf"
@pytest.mark.asyncio
async def test_event_crud_workflow(self, client):
"""
IT-API-07-002: 測試事件 CRUD 工作流程
流程:
1. 清空事件
2. 新增多個事件
3. 取得列表驗證
4. 刪除一個事件
5. 清空所有事件
預期結果:
- 所有 CRUD 操作成功
"""
# 1. 清空
await client.delete("/api/events")
# 2. 新增
for i in range(3):
event = {
"id": f"crud-{i}",
"title": f"CRUD Test {i}",
"start": f"2024-01-0{i+1}T09:00:00"
}
response = await client.post("/api/events", json=event)
assert response.status_code == 200
# 3. 取得列表
events_response = await client.get("/api/events")
events = events_response.json()
assert len(events) == 3
# 4. 刪除一個
delete_response = await client.delete("/api/events/crud-1")
assert delete_response.status_code == 200
# 驗證刪除後
events_response = await client.get("/api/events")
events = events_response.json()
assert len(events) == 2
assert not any(e["id"] == "crud-1" for e in events)
# 5. 清空
clear_response = await client.delete("/api/events")
assert clear_response.status_code == 200
# 驗證清空
events_response = await client.get("/api/events")
assert len(events_response.json()) == 0
if __name__ == "__main__":
pytest.main([__file__, "-v"])