"""故事 API 测试。""" import time from unittest.mock import AsyncMock, patch import pytest from fastapi.testclient import TestClient from app.api.stories import _request_log, RATE_LIMIT_REQUESTS class TestStoryGenerate: """故事生成测试。""" def test_generate_without_auth(self, client: TestClient): """未登录时生成故事。""" response = client.post( "/api/generate", json={"type": "keywords", "data": "小兔子, 森林"}, ) assert response.status_code == 401 def test_generate_with_empty_data(self, auth_client: TestClient): """空数据生成故事。""" response = auth_client.post( "/api/generate", json={"type": "keywords", "data": ""}, ) assert response.status_code == 422 def test_generate_with_invalid_type(self, auth_client: TestClient): """无效类型生成故事。""" response = auth_client.post( "/api/generate", json={"type": "invalid", "data": "test"}, ) assert response.status_code == 422 def test_generate_story_success(self, auth_client: TestClient, mock_text_provider): """成功生成故事。""" response = auth_client.post( "/api/generate", json={"type": "keywords", "data": "小兔子, 森林, 勇气"}, ) assert response.status_code == 200 data = response.json() assert "id" in data assert "title" in data assert "story_text" in data assert data["mode"] == "generated" class TestStoryList: """故事列表测试。""" def test_list_without_auth(self, client: TestClient): """未登录时获取列表。""" response = client.get("/api/stories") assert response.status_code == 401 def test_list_empty(self, auth_client: TestClient): """空列表。""" response = auth_client.get("/api/stories") assert response.status_code == 200 assert response.json() == [] def test_list_with_stories(self, auth_client: TestClient, test_story): """有故事时获取列表。""" response = auth_client.get("/api/stories") assert response.status_code == 200 data = response.json() assert len(data) == 1 assert data[0]["id"] == test_story.id assert data[0]["title"] == test_story.title def test_list_pagination(self, auth_client: TestClient, test_story): """分页测试。""" response = auth_client.get("/api/stories?limit=1&offset=0") assert response.status_code == 200 data = response.json() assert len(data) == 1 response = auth_client.get("/api/stories?limit=1&offset=1") assert response.status_code == 200 data = response.json() assert len(data) == 0 class TestStoryDetail: """故事详情测试。""" def test_get_story_without_auth(self, client: TestClient, test_story): """未登录时获取详情。""" response = client.get(f"/api/stories/{test_story.id}") assert response.status_code == 401 def test_get_story_not_found(self, auth_client: TestClient): """故事不存在。""" response = auth_client.get("/api/stories/99999") assert response.status_code == 404 def test_get_story_success(self, auth_client: TestClient, test_story): """成功获取详情。""" response = auth_client.get(f"/api/stories/{test_story.id}") assert response.status_code == 200 data = response.json() assert data["id"] == test_story.id assert data["title"] == test_story.title assert data["story_text"] == test_story.story_text class TestStoryDelete: """故事删除测试。""" def test_delete_without_auth(self, client: TestClient, test_story): """未登录时删除。""" response = client.delete(f"/api/stories/{test_story.id}") assert response.status_code == 401 def test_delete_not_found(self, auth_client: TestClient): """删除不存在的故事。""" response = auth_client.delete("/api/stories/99999") assert response.status_code == 404 def test_delete_success(self, auth_client: TestClient, test_story): """成功删除故事。""" response = auth_client.delete(f"/api/stories/{test_story.id}") assert response.status_code == 200 assert response.json()["message"] == "Deleted" response = auth_client.get(f"/api/stories/{test_story.id}") assert response.status_code == 404 class TestRateLimit: """Rate limit 测试。""" def setup_method(self): """每个测试前清理 rate limit 缓存。""" _request_log.clear() def test_rate_limit_allows_normal_requests(self, auth_client: TestClient, test_story): """正常请求不触发限流。""" for _ in range(RATE_LIMIT_REQUESTS - 1): response = auth_client.get(f"/api/stories/{test_story.id}") assert response.status_code == 200 def test_rate_limit_blocks_excess_requests(self, auth_client: TestClient, test_story): """超限请求被阻止。""" for _ in range(RATE_LIMIT_REQUESTS): auth_client.get(f"/api/stories/{test_story.id}") response = auth_client.get(f"/api/stories/{test_story.id}") assert response.status_code == 429 assert "Too many requests" in response.json()["detail"] class TestImageGenerate: """封面图片生成测试。""" def test_generate_image_without_auth(self, client: TestClient, test_story): """未登录时生成图片。""" response = client.post(f"/api/image/generate/{test_story.id}") assert response.status_code == 401 def test_generate_image_not_found(self, auth_client: TestClient): """故事不存在。""" response = auth_client.post("/api/image/generate/99999") assert response.status_code == 404 class TestAudio: """语音朗读测试。""" def test_get_audio_without_auth(self, client: TestClient, test_story): """未登录时获取音频。""" response = client.get(f"/api/audio/{test_story.id}") assert response.status_code == 401 def test_get_audio_not_found(self, auth_client: TestClient): """故事不存在。""" response = auth_client.get("/api/audio/99999") assert response.status_code == 404 def test_get_audio_success(self, auth_client: TestClient, test_story, mock_tts_provider): """成功获取音频。""" response = auth_client.get(f"/api/audio/{test_story.id}") assert response.status_code == 200 assert response.headers["content-type"] == "audio/mpeg" class TestGenerateFull: """完整故事生成测试(/api/generate/full)。""" def test_generate_full_without_auth(self, client: TestClient): """未登录时生成完整故事。""" response = client.post( "/api/generate/full", json={"type": "keywords", "data": "小兔子, 森林"}, ) assert response.status_code == 401 def test_generate_full_success(self, auth_client: TestClient, mock_text_provider, mock_image_provider): """成功生成完整故事(含图片)。""" response = auth_client.post( "/api/generate/full", json={"type": "keywords", "data": "小兔子, 森林, 勇气"}, ) assert response.status_code == 200 data = response.json() assert "id" in data assert "title" in data assert "story_text" in data assert data["mode"] == "generated" assert data["image_url"] == "https://example.com/image.png" assert data["audio_ready"] is False # 音频按需生成 assert data["errors"] == {} def test_generate_full_image_failure(self, auth_client: TestClient, mock_text_provider): """图片生成失败时返回部分成功。""" with patch("app.api.stories.generate_image", new_callable=AsyncMock) as mock_img: mock_img.side_effect = Exception("Image API error") response = auth_client.post( "/api/generate/full", json={"type": "keywords", "data": "小兔子, 森林"}, ) assert response.status_code == 200 data = response.json() assert data["image_url"] is None assert "image" in data["errors"] assert "Image API error" in data["errors"]["image"] def test_generate_full_with_education_theme(self, auth_client: TestClient, mock_text_provider, mock_image_provider): """带教育主题生成故事。""" response = auth_client.post( "/api/generate/full", json={ "type": "keywords", "data": "小兔子, 森林", "education_theme": "勇气与友谊", }, ) assert response.status_code == 200 mock_text_provider.assert_called_once() call_kwargs = mock_text_provider.call_args.kwargs assert call_kwargs["education_theme"] == "勇气与友谊" class TestImageGenerateSuccess: """封面图片生成成功测试。""" def test_generate_image_success(self, auth_client: TestClient, test_story, mock_image_provider): """成功生成图片。""" response = auth_client.post(f"/api/image/generate/{test_story.id}") assert response.status_code == 200 data = response.json() assert data["image_url"] == "https://example.com/image.png"