""" Comprehensive API Endpoint Tests for ClaudeTools FastAPI Application This test suite validates all 5 core API endpoints: - Machines - Clients - Projects - Sessions - Tags Tests include: - API startup and health checks - CRUD operations for all entities - Authentication (with and without JWT tokens) - Pagination parameters - Error handling (404, 409, 422 responses) """ import sys from datetime import timedelta from uuid import uuid4 from fastapi.testclient import TestClient # Import the FastAPI app and auth utilities from api.main import app from api.middleware.auth import create_access_token # Create test client client = TestClient(app) # Test counters tests_passed = 0 tests_failed = 0 test_results = [] def log_test(test_name: str, passed: bool, error_msg: str = ""): """Log test result and update counters.""" global tests_passed, tests_failed if passed: tests_passed += 1 status = "PASS" symbol = "[+]" else: tests_failed += 1 status = "FAIL" symbol = "[-]" result = f"{symbol} {status}: {test_name}" if error_msg: result += f"\n Error: {error_msg}" test_results.append((test_name, passed, error_msg)) print(result) def create_test_token(): """Create a test JWT token for authentication.""" token_data = { "sub": "test_user@claudetools.com", "scopes": ["msp:read", "msp:write", "msp:admin"] } return create_access_token(token_data, expires_delta=timedelta(hours=1)) def get_auth_headers(): """Get authorization headers with test token.""" token = create_test_token() return {"Authorization": f"Bearer {token}"} # ============================================================================ # SECTION 1: API Health and Startup Tests # ============================================================================ print("\n" + "="*70) print("SECTION 1: API Health and Startup Tests") print("="*70 + "\n") def test_root_endpoint(): """Test root endpoint returns API status.""" try: response = client.get("/") assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "online", f"Expected status 'online', got {data.get('status')}" assert "service" in data, "Response missing 'service' field" assert "version" in data, "Response missing 'version' field" log_test("Root endpoint (/)", True) except Exception as e: log_test("Root endpoint (/)", False, str(e)) def test_health_endpoint(): """Test health check endpoint.""" try: response = client.get("/health") assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "healthy", f"Expected status 'healthy', got {data.get('status')}" log_test("Health check endpoint (/health)", True) except Exception as e: log_test("Health check endpoint (/health)", False, str(e)) def test_jwt_token_creation(): """Test JWT token creation.""" try: token = create_test_token() assert token is not None, "Token creation returned None" assert len(token) > 20, "Token seems too short" log_test("JWT token creation", True) except Exception as e: log_test("JWT token creation", False, str(e)) # ============================================================================ # SECTION 2: Authentication Tests # ============================================================================ print("\n" + "="*70) print("SECTION 2: Authentication Tests") print("="*70 + "\n") def test_unauthenticated_access(): """Test that protected endpoints reject requests without auth.""" try: response = client.get("/api/machines") # Can be 401 (Unauthorized) or 403 (Forbidden) depending on implementation assert response.status_code in [401, 403], f"Expected 401 or 403, got {response.status_code}" log_test("Unauthenticated access rejected", True) except Exception as e: log_test("Unauthenticated access rejected", False, str(e)) def test_authenticated_access(): """Test that protected endpoints accept valid JWT tokens.""" try: headers = get_auth_headers() response = client.get("/api/machines", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" log_test("Authenticated access accepted", True) except Exception as e: log_test("Authenticated access accepted", False, str(e)) def test_invalid_token(): """Test that invalid tokens are rejected.""" try: headers = {"Authorization": "Bearer invalid_token_string"} response = client.get("/api/machines", headers=headers) assert response.status_code == 401, f"Expected 401, got {response.status_code}" log_test("Invalid token rejected", True) except Exception as e: log_test("Invalid token rejected", False, str(e)) # ============================================================================ # SECTION 3: Machine CRUD Operations # ============================================================================ print("\n" + "="*70) print("SECTION 3: Machine CRUD Operations") print("="*70 + "\n") machine_id = None def test_create_machine(): """Test creating a new machine.""" global machine_id try: headers = get_auth_headers() machine_data = { "hostname": f"test-machine-{uuid4().hex[:8]}", "friendly_name": "Test Machine", "machine_type": "laptop", "platform": "win32", "is_active": True } response = client.post("/api/machines", json=machine_data, headers=headers) assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}" data = response.json() assert "id" in data, f"Response missing 'id' field. Data: {data}" machine_id = data["id"] print(f" Created machine with ID: {machine_id}") log_test("Create machine", True) except Exception as e: log_test("Create machine", False, str(e)) def test_list_machines(): """Test listing machines with pagination.""" try: headers = get_auth_headers() response = client.get("/api/machines?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "total" in data, "Response missing 'total' field" assert "machines" in data, "Response missing 'machines' field" assert isinstance(data["machines"], list), "machines field is not a list" log_test("List machines", True) except Exception as e: log_test("List machines", False, str(e)) def test_get_machine(): """Test retrieving a specific machine by ID.""" try: if machine_id is None: raise Exception("No machine_id available (create test may have failed)") headers = get_auth_headers() print(f" Fetching machine with ID: {machine_id} (type: {type(machine_id)})") # List all machines to check if our machine exists list_response = client.get("/api/machines", headers=headers) all_machines = list_response.json().get("machines", []) print(f" Total machines in DB: {len(all_machines)}") if all_machines: print(f" First machine ID: {all_machines[0].get('id')} (type: {type(all_machines[0].get('id'))})") response = client.get(f"/api/machines/{machine_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}. Response: {response.text}" data = response.json() assert str(data["id"]) == str(machine_id), f"Expected ID {machine_id}, got {data.get('id')}" log_test("Get machine by ID", True) except Exception as e: log_test("Get machine by ID", False, str(e)) def test_update_machine(): """Test updating a machine.""" try: if machine_id is None: raise Exception("No machine_id available (create test may have failed)") headers = get_auth_headers() update_data = { "friendly_name": "Updated Test Machine", "notes": "Updated during testing" } response = client.put(f"/api/machines/{machine_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["friendly_name"] == "Updated Test Machine", "Update not reflected" log_test("Update machine", True) except Exception as e: log_test("Update machine", False, str(e)) def test_machine_not_found(): """Test getting non-existent machine returns 404.""" try: headers = get_auth_headers() fake_id = str(uuid4()) response = client.get(f"/api/machines/{fake_id}", headers=headers) assert response.status_code == 404, f"Expected 404, got {response.status_code}" log_test("Machine not found (404)", True) except Exception as e: log_test("Machine not found (404)", False, str(e)) def test_delete_machine(): """Test deleting a machine.""" try: if machine_id is None: raise Exception("No machine_id available (create test may have failed)") headers = get_auth_headers() response = client.delete(f"/api/machines/{machine_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" log_test("Delete machine", True) except Exception as e: log_test("Delete machine", False, str(e)) # ============================================================================ # SECTION 4: Client CRUD Operations # ============================================================================ print("\n" + "="*70) print("SECTION 4: Client CRUD Operations") print("="*70 + "\n") client_id = None def test_create_client(): """Test creating a new client.""" global client_id try: headers = get_auth_headers() client_data = { "name": f"Test Client {uuid4().hex[:8]}", "type": "msp_client", "primary_contact": "John Doe", "is_active": True } response = client.post("/api/clients", json=client_data, headers=headers) assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}" data = response.json() assert "id" in data, f"Response missing 'id' field. Data: {data}" client_id = data["id"] print(f" Created client with ID: {client_id}") log_test("Create client", True) except Exception as e: log_test("Create client", False, str(e)) def test_list_clients(): """Test listing clients with pagination.""" try: headers = get_auth_headers() response = client.get("/api/clients?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "total" in data, "Response missing 'total' field" assert "clients" in data, "Response missing 'clients' field" log_test("List clients", True) except Exception as e: log_test("List clients", False, str(e)) def test_get_client(): """Test retrieving a specific client by ID.""" try: if client_id is None: raise Exception("No client_id available") headers = get_auth_headers() response = client.get(f"/api/clients/{client_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == client_id, f"Expected ID {client_id}, got {data.get('id')}" log_test("Get client by ID", True) except Exception as e: log_test("Get client by ID", False, str(e)) def test_update_client(): """Test updating a client.""" try: if client_id is None: raise Exception("No client_id available") headers = get_auth_headers() update_data = { "primary_contact": "Jane Doe", "notes": "Updated contact" } response = client.put(f"/api/clients/{client_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["primary_contact"] == "Jane Doe", "Update not reflected" log_test("Update client", True) except Exception as e: log_test("Update client", False, str(e)) def test_delete_client(): """Test deleting a client.""" try: if client_id is None: raise Exception("No client_id available") headers = get_auth_headers() response = client.delete(f"/api/clients/{client_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" log_test("Delete client", True) except Exception as e: log_test("Delete client", False, str(e)) # ============================================================================ # SECTION 5: Project CRUD Operations # ============================================================================ print("\n" + "="*70) print("SECTION 5: Project CRUD Operations") print("="*70 + "\n") project_id = None project_client_id = None def test_create_project(): """Test creating a new project.""" global project_id, project_client_id try: headers = get_auth_headers() # First create a client for the project client_data = { "name": f"Project Test Client {uuid4().hex[:8]}", "type": "msp_client", "is_active": True } client_response = client.post("/api/clients", json=client_data, headers=headers) assert client_response.status_code == 201, f"Failed to create test client: {client_response.text}" project_client_id = client_response.json()["id"] # Now create the project project_data = { "name": f"Test Project {uuid4().hex[:8]}", "client_id": project_client_id, "status": "active" } response = client.post("/api/projects", json=project_data, headers=headers) assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}" data = response.json() assert "id" in data, f"Response missing 'id' field. Data: {data}" project_id = data["id"] print(f" Created project with ID: {project_id}") log_test("Create project", True) except Exception as e: log_test("Create project", False, str(e)) def test_list_projects(): """Test listing projects with pagination.""" try: headers = get_auth_headers() response = client.get("/api/projects?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "total" in data, "Response missing 'total' field" assert "projects" in data, "Response missing 'projects' field" log_test("List projects", True) except Exception as e: log_test("List projects", False, str(e)) def test_get_project(): """Test retrieving a specific project by ID.""" try: if project_id is None: raise Exception("No project_id available") headers = get_auth_headers() response = client.get(f"/api/projects/{project_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == project_id, f"Expected ID {project_id}, got {data.get('id')}" log_test("Get project by ID", True) except Exception as e: log_test("Get project by ID", False, str(e)) def test_update_project(): """Test updating a project.""" try: if project_id is None: raise Exception("No project_id available") headers = get_auth_headers() update_data = { "status": "completed", "notes": "Project completed during testing" } response = client.put(f"/api/projects/{project_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "completed", "Update not reflected" log_test("Update project", True) except Exception as e: log_test("Update project", False, str(e)) def test_delete_project(): """Test deleting a project.""" try: if project_id is None: raise Exception("No project_id available") headers = get_auth_headers() response = client.delete(f"/api/projects/{project_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" # Clean up test client if project_client_id: client.delete(f"/api/clients/{project_client_id}", headers=headers) log_test("Delete project", True) except Exception as e: log_test("Delete project", False, str(e)) # ============================================================================ # SECTION 6: Session CRUD Operations # ============================================================================ print("\n" + "="*70) print("SECTION 6: Session CRUD Operations") print("="*70 + "\n") session_id = None session_client_id = None session_project_id = None def test_create_session(): """Test creating a new session.""" global session_id, session_client_id, session_project_id try: headers = get_auth_headers() # Create client for session client_data = { "name": f"Session Test Client {uuid4().hex[:8]}", "type": "msp_client", "is_active": True } client_response = client.post("/api/clients", json=client_data, headers=headers) assert client_response.status_code == 201, f"Failed to create test client: {client_response.text}" session_client_id = client_response.json()["id"] # Create project for session project_data = { "name": f"Session Test Project {uuid4().hex[:8]}", "client_id": session_client_id, "status": "active" } project_response = client.post("/api/projects", json=project_data, headers=headers) assert project_response.status_code == 201, f"Failed to create test project: {project_response.text}" session_project_id = project_response.json()["id"] # Create session from datetime import date session_data = { "session_title": f"Test Session {uuid4().hex[:8]}", "session_date": str(date.today()), "client_id": session_client_id, "project_id": session_project_id, "status": "completed" } response = client.post("/api/sessions", json=session_data, headers=headers) assert response.status_code == 201, f"Expected 201, got {response.status_code}. Response: {response.text}" data = response.json() assert "id" in data, f"Response missing 'id' field. Data: {data}" session_id = data["id"] print(f" Created session with ID: {session_id}") log_test("Create session", True) except Exception as e: log_test("Create session", False, str(e)) def test_list_sessions(): """Test listing sessions with pagination.""" try: headers = get_auth_headers() response = client.get("/api/sessions?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "total" in data, "Response missing 'total' field" assert "sessions" in data, "Response missing 'sessions' field" log_test("List sessions", True) except Exception as e: log_test("List sessions", False, str(e)) def test_get_session(): """Test retrieving a specific session by ID.""" try: if session_id is None: raise Exception("No session_id available") headers = get_auth_headers() response = client.get(f"/api/sessions/{session_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == session_id, f"Expected ID {session_id}, got {data.get('id')}" log_test("Get session by ID", True) except Exception as e: log_test("Get session by ID", False, str(e)) def test_update_session(): """Test updating a session.""" try: if session_id is None: raise Exception("No session_id available") headers = get_auth_headers() update_data = { "status": "completed", "summary": "Test session completed" } response = client.put(f"/api/sessions/{session_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "completed", "Update not reflected" log_test("Update session", True) except Exception as e: log_test("Update session", False, str(e)) def test_delete_session(): """Test deleting a session.""" try: if session_id is None: raise Exception("No session_id available") headers = get_auth_headers() response = client.delete(f"/api/sessions/{session_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" # Clean up test data if session_project_id: client.delete(f"/api/projects/{session_project_id}", headers=headers) if session_client_id: client.delete(f"/api/clients/{session_client_id}", headers=headers) log_test("Delete session", True) except Exception as e: log_test("Delete session", False, str(e)) # ============================================================================ # SECTION 7: Tag CRUD Operations # ============================================================================ print("\n" + "="*70) print("SECTION 7: Tag CRUD Operations") print("="*70 + "\n") tag_id = None def test_create_tag(): """Test creating a new tag.""" global tag_id try: headers = get_auth_headers() tag_data = { "name": f"test-tag-{uuid4().hex[:8]}", "category": "technology", "color": "#FF5733" } response = client.post("/api/tags", json=tag_data, headers=headers) assert response.status_code == 201, f"Expected 201, got {response.status_code}" data = response.json() assert "id" in data, "Response missing 'id' field" tag_id = data["id"] log_test("Create tag", True) except Exception as e: log_test("Create tag", False, str(e)) def test_list_tags(): """Test listing tags with pagination.""" try: headers = get_auth_headers() response = client.get("/api/tags?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "total" in data, "Response missing 'total' field" assert "tags" in data, "Response missing 'tags' field" log_test("List tags", True) except Exception as e: log_test("List tags", False, str(e)) def test_get_tag(): """Test retrieving a specific tag by ID.""" try: if tag_id is None: raise Exception("No tag_id available") headers = get_auth_headers() response = client.get(f"/api/tags/{tag_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == tag_id, f"Expected ID {tag_id}, got {data.get('id')}" log_test("Get tag by ID", True) except Exception as e: log_test("Get tag by ID", False, str(e)) def test_update_tag(): """Test updating a tag.""" try: if tag_id is None: raise Exception("No tag_id available") headers = get_auth_headers() update_data = { "color": "#00FF00", "description": "Updated test tag" } response = client.put(f"/api/tags/{tag_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["color"] == "#00FF00", "Update not reflected" log_test("Update tag", True) except Exception as e: log_test("Update tag", False, str(e)) def test_tag_duplicate_name(): """Test creating tag with duplicate name returns 409.""" try: if tag_id is None: raise Exception("No tag_id available") headers = get_auth_headers() # Get existing tag name existing_response = client.get(f"/api/tags/{tag_id}", headers=headers) existing_name = existing_response.json()["name"] # Try to create duplicate duplicate_data = { "name": existing_name, "category": "test" } response = client.post("/api/tags", json=duplicate_data, headers=headers) assert response.status_code == 409, f"Expected 409, got {response.status_code}" log_test("Tag duplicate name (409)", True) except Exception as e: log_test("Tag duplicate name (409)", False, str(e)) def test_delete_tag(): """Test deleting a tag.""" try: if tag_id is None: raise Exception("No tag_id available") headers = get_auth_headers() response = client.delete(f"/api/tags/{tag_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" log_test("Delete tag", True) except Exception as e: log_test("Delete tag", False, str(e)) # ============================================================================ # SECTION 8: Pagination Tests # ============================================================================ print("\n" + "="*70) print("SECTION 8: Pagination Tests") print("="*70 + "\n") def test_pagination_skip_limit(): """Test pagination with skip and limit parameters.""" try: headers = get_auth_headers() response = client.get("/api/machines?skip=0&limit=5", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["skip"] == 0, f"Expected skip=0, got {data.get('skip')}" assert data["limit"] == 5, f"Expected limit=5, got {data.get('limit')}" log_test("Pagination skip/limit parameters", True) except Exception as e: log_test("Pagination skip/limit parameters", False, str(e)) def test_pagination_max_limit(): """Test that pagination enforces maximum limit.""" try: headers = get_auth_headers() # Try to request more than max (1000) response = client.get("/api/machines?limit=2000", headers=headers) # Should either return 422 or clamp to max assert response.status_code in [200, 422], f"Unexpected status {response.status_code}" log_test("Pagination max limit enforcement", True) except Exception as e: log_test("Pagination max limit enforcement", False, str(e)) # ============================================================================ # Run All Tests # ============================================================================ def run_all_tests(): """Run all test functions.""" print("\n" + "="*70) print("CLAUDETOOLS API ENDPOINT TESTS") print("="*70) # Section 1: Health test_root_endpoint() test_health_endpoint() test_jwt_token_creation() # Section 2: Auth test_unauthenticated_access() test_authenticated_access() test_invalid_token() # Section 3: Machines test_create_machine() test_list_machines() test_get_machine() test_update_machine() test_machine_not_found() test_delete_machine() # Section 4: Clients test_create_client() test_list_clients() test_get_client() test_update_client() test_delete_client() # Section 5: Projects test_create_project() test_list_projects() test_get_project() test_update_project() test_delete_project() # Section 6: Sessions test_create_session() test_list_sessions() test_get_session() test_update_session() test_delete_session() # Section 7: Tags test_create_tag() test_list_tags() test_get_tag() test_update_tag() test_tag_duplicate_name() test_delete_tag() # Section 8: Pagination test_pagination_skip_limit() test_pagination_max_limit() if __name__ == "__main__": print("\n>> Starting ClaudeTools API Test Suite...") try: run_all_tests() # Print summary print("\n" + "="*70) print("TEST SUMMARY") print("="*70) print(f"\nTotal Tests: {tests_passed + tests_failed}") print(f"Passed: {tests_passed}") print(f"Failed: {tests_failed}") if tests_failed > 0: print("\nFAILED TESTS:") for name, passed, error in test_results: if not passed: print(f" - {name}") if error: print(f" Error: {error}") if tests_failed == 0: print("\n>> All tests passed!") sys.exit(0) else: print(f"\n>> {tests_failed} test(s) failed") sys.exit(1) except Exception as e: print(f"\n>> Fatal error running tests: {e}") import traceback traceback.print_exc() sys.exit(1)