""" 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'