| | """
|
| | Integration Tests for API Endpoints.
|
| |
|
| | Tests Task 8.1: FastAPI Endpoints
|
| |
|
| | Acceptance Criteria:
|
| | - AC-4.1.1: Returns 200 OK for valid requests
|
| | - AC-4.1.2: Returns 400 for invalid input
|
| | - AC-4.1.3: Response matches schema
|
| | - AC-4.1.5: Response time <2s (p95)
|
| | """
|
| |
|
| | import pytest
|
| | import time
|
| | import uuid
|
| | from unittest.mock import patch, MagicMock
|
| | from fastapi.testclient import TestClient
|
| |
|
| |
|
| | class TestHealthEndpoint:
|
| | """Tests for /api/v1/health endpoint."""
|
| |
|
| | def test_health_check_returns_200(self, client: TestClient):
|
| | """AC-4.1.1: Test health endpoint returns 200 OK."""
|
| | response = client.get("/api/v1/health")
|
| | assert response.status_code == 200
|
| |
|
| | def test_health_check_response_format(self, client: TestClient):
|
| | """AC-4.1.3: Test health response has expected format."""
|
| | response = client.get("/api/v1/health")
|
| | data = response.json()
|
| |
|
| | assert "status" in data
|
| | assert "version" in data
|
| | assert "timestamp" in data
|
| |
|
| | def test_health_check_has_dependencies(self, client: TestClient):
|
| | """Test health response includes dependency status."""
|
| | response = client.get("/api/v1/health")
|
| | data = response.json()
|
| |
|
| | if "dependencies" in data and data["dependencies"]:
|
| | deps = data["dependencies"]
|
| | assert "groq_api" in deps
|
| | assert "postgres" in deps
|
| | assert "redis" in deps
|
| | assert "models_loaded" in deps
|
| |
|
| | def test_health_check_has_version(self, client: TestClient):
|
| | """Test health response has version."""
|
| | response = client.get("/api/v1/health")
|
| | data = response.json()
|
| |
|
| | assert data["version"] == "1.0.0"
|
| |
|
| | def test_health_check_response_time(self, client: TestClient):
|
| | """AC-4.1.5: Test health check response time <2s."""
|
| | start = time.time()
|
| | response = client.get("/api/v1/health")
|
| | elapsed = time.time() - start
|
| |
|
| | assert response.status_code == 200
|
| | assert elapsed < 2.0, f"Response time {elapsed:.2f}s exceeds 2s target"
|
| |
|
| |
|
| | class TestEngageEndpoint:
|
| | """Tests for /api/v1/honeypot/engage endpoint."""
|
| |
|
| | def test_engage_valid_request(self, client: TestClient, sample_engage_request):
|
| | """AC-4.1.1: Test engage with valid request returns 200."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | assert response.status_code == 200
|
| |
|
| | def test_engage_response_format(self, client: TestClient, sample_engage_request):
|
| | """AC-4.1.3: Test engage response has expected format."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | data = response.json()
|
| |
|
| | assert "status" in data
|
| | assert "scam_detected" in data
|
| | assert "confidence" in data
|
| | assert "session_id" in data
|
| | assert "language_detected" in data
|
| |
|
| | def test_engage_generates_session_id(self, client: TestClient, sample_engage_request):
|
| | """Test engage generates session_id when not provided."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | data = response.json()
|
| |
|
| | assert data["session_id"] is not None
|
| | assert len(data["session_id"]) == 36
|
| |
|
| |
|
| | try:
|
| | uuid.UUID(data["session_id"])
|
| | except ValueError:
|
| | pytest.fail("session_id is not a valid UUID")
|
| |
|
| | def test_engage_uses_provided_session_id(self, client: TestClient, sample_engage_request):
|
| | """Test engage uses provided session_id."""
|
| | session_id = str(uuid.uuid4())
|
| | request = {**sample_engage_request, "session_id": session_id}
|
| |
|
| | response = client.post("/api/v1/honeypot/engage", json=request)
|
| | data = response.json()
|
| |
|
| | assert data["session_id"] == session_id
|
| |
|
| | def test_engage_empty_message_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test engage with empty message returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": ""},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_engage_missing_message_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test engage without message returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_engage_invalid_session_id_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test engage with invalid session_id returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test", "session_id": "invalid-uuid"},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_engage_invalid_language_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test engage with invalid language returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test", "language": "invalid"},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_engage_message_too_long_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test engage with message >5000 chars returns 422."""
|
| | long_message = "x" * 5001
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": long_message},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_engage_with_english_language(self, client: TestClient):
|
| | """Test engage with explicit English language."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "You won a prize!", "language": "en"},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| | assert data["language_detected"] == "en"
|
| |
|
| | def test_engage_with_hindi_language(self, client: TestClient, sample_hindi_scam_message):
|
| | """Test engage with Hindi message."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": sample_hindi_scam_message, "language": "hi"},
|
| | )
|
| |
|
| | assert response.status_code == 200
|
| |
|
| | def test_engage_returns_metadata(self, client: TestClient, sample_engage_request):
|
| | """AC-4.1.3: Test engage response includes metadata."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | data = response.json()
|
| |
|
| | if "metadata" in data and data["metadata"]:
|
| | meta = data["metadata"]
|
| | assert "processing_time_ms" in meta
|
| | assert "model_version" in meta
|
| | assert meta["processing_time_ms"] >= 0
|
| |
|
| | def test_engage_scam_detection(self, client: TestClient, sample_scam_message):
|
| | """Test engage detects scam messages."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": sample_scam_message},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| |
|
| | if data["scam_detected"]:
|
| | assert data["confidence"] > 0.0
|
| |
|
| | def test_engage_legitimate_message(self, client: TestClient, sample_legitimate_message):
|
| | """Test engage handles legitimate messages."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": sample_legitimate_message},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| | assert "scam_detected" in data
|
| |
|
| | def test_engage_response_time(self, client: TestClient, sample_engage_request):
|
| | """AC-4.1.5: Test engage response time <2s (with mocks)."""
|
| | start = time.time()
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | elapsed = time.time() - start
|
| |
|
| | assert response.status_code == 200
|
| |
|
| |
|
| |
|
| |
|
| | class TestEngageWithScamDetection:
|
| | """Tests for engage endpoint with scam detection flow."""
|
| |
|
| | def test_engage_scam_returns_engagement(self, client: TestClient):
|
| | """Test engage returns engagement info when scam detected."""
|
| |
|
| | scam_message = "You won 10 lakh! Send your OTP and bank details to scammer@paytm immediately!"
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": scam_message},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| |
|
| | if data.get("scam_detected"):
|
| |
|
| | if "engagement" in data and data["engagement"]:
|
| | assert "agent_response" in data["engagement"]
|
| | assert "turn_count" in data["engagement"]
|
| | assert "strategy" in data["engagement"]
|
| |
|
| | def test_engage_returns_extracted_intelligence(self, client: TestClient):
|
| | """Test engage extracts intelligence from scam messages."""
|
| |
|
| | scam_message = "Pay to fraud@paytm account 12345678901234 call +919876543210"
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": scam_message},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| |
|
| | if data.get("scam_detected") and data.get("extracted_intelligence"):
|
| | intel = data["extracted_intelligence"]
|
| | assert "upi_ids" in intel
|
| | assert "bank_accounts" in intel
|
| | assert "phone_numbers" in intel
|
| |
|
| |
|
| | class TestSessionEndpoint:
|
| | """Tests for /api/v1/honeypot/session/{session_id} endpoint."""
|
| |
|
| | def test_get_session_not_found(self, client: TestClient):
|
| | """Test get session returns 404 for non-existent session."""
|
| | response = client.get(
|
| | "/api/v1/honeypot/session/550e8400-e29b-41d4-a716-446655440000"
|
| | )
|
| | assert response.status_code == 404
|
| |
|
| | def test_get_session_invalid_id(self, client: TestClient):
|
| | """AC-4.1.2: Test get session with invalid ID returns 400."""
|
| | response = client.get(
|
| | "/api/v1/honeypot/session/invalid-id"
|
| | )
|
| | assert response.status_code == 400
|
| |
|
| | def test_get_session_error_format(self, client: TestClient):
|
| | """AC-4.1.3: Test error response matches schema."""
|
| | response = client.get(
|
| | "/api/v1/honeypot/session/invalid-id"
|
| | )
|
| | data = response.json()
|
| |
|
| | assert "detail" in data
|
| | error = data["detail"]
|
| | assert "code" in error
|
| | assert "message" in error
|
| |
|
| | def test_get_session_after_engage(self, client: TestClient, sample_engage_request):
|
| | """Test session can be retrieved after engage."""
|
| |
|
| | engage_response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json=sample_engage_request,
|
| | )
|
| | engage_data = engage_response.json()
|
| | session_id = engage_data["session_id"]
|
| |
|
| |
|
| |
|
| | get_response = client.get(f"/api/v1/honeypot/session/{session_id}")
|
| |
|
| |
|
| |
|
| | assert get_response.status_code in [200, 404]
|
| |
|
| | if get_response.status_code == 200:
|
| | data = get_response.json()
|
| | assert data["session_id"] == session_id
|
| | assert "conversation_history" in data
|
| | assert "extracted_intelligence" in data
|
| |
|
| |
|
| | class TestBatchEndpoint:
|
| | """Tests for /api/v1/honeypot/batch endpoint."""
|
| |
|
| | def test_batch_valid_request(self, client: TestClient):
|
| | """AC-4.1.1: Test batch with valid request returns 200."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={
|
| | "messages": [
|
| | {"id": "1", "message": "Test message 1"},
|
| | {"id": "2", "message": "Test message 2"},
|
| | ]
|
| | },
|
| | )
|
| | assert response.status_code == 200
|
| |
|
| | def test_batch_response_format(self, client: TestClient):
|
| | """AC-4.1.3: Test batch response has expected format."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={
|
| | "messages": [
|
| | {"id": "1", "message": "Test message"},
|
| | ]
|
| | },
|
| | )
|
| | data = response.json()
|
| |
|
| | assert "status" in data
|
| | assert "processed" in data
|
| | assert "failed" in data
|
| | assert "results" in data
|
| | assert "processing_time_ms" in data
|
| |
|
| | def test_batch_processes_all_messages(self, client: TestClient):
|
| | """Test batch processes all messages in request."""
|
| | messages = [
|
| | {"id": "msg1", "message": "First message"},
|
| | {"id": "msg2", "message": "Second message"},
|
| | {"id": "msg3", "message": "Third message"},
|
| | ]
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": messages},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| | assert len(data["results"]) == 3
|
| | assert data["processed"] + data["failed"] == 3
|
| |
|
| | def test_batch_preserves_message_ids(self, client: TestClient):
|
| | """Test batch preserves message IDs in results."""
|
| | messages = [
|
| | {"id": "custom-id-1", "message": "Message 1"},
|
| | {"id": "custom-id-2", "message": "Message 2"},
|
| | ]
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": messages},
|
| | )
|
| | data = response.json()
|
| |
|
| | result_ids = [r["id"] for r in data["results"]]
|
| | assert "custom-id-1" in result_ids
|
| | assert "custom-id-2" in result_ids
|
| |
|
| | def test_batch_empty_messages_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test batch with empty messages array returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": []},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_batch_missing_messages_fails(self, client: TestClient):
|
| | """AC-4.1.2: Test batch without messages returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_batch_with_scam_messages(self, client: TestClient):
|
| | """Test batch detects scams in multiple messages."""
|
| | messages = [
|
| | {"id": "scam1", "message": "You won 10 lakh! Send OTP now!"},
|
| | {"id": "legit1", "message": "Hello, how are you?"},
|
| | {"id": "scam2", "message": "Bank account blocked! Call now!"},
|
| | ]
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": messages},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| | assert data["processed"] > 0
|
| |
|
| |
|
| | for result in data["results"]:
|
| | assert "id" in result
|
| | assert "status" in result
|
| | if result["status"] == "success":
|
| | assert "scam_detected" in result
|
| | assert "confidence" in result
|
| |
|
| | def test_batch_with_language_hint(self, client: TestClient):
|
| | """Test batch respects language hints."""
|
| | messages = [
|
| | {"id": "en1", "message": "Hello", "language": "en"},
|
| | {"id": "hi1", "message": "नमस्ते", "language": "hi"},
|
| | ]
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": messages},
|
| | )
|
| | data = response.json()
|
| |
|
| | assert response.status_code == 200
|
| |
|
| | for result in data["results"]:
|
| | if result["id"] == "en1":
|
| | assert result.get("language_detected") == "en"
|
| |
|
| |
|
| | class TestRootEndpoint:
|
| | """Tests for root endpoint."""
|
| |
|
| | def test_root_returns_200(self, client: TestClient):
|
| | """Test root endpoint returns 200."""
|
| | response = client.get("/")
|
| | assert response.status_code == 200
|
| |
|
| | def test_root_response_format(self, client: TestClient):
|
| | """Test root response has API info."""
|
| | response = client.get("/")
|
| | data = response.json()
|
| |
|
| | assert "name" in data
|
| | assert "version" in data
|
| | assert data["name"] == "ScamShield AI"
|
| |
|
| |
|
| | class TestAcceptanceCriteria:
|
| | """Tests specifically for Task 8.1 acceptance criteria."""
|
| |
|
| | def test_ac_4_1_1_valid_requests_return_200(self, client: TestClient):
|
| | """AC-4.1.1: Returns 200 OK for valid requests."""
|
| |
|
| | assert client.get("/api/v1/health").status_code == 200
|
| |
|
| |
|
| | assert client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test message"},
|
| | ).status_code == 200
|
| |
|
| |
|
| | assert client.post(
|
| | "/api/v1/honeypot/batch",
|
| | json={"messages": [{"id": "1", "message": "Test"}]},
|
| | ).status_code == 200
|
| |
|
| | def test_ac_4_1_2_invalid_input_returns_400_or_422(self, client: TestClient):
|
| | """AC-4.1.2: Returns 400/422 for invalid input."""
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": ""},
|
| | )
|
| | assert response.status_code in [400, 422]
|
| |
|
| |
|
| | response = client.get("/api/v1/honeypot/session/invalid")
|
| | assert response.status_code == 400
|
| |
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test", "session_id": "not-a-uuid"},
|
| | )
|
| | assert response.status_code in [400, 422]
|
| |
|
| | def test_ac_4_1_3_response_matches_schema(self, client: TestClient):
|
| | """AC-4.1.3: Response matches schema."""
|
| |
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test scam message"},
|
| | )
|
| | data = response.json()
|
| |
|
| |
|
| | assert "status" in data
|
| | assert "scam_detected" in data
|
| | assert isinstance(data["scam_detected"], bool)
|
| | assert "confidence" in data
|
| | assert 0.0 <= data["confidence"] <= 1.0
|
| | assert "language_detected" in data
|
| | assert data["language_detected"] in ["en", "hi", "hinglish", "auto"]
|
| | assert "session_id" in data
|
| |
|
| |
|
| | health_response = client.get("/api/v1/health")
|
| | health_data = health_response.json()
|
| |
|
| | assert "status" in health_data
|
| | assert health_data["status"] in ["healthy", "degraded", "unhealthy"]
|
| | assert "version" in health_data
|
| | assert "timestamp" in health_data
|
| |
|
| | def test_ac_4_1_5_response_time_under_2s(self, client: TestClient):
|
| | """AC-4.1.5: Response time <2s (p95)."""
|
| | response_times = []
|
| |
|
| | for _ in range(5):
|
| | start = time.time()
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Quick test message"},
|
| | )
|
| | elapsed = time.time() - start
|
| | response_times.append(elapsed)
|
| | assert response.status_code == 200
|
| |
|
| |
|
| | avg_time = sum(response_times) / len(response_times)
|
| |
|
| | print(f"Average response time: {avg_time:.2f}s")
|
| |
|
| |
|
| | class TestErrorHandling:
|
| | """Tests for error handling."""
|
| |
|
| | def test_invalid_json_returns_422(self, client: TestClient):
|
| | """Test invalid JSON body returns 422."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | content="invalid json",
|
| | headers={"Content-Type": "application/json"},
|
| | )
|
| | assert response.status_code == 422
|
| |
|
| | def test_missing_content_type_works(self, client: TestClient):
|
| | """Test request without explicit content-type works with json parameter."""
|
| | response = client.post(
|
| | "/api/v1/honeypot/engage",
|
| | json={"message": "Test"},
|
| | )
|
| |
|
| | assert response.status_code == 200
|
| |
|