| """Tests for main FastAPI application. |
| |
| Requirements: 10.4 - Startup configuration validation |
| Requirements: 8.1, 8.2, 8.3 - API endpoint implementation |
| """ |
|
|
| import os |
| import pytest |
| from unittest.mock import patch, AsyncMock, MagicMock |
| from io import BytesIO |
|
|
|
|
| class TestApplicationStartup: |
| """Test application startup and configuration validation. |
| |
| Requirement 10.4: Application should refuse to start if required config is missing. |
| """ |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_app_starts_with_valid_config(self, tmp_path): |
| """Test that application starts successfully with valid configuration.""" |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| response = client.get("/") |
| |
| assert response.status_code == 200 |
| assert response.json()["status"] == "running" |
| |
| @patch.dict(os.environ, {}, clear=True) |
| def test_app_refuses_to_start_without_api_key(self): |
| """Test that application refuses to start without API key. |
| |
| Requirement 10.4: Missing required config should cause startup failure. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| import importlib |
| import app.main |
| importlib.reload(app.main) |
| |
| from fastapi.testclient import TestClient |
| |
| with pytest.raises(RuntimeError, match="Configuration error"): |
| with TestClient(app.main.app) as client: |
| |
| pass |
|
|
|
|
| class TestHealthEndpoint: |
| """Test health check endpoint.""" |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_health_check_success(self, tmp_path): |
| """Test health check returns healthy status.""" |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| response = client.get("/health") |
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert data["status"] == "healthy" |
| assert "data_dir" in data |
| assert "max_audio_size" in data |
|
|
|
|
| class TestRootEndpoint: |
| """Test root endpoint.""" |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_root_endpoint(self, tmp_path): |
| """Test root endpoint returns service information.""" |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| response = client.get("/") |
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert data["service"] == "Voice Text Processor" |
| assert data["status"] == "running" |
| assert "version" in data |
|
|
|
|
|
|
| class TestProcessEndpoint: |
| """Test /api/process endpoint. |
| |
| Requirements: 8.1, 8.2, 8.3 - API endpoint, business logic, error handling |
| """ |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_process_endpoint_exists(self, tmp_path): |
| """Test that POST /api/process endpoint exists. |
| |
| Requirement 8.1: System should provide POST /api/process interface. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| response = client.post("/api/process") |
| |
| |
| assert response.status_code == 400 |
| assert "error" in response.json() |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.SemanticParserService") |
| def test_process_text_input(self, mock_parser_class, tmp_path): |
| """Test processing text input (application/json format). |
| |
| Requirement 8.3: System should accept application/json format. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| from app.models import ParsedData |
| mock_parser = MagicMock() |
| mock_parser.parse = AsyncMock(return_value=ParsedData( |
| mood=None, |
| inspirations=[], |
| todos=[] |
| )) |
| mock_parser.close = AsyncMock() |
| mock_parser_class.return_value = mock_parser |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/process", |
| data={"text": "今天心情很好"} |
| ) |
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert "record_id" in data |
| assert "timestamp" in data |
| assert "mood" in data |
| assert "inspirations" in data |
| assert "todos" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.ASRService") |
| @patch("app.main.SemanticParserService") |
| def test_process_audio_input(self, mock_parser_class, mock_asr_class, tmp_path): |
| """Test processing audio input (multipart/form-data format). |
| |
| Requirement 8.2: System should accept multipart/form-data format. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| mock_asr = MagicMock() |
| mock_asr.transcribe = AsyncMock(return_value="转写后的文本") |
| mock_asr.close = AsyncMock() |
| mock_asr_class.return_value = mock_asr |
| |
| |
| from app.models import ParsedData |
| mock_parser = MagicMock() |
| mock_parser.parse = AsyncMock(return_value=ParsedData( |
| mood=None, |
| inspirations=[], |
| todos=[] |
| )) |
| mock_parser.close = AsyncMock() |
| mock_parser_class.return_value = mock_parser |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| audio_data = b"fake audio content" |
| files = {"audio": ("test.mp3", BytesIO(audio_data), "audio/mpeg")} |
| |
| response = client.post("/api/process", files=files) |
| |
| assert response.status_code == 200 |
| data = response.json() |
| assert "record_id" in data |
| assert "timestamp" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_validation_error_empty_input(self, tmp_path): |
| """Test validation error for empty input. |
| |
| Requirement 8.3: System should return HTTP 400 for validation errors. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| response = client.post("/api/process") |
| |
| assert response.status_code == 400 |
| data = response.json() |
| assert "error" in data |
| assert "timestamp" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_validation_error_unsupported_audio_format(self, tmp_path): |
| """Test validation error for unsupported audio format. |
| |
| Requirement 1.1: System should reject unsupported audio formats. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| audio_data = b"fake audio content" |
| files = {"audio": ("test.ogg", BytesIO(audio_data), "audio/ogg")} |
| |
| response = client.post("/api/process", files=files) |
| |
| assert response.status_code == 400 |
| data = response.json() |
| assert "error" in data |
| assert "不支持的音频格式" in data["error"] |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| def test_validation_error_file_too_large(self, tmp_path): |
| """Test validation error for file size exceeding limit. |
| |
| Requirement 1.4: System should reject files larger than max size. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log"), |
| "MAX_AUDIO_SIZE": "100" |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| audio_data = b"x" * 200 |
| files = {"audio": ("test.mp3", BytesIO(audio_data), "audio/mpeg")} |
| |
| response = client.post("/api/process", files=files) |
| |
| assert response.status_code == 400 |
| data = response.json() |
| assert "error" in data |
| assert "音频文件过大" in data["error"] |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.ASRService") |
| def test_asr_service_error(self, mock_asr_class, tmp_path): |
| """Test ASR service error handling. |
| |
| Requirement 8.3: System should return HTTP 500 for ASR service errors. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| from app.asr_service import ASRServiceError |
| mock_asr = MagicMock() |
| mock_asr.transcribe = AsyncMock(side_effect=ASRServiceError("API调用失败")) |
| mock_asr.close = AsyncMock() |
| mock_asr_class.return_value = mock_asr |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| audio_data = b"fake audio content" |
| files = {"audio": ("test.mp3", BytesIO(audio_data), "audio/mpeg")} |
| |
| response = client.post("/api/process", files=files) |
| |
| assert response.status_code == 500 |
| data = response.json() |
| assert "error" in data |
| assert "语音识别服务不可用" in data["error"] |
| assert "timestamp" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.SemanticParserService") |
| def test_semantic_parser_error(self, mock_parser_class, tmp_path): |
| """Test semantic parser error handling. |
| |
| Requirement 8.3: System should return HTTP 500 for semantic parser errors. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| from app.semantic_parser import SemanticParserError |
| mock_parser = MagicMock() |
| mock_parser.parse = AsyncMock(side_effect=SemanticParserError("API调用失败")) |
| mock_parser.close = AsyncMock() |
| mock_parser_class.return_value = mock_parser |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/process", |
| data={"text": "今天心情很好"} |
| ) |
| |
| assert response.status_code == 500 |
| data = response.json() |
| assert "error" in data |
| assert "语义解析服务不可用" in data["error"] |
| assert "timestamp" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.SemanticParserService") |
| @patch("app.main.StorageService") |
| def test_storage_error(self, mock_storage_class, mock_parser_class, tmp_path): |
| """Test storage error handling. |
| |
| Requirement 8.3: System should return HTTP 500 for storage errors. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| from app.models import ParsedData |
| mock_parser = MagicMock() |
| mock_parser.parse = AsyncMock(return_value=ParsedData( |
| mood=None, |
| inspirations=[], |
| todos=[] |
| )) |
| mock_parser.close = AsyncMock() |
| mock_parser_class.return_value = mock_parser |
| |
| |
| from app.storage import StorageError |
| mock_storage = MagicMock() |
| mock_storage.save_record = MagicMock(side_effect=StorageError("磁盘空间不足")) |
| mock_storage_class.return_value = mock_storage |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/process", |
| data={"text": "今天心情很好"} |
| ) |
| |
| assert response.status_code == 500 |
| data = response.json() |
| assert "error" in data |
| assert "数据存储失败" in data["error"] |
| assert "timestamp" in data |
| |
| @patch.dict(os.environ, {"ZHIPU_API_KEY": "test_key_1234567890"}, clear=True) |
| @patch("app.main.SemanticParserService") |
| def test_success_response_format(self, mock_parser_class, tmp_path): |
| """Test success response format. |
| |
| Requirement 8.4, 8.6: Success response should include all required fields. |
| """ |
| |
| import app.config |
| app.config._config = None |
| |
| |
| from app.models import MoodData, InspirationData, TodoData, ParsedData |
| mock_parser = MagicMock() |
| mock_parser.parse = AsyncMock(return_value=ParsedData( |
| mood=MoodData(type="开心", intensity=8, keywords=["愉快"]), |
| inspirations=[InspirationData(core_idea="新想法", tags=["创新"], category="工作")], |
| todos=[TodoData(task="完成报告", time="明天", location="办公室")] |
| )) |
| mock_parser.close = AsyncMock() |
| mock_parser_class.return_value = mock_parser |
| |
| with patch.dict(os.environ, { |
| "DATA_DIR": str(tmp_path / "data"), |
| "LOG_FILE": str(tmp_path / "logs" / "app.log") |
| }, clear=False): |
| from fastapi.testclient import TestClient |
| from app.main import app |
| |
| with TestClient(app) as client: |
| |
| response = client.post( |
| "/api/process", |
| data={"text": "今天心情很好,有个新想法,明天要完成报告"} |
| ) |
| |
| assert response.status_code == 200 |
| data = response.json() |
| |
| |
| assert "record_id" in data |
| assert "timestamp" in data |
| assert "mood" in data |
| assert "inspirations" in data |
| assert "todos" in data |
| |
| |
| assert data["mood"]["type"] == "开心" |
| assert data["mood"]["intensity"] == 8 |
| |
| |
| assert len(data["inspirations"]) == 1 |
| assert data["inspirations"][0]["core_idea"] == "新想法" |
| |
| |
| assert len(data["todos"]) == 1 |
| assert data["todos"][0]["task"] == "完成报告" |
|
|