|
| 1 | +import json |
| 2 | +import os |
| 3 | +import time |
| 4 | +from typing import Any, Dict |
| 5 | + |
| 6 | +import pytest |
| 7 | +import requests |
| 8 | + |
| 9 | +# Backend API base URL |
| 10 | +API_BASE_URL = "http://localhost:8000" |
| 11 | + |
| 12 | +# Skip these tests if no backend server is running (CI environment) |
| 13 | +pytestmark = pytest.mark.skipif( |
| 14 | + os.getenv("CI") == "true" and not os.getenv("RUN_INTEGRATION_TESTS"), |
| 15 | + reason="Integration tests require a running backend server", |
| 16 | +) |
| 17 | + |
| 18 | + |
| 19 | +class TestProjectIntegration: |
| 20 | + """Test project integration between frontend API client and backend""" |
| 21 | + |
| 22 | + def setup_method(self): |
| 23 | + """Setup for each test method""" |
| 24 | + self.base_url = API_BASE_URL |
| 25 | + self.headers = {"Content-Type": "application/json"} |
| 26 | + |
| 27 | + def test_backend_health_check(self): |
| 28 | + """Test that backend is running and healthy""" |
| 29 | + response = requests.get(f"{self.base_url}/health") |
| 30 | + assert ( |
| 31 | + response.status_code == 200 |
| 32 | + ), f"Health check failed: {response.status_code}" |
| 33 | + |
| 34 | + # Check if we get a proper health response |
| 35 | + try: |
| 36 | + data = response.json() |
| 37 | + # Should have success field or be a health status |
| 38 | + assert ( |
| 39 | + "success" in data or "status" in data |
| 40 | + ), f"Invalid health response: {data}" |
| 41 | + except json.JSONDecodeError: |
| 42 | + # If no JSON, at least check it responds |
| 43 | + assert response.status_code == 200 |
| 44 | + |
| 45 | + def test_root_endpoint(self): |
| 46 | + """Test root endpoint returns expected format""" |
| 47 | + response = requests.get(f"{self.base_url}/") |
| 48 | + assert ( |
| 49 | + response.status_code == 200 |
| 50 | + ), f"Root endpoint failed: {response.status_code}" |
| 51 | + |
| 52 | + data = response.json() |
| 53 | + assert data["success"] is True, f"Root response missing success: {data}" |
| 54 | + assert "data" in data, f"Root response missing data field: {data}" |
| 55 | + assert ( |
| 56 | + data["data"]["message"] == "SmartQuery API is running" |
| 57 | + ), f"Unexpected message: {data}" |
| 58 | + assert data["data"]["status"] == "healthy", f"Unexpected status: {data}" |
| 59 | + |
| 60 | + def test_project_endpoints_structure(self): |
| 61 | + """Test that project endpoints are available (even if auth required)""" |
| 62 | + # Test GET /projects |
| 63 | + response = requests.get(f"{self.base_url}/projects") |
| 64 | + # Should return 401 (auth required) or 200, not 404 |
| 65 | + assert response.status_code in [ |
| 66 | + 200, |
| 67 | + 401, |
| 68 | + 403, |
| 69 | + ], f"Projects GET unexpected status: {response.status_code}" |
| 70 | + |
| 71 | + # Test POST /projects |
| 72 | + response = requests.post(f"{self.base_url}/projects", json={}) |
| 73 | + # Should return 401 (auth required) or 422 (validation error), not 404 |
| 74 | + assert response.status_code in [ |
| 75 | + 401, |
| 76 | + 403, |
| 77 | + 422, |
| 78 | + ], f"Projects POST unexpected status: {response.status_code}" |
| 79 | + |
| 80 | + def test_auth_endpoints_structure(self): |
| 81 | + """Test that auth endpoints are available""" |
| 82 | + # Test GET /auth/me |
| 83 | + response = requests.get(f"{self.base_url}/auth/me") |
| 84 | + # Should return 401 (auth required), not 404 |
| 85 | + assert response.status_code in [ |
| 86 | + 401, |
| 87 | + 403, |
| 88 | + ], f"Auth me unexpected status: {response.status_code}" |
| 89 | + |
| 90 | + # Test POST /auth/logout |
| 91 | + response = requests.post(f"{self.base_url}/auth/logout") |
| 92 | + # Should return 401 (auth required) or handle gracefully, not 404 |
| 93 | + assert response.status_code in [ |
| 94 | + 200, |
| 95 | + 401, |
| 96 | + 403, |
| 97 | + ], f"Auth logout unexpected status: {response.status_code}" |
| 98 | + |
| 99 | + def test_api_response_format(self): |
| 100 | + """Test that API responses follow expected format""" |
| 101 | + response = requests.get(f"{self.base_url}/") |
| 102 | + assert ( |
| 103 | + response.status_code == 200 |
| 104 | + ), f"API format test failed: {response.status_code}" |
| 105 | + |
| 106 | + data = response.json() |
| 107 | + # Check API response structure matches frontend expectations |
| 108 | + assert isinstance(data, dict), f"Response not a dict: {type(data)}" |
| 109 | + assert "success" in data, f"Response missing success field: {data}" |
| 110 | + assert isinstance( |
| 111 | + data["success"], bool |
| 112 | + ), f"Success field not boolean: {data['success']}" |
| 113 | + |
| 114 | + if "data" in data: |
| 115 | + assert isinstance( |
| 116 | + data["data"], dict |
| 117 | + ), f"Data field not a dict: {type(data['data'])}" |
| 118 | + |
| 119 | + def test_cors_headers(self): |
| 120 | + """Test that CORS is properly configured for frontend""" |
| 121 | + # Test preflight request |
| 122 | + headers = { |
| 123 | + "Origin": "http://localhost:3000", |
| 124 | + "Access-Control-Request-Method": "POST", |
| 125 | + "Access-Control-Request-Headers": "Content-Type,Authorization", |
| 126 | + } |
| 127 | + |
| 128 | + response = requests.options(f"{self.base_url}/projects", headers=headers) |
| 129 | + |
| 130 | + # Should handle CORS or at least not fail completely |
| 131 | + # Accept 200 (CORS enabled) or 405 (method not allowed, but server responds) |
| 132 | + assert response.status_code in [ |
| 133 | + 200, |
| 134 | + 405, |
| 135 | + ], f"CORS test failed: {response.status_code}" |
| 136 | + |
| 137 | + def test_mock_auth_mode(self): |
| 138 | + """Test if mock auth mode is working for development""" |
| 139 | + # Try to access endpoints that might work in mock mode |
| 140 | + response = requests.get(f"{self.base_url}/health") |
| 141 | + assert ( |
| 142 | + response.status_code == 200 |
| 143 | + ), f"Health check failed in mock auth test: {response.status_code}" |
| 144 | + |
| 145 | + # Check if backend is configured for development |
| 146 | + try: |
| 147 | + # Some endpoints might be accessible in mock mode |
| 148 | + response = requests.get(f"{self.base_url}/projects", headers=self.headers) |
| 149 | + |
| 150 | + if response.status_code == 200: |
| 151 | + # Mock mode working - verify response structure |
| 152 | + data = response.json() |
| 153 | + assert ( |
| 154 | + "success" in data or "items" in data or isinstance(data, list) |
| 155 | + ), f"Invalid mock response: {data}" |
| 156 | + else: |
| 157 | + # Auth required - expected in production mode |
| 158 | + assert response.status_code in [ |
| 159 | + 401, |
| 160 | + 403, |
| 161 | + ], f"Unexpected auth status: {response.status_code}" |
| 162 | + except requests.exceptions.ConnectionError: |
| 163 | + raise Exception( |
| 164 | + "Backend not accessible - ensure it's running on localhost:8000" |
| 165 | + ) |
| 166 | + |
| 167 | + def test_error_handling_format(self): |
| 168 | + """Test that error responses follow expected format""" |
| 169 | + # Test invalid endpoint |
| 170 | + response = requests.get(f"{self.base_url}/invalid-endpoint") |
| 171 | + assert ( |
| 172 | + response.status_code == 404 |
| 173 | + ), f"Invalid endpoint should return 404: {response.status_code}" |
| 174 | + |
| 175 | + # Test malformed request |
| 176 | + response = requests.post( |
| 177 | + f"{self.base_url}/projects", |
| 178 | + data="invalid json", |
| 179 | + headers={"Content-Type": "application/json"}, |
| 180 | + ) |
| 181 | + |
| 182 | + # Should return proper error status |
| 183 | + assert response.status_code in [ |
| 184 | + 400, |
| 185 | + 401, |
| 186 | + 403, |
| 187 | + 422, |
| 188 | + ], f"Malformed request unexpected status: {response.status_code}" |
| 189 | + |
| 190 | + def test_api_documentation_available(self): |
| 191 | + """Test that API documentation is accessible""" |
| 192 | + # Test OpenAPI docs |
| 193 | + response = requests.get(f"{self.base_url}/docs") |
| 194 | + assert ( |
| 195 | + response.status_code == 200 |
| 196 | + ), f"API docs not accessible: {response.status_code}" |
| 197 | + |
| 198 | + # Test OpenAPI spec |
| 199 | + response = requests.get(f"{self.base_url}/openapi.json") |
| 200 | + assert ( |
| 201 | + response.status_code == 200 |
| 202 | + ), f"OpenAPI spec not accessible: {response.status_code}" |
| 203 | + |
| 204 | + # Verify it's valid JSON |
| 205 | + data = response.json() |
| 206 | + assert "openapi" in data or "info" in data, f"Invalid OpenAPI spec: {data}" |
| 207 | + |
| 208 | + |
| 209 | +def run_all_tests(): |
| 210 | + """Run all integration tests""" |
| 211 | + test = TestProjectIntegration() |
| 212 | + test.setup_method() |
| 213 | + |
| 214 | + tests = [ |
| 215 | + ("Backend Health Check", test.test_backend_health_check), |
| 216 | + ("Root Endpoint", test.test_root_endpoint), |
| 217 | + ("Project Endpoints Structure", test.test_project_endpoints_structure), |
| 218 | + ("Auth Endpoints Structure", test.test_auth_endpoints_structure), |
| 219 | + ("API Response Format", test.test_api_response_format), |
| 220 | + ("CORS Headers", test.test_cors_headers), |
| 221 | + ("Mock Auth Mode", test.test_mock_auth_mode), |
| 222 | + ("Error Handling Format", test.test_error_handling_format), |
| 223 | + ("API Documentation", test.test_api_documentation_available), |
| 224 | + ] |
| 225 | + |
| 226 | + passed = 0 |
| 227 | + failed = 0 |
| 228 | + |
| 229 | + for test_name, test_func in tests: |
| 230 | + try: |
| 231 | + test_func() |
| 232 | + print(f"✅ {test_name}") |
| 233 | + passed += 1 |
| 234 | + except Exception as e: |
| 235 | + print(f"❌ {test_name}: {e}") |
| 236 | + failed += 1 |
| 237 | + |
| 238 | + print(f"\n📊 Results: {passed} passed, {failed} failed") |
| 239 | + |
| 240 | + if failed == 0: |
| 241 | + print("🎉 All integration tests passed!") |
| 242 | + return True |
| 243 | + else: |
| 244 | + print("❌ Some tests failed. Check backend is running on localhost:8000") |
| 245 | + return False |
| 246 | + |
| 247 | + |
| 248 | +if __name__ == "__main__": |
| 249 | + success = run_all_tests() |
| 250 | + exit(0 if success else 1) |
0 commit comments