diff --git a/src/backend/app.py b/src/backend/app.py index 550b1e1a3..4685b7fd1 100644 --- a/src/backend/app.py +++ b/src/backend/app.py @@ -161,6 +161,14 @@ async def handle_chat(): """Unified chat endpoint - routes messages to appropriate handlers based on intent.""" data = await request.get_json() + if not data: + return jsonify({ + "action_type": "error", + "message": "Request body is required", + "data": {}, + "conversation_id": "" + }), 400 + # Extract request fields conversation_id = data.get("conversation_id") or str(uuid.uuid4()) user_id = data.get("user_id", "anonymous") @@ -168,6 +176,15 @@ async def handle_chat(): action = data.get("action") payload = data.get("payload", {}) + # Validate that message content is not empty when no action is specified + if not action and not message.strip(): + return jsonify({ + "action_type": "error", + "message": "Message content must not be empty", + "data": {}, + "conversation_id": conversation_id + }), 400 + selected_products = data.get("selected_products", []) brief_data = data.get("brief", {}) diff --git a/src/tests/test_app.py b/src/tests/test_app.py index 57d9237d1..7882a72ae 100644 --- a/src/tests/test_app.py +++ b/src/tests/test_app.py @@ -63,7 +63,46 @@ async def test_health_check_api(client): @pytest.mark.asyncio async def test_chat_missing_message(client): - """Test chat endpoint with missing message still returns response (no validation).""" + """Test chat endpoint rejects missing/empty message with 400.""" + response = await client.post( + "/api/chat", + json={"conversation_id": "test-conv"} + ) + + assert response.status_code == 400 + data = await response.get_json() + assert data["action_type"] == "error" + assert "empty" in data["message"].lower() + + +@pytest.mark.asyncio +async def test_chat_empty_body(client): + """Test chat endpoint rejects empty request body with 400.""" + response = await client.post( + "/api/chat", + data="", + headers={"Content-Type": "application/json"} + ) + + assert response.status_code == 400 + + +@pytest.mark.asyncio +async def test_chat_whitespace_message(client): + """Test chat endpoint rejects whitespace-only message with 400.""" + response = await client.post( + "/api/chat", + json={"conversation_id": "test-conv", "message": " "} + ) + + assert response.status_code == 400 + data = await response.get_json() + assert data["action_type"] == "error" + + +@pytest.mark.asyncio +async def test_chat_empty_message_with_action_allowed(client): + """Test chat endpoint allows empty message when action is specified.""" with patch("app.get_routing_service") as mock_routing, \ patch("app.get_cosmos_service") as mock_cosmos, \ patch("app.get_orchestrator") as mock_orch: @@ -88,10 +127,10 @@ async def test_chat_missing_message(client): response = await client.post( "/api/chat", - json={"conversation_id": "test-conv"} + json={"conversation_id": "test-conv", "action": "confirm_brief", "message": ""} ) - # API doesn't validate missing message - routes to handler with empty message + # Action-based requests bypass message validation assert response.status_code in [200, 500] @@ -187,36 +226,15 @@ async def test_chat_cosmos_failure(client): @pytest.mark.asyncio async def test_parse_brief_missing_text(client): - """Test chat endpoint with missing message still processes (no validation).""" - with patch("app.get_routing_service") as mock_routing, \ - patch("app.get_cosmos_service") as mock_cosmos, \ - patch("app.get_orchestrator") as mock_orch: - - from services.routing_service import Intent, RoutingResult, ConversationState - mock_routing_service = MagicMock() - mock_routing_service.classify_intent = MagicMock(return_value=RoutingResult( - intent=Intent.PARSE_BRIEF, - confidence=0.5 - )) - mock_routing_service.derive_state_from_conversation = MagicMock(return_value=ConversationState()) - mock_routing.return_value = mock_routing_service - - mock_cosmos_service = AsyncMock() - mock_cosmos_service.get_conversation = AsyncMock(return_value=None) - mock_cosmos_service.add_message_to_conversation = AsyncMock() - mock_cosmos.return_value = mock_cosmos_service - - mock_orchestrator = AsyncMock() - mock_orchestrator.parse_brief = AsyncMock(return_value=(MagicMock(model_dump=lambda: {}), None, False)) - mock_orch.return_value = mock_orchestrator - - response = await client.post( - "/api/chat", - json={"conversation_id": "test-conv"} - ) + """Test chat endpoint rejects missing message with 400.""" + response = await client.post( + "/api/chat", + json={"conversation_id": "test-conv"} + ) - # API doesn't validate missing message - routes to handler with empty message - assert response.status_code in [200, 500] + assert response.status_code == 400 + data = await response.get_json() + assert data["action_type"] == "error" @pytest.mark.asyncio @@ -864,39 +882,17 @@ async def test_regenerate_content_success(client, sample_creative_brief_dict): @pytest.mark.asyncio async def test_regenerate_content_missing_modification_request(client, sample_creative_brief_dict): - """Test regeneration without message still routes (no validation).""" - with patch("app.get_routing_service") as mock_routing, \ - patch("app.get_cosmos_service") as mock_cosmos, \ - patch("app.get_orchestrator") as mock_orch: - - from services.routing_service import Intent, RoutingResult, ConversationState - mock_routing_service = MagicMock() - mock_routing_service.classify_intent = MagicMock(return_value=RoutingResult( - intent=Intent.PARSE_BRIEF, - confidence=0.5 - )) - mock_routing_service.derive_state_from_conversation = MagicMock(return_value=ConversationState()) - mock_routing.return_value = mock_routing_service - - mock_cosmos_service = AsyncMock() - mock_cosmos_service.get_conversation = AsyncMock(return_value=None) - mock_cosmos_service.add_message_to_conversation = AsyncMock() - mock_cosmos.return_value = mock_cosmos_service - - mock_orchestrator = AsyncMock() - mock_orchestrator.parse_brief = AsyncMock(return_value=(MagicMock(model_dump=lambda: {}), None, False)) - mock_orch.return_value = mock_orchestrator - - response = await client.post( - "/api/chat", - json={ - "conversation_id": "test-conv" - # Missing message - no validation in backend - } - ) + """Test regeneration rejects missing message with 400.""" + response = await client.post( + "/api/chat", + json={ + "conversation_id": "test-conv" + } + ) - # Backend doesn't validate missing message - assert response.status_code in [200, 500] + assert response.status_code == 400 + data = await response.get_json() + assert data["action_type"] == "error" @pytest.mark.asyncio