""" Comprehensive API Endpoint Tests for ClaudeTools Phase 5 Endpoints This test suite validates all 12 Phase 5 API endpoints across 3 categories: MSP Work Tracking (3 entities): - Work Items API - /api/work-items - Tasks API - /api/tasks - Billable Time API - /api/billable-time Infrastructure Management (6 entities): - Sites API - /api/sites - Infrastructure API - /api/infrastructure - Services API - /api/services - Networks API - /api/networks - Firewall Rules API - /api/firewall-rules - M365 Tenants API - /api/m365-tenants Credentials Management (3 entities): - Credentials API - /api/credentials (with encryption!) - Credential Audit Logs API - /api/credential-audit-logs (read-only) - Security Incidents API - /api/security-incidents Tests include: - CRUD operations for all entities - Authentication (with JWT tokens) - Pagination parameters - Relationship queries (by-client, by-site, etc.) - Special features (encryption for credentials, audit log creation) - Error handling (404, 409, 422 responses) """ import sys from datetime import date, 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 = [] # Track created entities for cleanup created_entities = { "clients": [], "sites": [], "projects": [], "sessions": [], "work_items": [], "tasks": [], "billable_time": [], "infrastructure": [], "services": [], "networks": [], "firewall_rules": [], "m365_tenants": [], "credentials": [], "security_incidents": [], } 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: MSP Work Tracking - Work Items API # ============================================================================ print("\n" + "="*70) print("SECTION 1: Work Items API Tests") print("="*70 + "\n") # First, create dependencies (client, project, session) work_items_client_id = None work_items_project_id = None work_items_session_id = None work_item_id = None def test_create_work_items_dependencies(): """Create client, project, and session for work items tests.""" global work_items_client_id, work_items_project_id, work_items_session_id try: headers = get_auth_headers() # Create client client_data = { "name": f"WorkItems Test Client {uuid4().hex[:8]}", "type": "msp_client", "is_active": True } response = client.post("/api/clients", json=client_data, headers=headers) assert response.status_code == 201, f"Failed to create client: {response.text}" work_items_client_id = response.json()["id"] created_entities["clients"].append(work_items_client_id) # Create project project_data = { "name": f"WorkItems Test Project {uuid4().hex[:8]}", "client_id": work_items_client_id, "status": "active" } response = client.post("/api/projects", json=project_data, headers=headers) assert response.status_code == 201, f"Failed to create project: {response.text}" work_items_project_id = response.json()["id"] created_entities["projects"].append(work_items_project_id) # Create session session_data = { "session_title": f"WorkItems Test Session {uuid4().hex[:8]}", "session_date": str(date.today()), "client_id": work_items_client_id, "project_id": work_items_project_id, "status": "completed" } response = client.post("/api/sessions", json=session_data, headers=headers) assert response.status_code == 201, f"Failed to create session: {response.text}" work_items_session_id = response.json()["id"] created_entities["sessions"].append(work_items_session_id) log_test("Create work items dependencies (client, project, session)", True) except Exception as e: log_test("Create work items dependencies (client, project, session)", False, str(e)) def test_create_work_item(): """Test creating a work item.""" global work_item_id try: headers = get_auth_headers() work_item_data = { "session_id": work_items_session_id, "category": "infrastructure", "title": f"Test Work Item {uuid4().hex[:8]}", "description": "Testing work item creation", "status": "completed", "priority": "high", "is_billable": True, "estimated_minutes": 30, "actual_minutes": 25 } response = client.post("/api/work-items", json=work_item_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, "Response missing 'id' field" work_item_id = data["id"] created_entities["work_items"].append(work_item_id) print(f" Created work item with ID: {work_item_id}") log_test("Create work item", True) except Exception as e: log_test("Create work item", False, str(e)) def test_list_work_items(): """Test listing work items.""" try: headers = get_auth_headers() response = client.get("/api/work-items?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 "work_items" in data, "Response missing 'work_items' field" log_test("List work items", True) except Exception as e: log_test("List work items", False, str(e)) def test_get_work_item(): """Test getting a specific work item.""" try: if work_item_id is None: raise Exception("No work_item_id available") headers = get_auth_headers() response = client.get(f"/api/work-items/{work_item_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == work_item_id, f"Expected ID {work_item_id}, got {data.get('id')}" log_test("Get work item by ID", True) except Exception as e: log_test("Get work item by ID", False, str(e)) def test_update_work_item(): """Test updating a work item.""" try: if work_item_id is None: raise Exception("No work_item_id available") headers = get_auth_headers() update_data = { "status": "completed", "actual_minutes": 30 } response = client.put(f"/api/work-items/{work_item_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["actual_minutes"] == 30, "Update not reflected" log_test("Update work item", True) except Exception as e: log_test("Update work item", False, str(e)) def test_get_work_items_by_client(): """Test getting work items by client.""" try: headers = get_auth_headers() response = client.get(f"/api/work-items/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "work_items" in data, "Response missing 'work_items' field" log_test("Get work items by client", True) except Exception as e: log_test("Get work items by client", False, str(e)) # ============================================================================ # SECTION 2: MSP Work Tracking - Tasks API # ============================================================================ print("\n" + "="*70) print("SECTION 2: Tasks API Tests") print("="*70 + "\n") task_id = None def test_create_task(): """Test creating a task.""" global task_id try: headers = get_auth_headers() task_data = { "client_id": work_items_client_id, "title": f"Test Task {uuid4().hex[:8]}", "description": "Testing task creation", "status": "pending", "task_order": 1 } response = client.post("/api/tasks", json=task_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, "Response missing 'id' field" task_id = data["id"] created_entities["tasks"].append(task_id) print(f" Created task with ID: {task_id}") log_test("Create task", True) except Exception as e: log_test("Create task", False, str(e)) def test_list_tasks(): """Test listing tasks.""" try: headers = get_auth_headers() response = client.get("/api/tasks?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 "tasks" in data, "Response missing 'tasks' field" log_test("List tasks", True) except Exception as e: log_test("List tasks", False, str(e)) def test_get_task(): """Test getting a specific task.""" try: if task_id is None: raise Exception("No task_id available") headers = get_auth_headers() response = client.get(f"/api/tasks/{task_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == task_id, f"Expected ID {task_id}, got {data.get('id')}" log_test("Get task by ID", True) except Exception as e: log_test("Get task by ID", False, str(e)) def test_update_task(): """Test updating a task.""" try: if task_id is None: raise Exception("No task_id available") headers = get_auth_headers() update_data = { "status": "in_progress", "priority": "high" } response = client.put(f"/api/tasks/{task_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "in_progress", "Update not reflected" log_test("Update task", True) except Exception as e: log_test("Update task", False, str(e)) def test_get_tasks_filtering(): """Test getting tasks with status filtering.""" try: headers = get_auth_headers() response = client.get(f"/api/tasks?status_filter=pending&skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "tasks" in data, "Response missing 'tasks' field" log_test("Get tasks with status filtering", True) except Exception as e: log_test("Get tasks with status filtering", False, str(e)) # ============================================================================ # SECTION 3: MSP Work Tracking - Billable Time API # ============================================================================ print("\n" + "="*70) print("SECTION 3: Billable Time API Tests") print("="*70 + "\n") billable_time_id = None def test_create_billable_time(): """Test creating a billable time entry.""" global billable_time_id try: headers = get_auth_headers() from datetime import datetime billable_time_data = { "client_id": work_items_client_id, "session_id": work_items_session_id, "work_item_id": work_item_id, "description": "Testing billable time entry", "start_time": datetime.now().isoformat(), "duration_minutes": 60, "hourly_rate": 150.00, "total_amount": 150.00, "category": "consulting", "is_billable": True } response = client.post("/api/billable-time", json=billable_time_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, "Response missing 'id' field" billable_time_id = data["id"] created_entities["billable_time"].append(billable_time_id) print(f" Created billable time with ID: {billable_time_id}") log_test("Create billable time", True) except Exception as e: log_test("Create billable time", False, str(e)) def test_list_billable_time(): """Test listing billable time entries.""" try: headers = get_auth_headers() response = client.get("/api/billable-time?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 "billable_time" in data, "Response missing 'billable_time' field" log_test("List billable time", True) except Exception as e: log_test("List billable time", False, str(e)) def test_get_billable_time(): """Test getting a specific billable time entry.""" try: if billable_time_id is None: raise Exception("No billable_time_id available") headers = get_auth_headers() response = client.get(f"/api/billable-time/{billable_time_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == billable_time_id, f"Expected ID {billable_time_id}, got {data.get('id')}" log_test("Get billable time by ID", True) except Exception as e: log_test("Get billable time by ID", False, str(e)) def test_update_billable_time(): """Test updating a billable time entry.""" try: if billable_time_id is None: raise Exception("No billable_time_id available") headers = get_auth_headers() update_data = { "duration_minutes": 90, "hourly_rate": 175.00, "total_amount": 262.50 } response = client.put(f"/api/billable-time/{billable_time_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["duration_minutes"] == 90, "Update not reflected" log_test("Update billable time", True) except Exception as e: log_test("Update billable time", False, str(e)) def test_get_billable_time_by_session(): """Test getting billable time by session.""" try: headers = get_auth_headers() response = client.get(f"/api/billable-time/by-session/{work_items_session_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "billable_time" in data, "Response missing 'billable_time' field" log_test("Get billable time by session", True) except Exception as e: log_test("Get billable time by session", False, str(e)) # ============================================================================ # SECTION 4: Infrastructure Management - Sites API # ============================================================================ print("\n" + "="*70) print("SECTION 4: Sites API Tests") print("="*70 + "\n") site_id = None def test_create_site(): """Test creating a site.""" global site_id try: headers = get_auth_headers() site_data = { "client_id": work_items_client_id, "name": f"Test Site {uuid4().hex[:8]}", "network_subnet": "172.16.1.0/24", "vpn_required": True, "gateway_ip": "172.16.1.1" } response = client.post("/api/sites", json=site_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, "Response missing 'id' field" site_id = data["id"] created_entities["sites"].append(site_id) print(f" Created site with ID: {site_id}") log_test("Create site", True) except Exception as e: log_test("Create site", False, str(e)) def test_list_sites(): """Test listing sites.""" try: headers = get_auth_headers() response = client.get("/api/sites?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 "sites" in data, "Response missing 'sites' field" log_test("List sites", True) except Exception as e: log_test("List sites", False, str(e)) def test_get_site(): """Test getting a specific site.""" try: if site_id is None: raise Exception("No site_id available") headers = get_auth_headers() response = client.get(f"/api/sites/{site_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == site_id, f"Expected ID {site_id}, got {data.get('id')}" log_test("Get site by ID", True) except Exception as e: log_test("Get site by ID", False, str(e)) def test_update_site(): """Test updating a site.""" try: if site_id is None: raise Exception("No site_id available") headers = get_auth_headers() update_data = { "vpn_required": False, "notes": "VPN disabled" } response = client.put(f"/api/sites/{site_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["vpn_required"] == False, "Update not reflected" log_test("Update site", True) except Exception as e: log_test("Update site", False, str(e)) def test_get_sites_by_client(): """Test getting sites by client.""" try: headers = get_auth_headers() response = client.get(f"/api/sites/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "sites" in data, "Response missing 'sites' field" log_test("Get sites by client", True) except Exception as e: log_test("Get sites by client", False, str(e)) # ============================================================================ # SECTION 5: Infrastructure Management - Infrastructure API # ============================================================================ print("\n" + "="*70) print("SECTION 5: Infrastructure API Tests") print("="*70 + "\n") infrastructure_id = None def test_create_infrastructure(): """Test creating an infrastructure component.""" global infrastructure_id try: headers = get_auth_headers() infra_data = { "site_id": site_id, "asset_type": "physical_server", "hostname": f"test-server-{uuid4().hex[:8]}", "ip_address": "172.16.1.100", "status": "active" } response = client.post("/api/infrastructure", json=infra_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, "Response missing 'id' field" infrastructure_id = data["id"] created_entities["infrastructure"].append(infrastructure_id) print(f" Created infrastructure with ID: {infrastructure_id}") log_test("Create infrastructure", True) except Exception as e: log_test("Create infrastructure", False, str(e)) def test_list_infrastructure(): """Test listing infrastructure components.""" try: headers = get_auth_headers() response = client.get("/api/infrastructure?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 "infrastructure" in data, "Response missing 'infrastructure' field" log_test("List infrastructure", True) except Exception as e: log_test("List infrastructure", False, str(e)) def test_get_infrastructure(): """Test getting a specific infrastructure component.""" try: if infrastructure_id is None: raise Exception("No infrastructure_id available") headers = get_auth_headers() response = client.get(f"/api/infrastructure/{infrastructure_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == infrastructure_id, f"Expected ID {infrastructure_id}, got {data.get('id')}" log_test("Get infrastructure by ID", True) except Exception as e: log_test("Get infrastructure by ID", False, str(e)) def test_update_infrastructure(): """Test updating an infrastructure component.""" try: if infrastructure_id is None: raise Exception("No infrastructure_id available") headers = get_auth_headers() update_data = { "os": "Ubuntu 24.04", "notes": "OS updated" } response = client.put(f"/api/infrastructure/{infrastructure_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["os"] == "Ubuntu 24.04", "Update not reflected" log_test("Update infrastructure", True) except Exception as e: log_test("Update infrastructure", False, str(e)) def test_get_infrastructure_by_site(): """Test getting infrastructure by site.""" try: headers = get_auth_headers() response = client.get(f"/api/infrastructure/by-site/{site_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "infrastructure" in data, "Response missing 'infrastructure' field" log_test("Get infrastructure by site", True) except Exception as e: log_test("Get infrastructure by site", False, str(e)) # ============================================================================ # SECTION 6: Infrastructure Management - Services API # ============================================================================ print("\n" + "="*70) print("SECTION 6: Services API Tests") print("="*70 + "\n") service_id = None def test_create_service(): """Test creating a service.""" global service_id try: headers = get_auth_headers() service_data = { "infrastructure_id": infrastructure_id, "service_name": f"Test Service {uuid4().hex[:8]}", "service_type": "web_server", "port": 443, "protocol": "https", "status": "running" } response = client.post("/api/services", json=service_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, "Response missing 'id' field" service_id = data["id"] created_entities["services"].append(service_id) print(f" Created service with ID: {service_id}") log_test("Create service", True) except Exception as e: log_test("Create service", False, str(e)) def test_list_services(): """Test listing services.""" try: headers = get_auth_headers() response = client.get("/api/services?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 "services" in data, "Response missing 'services' field" log_test("List services", True) except Exception as e: log_test("List services", False, str(e)) def test_get_service(): """Test getting a specific service.""" try: if service_id is None: raise Exception("No service_id available") headers = get_auth_headers() response = client.get(f"/api/services/{service_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == service_id, f"Expected ID {service_id}, got {data.get('id')}" log_test("Get service by ID", True) except Exception as e: log_test("Get service by ID", False, str(e)) def test_update_service(): """Test updating a service.""" try: if service_id is None: raise Exception("No service_id available") headers = get_auth_headers() update_data = { "status": "stopped", "notes": "Service stopped for maintenance" } response = client.put(f"/api/services/{service_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "stopped", "Update not reflected" log_test("Update service", True) except Exception as e: log_test("Update service", False, str(e)) def test_get_services_by_client(): """Test getting services by client.""" try: headers = get_auth_headers() response = client.get(f"/api/services/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "services" in data, "Response missing 'services' field" log_test("Get services by client", True) except Exception as e: log_test("Get services by client", False, str(e)) # ============================================================================ # SECTION 7: Infrastructure Management - Networks API # ============================================================================ print("\n" + "="*70) print("SECTION 7: Networks API Tests") print("="*70 + "\n") network_id = None def test_create_network(): """Test creating a network.""" global network_id try: headers = get_auth_headers() network_data = { "site_id": site_id, "network_name": f"Test Network {uuid4().hex[:8]}", "cidr": "10.0.1.0/24", "vlan_id": 100, "network_type": "lan" } response = client.post("/api/networks", json=network_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, "Response missing 'id' field" network_id = data["id"] created_entities["networks"].append(network_id) print(f" Created network with ID: {network_id}") log_test("Create network", True) except Exception as e: log_test("Create network", False, str(e)) def test_list_networks(): """Test listing networks.""" try: headers = get_auth_headers() response = client.get("/api/networks?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 "networks" in data, "Response missing 'networks' field" log_test("List networks", True) except Exception as e: log_test("List networks", False, str(e)) def test_get_network(): """Test getting a specific network.""" try: if network_id is None: raise Exception("No network_id available") headers = get_auth_headers() response = client.get(f"/api/networks/{network_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == network_id, f"Expected ID {network_id}, got {data.get('id')}" log_test("Get network by ID", True) except Exception as e: log_test("Get network by ID", False, str(e)) def test_update_network(): """Test updating a network.""" try: if network_id is None: raise Exception("No network_id available") headers = get_auth_headers() update_data = { "vlan_id": 200, "notes": "VLAN updated" } response = client.put(f"/api/networks/{network_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["vlan_id"] == 200, "Update not reflected" log_test("Update network", True) except Exception as e: log_test("Update network", False, str(e)) def test_get_networks_by_site(): """Test getting networks by site.""" try: headers = get_auth_headers() response = client.get(f"/api/networks/by-site/{site_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "networks" in data, "Response missing 'networks' field" log_test("Get networks by site", True) except Exception as e: log_test("Get networks by site", False, str(e)) # ============================================================================ # SECTION 8: Infrastructure Management - Firewall Rules API # ============================================================================ print("\n" + "="*70) print("SECTION 8: Firewall Rules API Tests") print("="*70 + "\n") firewall_rule_id = None def test_create_firewall_rule(): """Test creating a firewall rule.""" global firewall_rule_id try: headers = get_auth_headers() firewall_data = { "infrastructure_id": infrastructure_id, "rule_name": f"Test Rule {uuid4().hex[:8]}", "source": "0.0.0.0/0", "destination": "172.16.1.100", "port": 443, "protocol": "tcp", "action": "allow", "priority": 100 } response = client.post("/api/firewall-rules", json=firewall_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, "Response missing 'id' field" firewall_rule_id = data["id"] created_entities["firewall_rules"].append(firewall_rule_id) print(f" Created firewall rule with ID: {firewall_rule_id}") log_test("Create firewall rule", True) except Exception as e: log_test("Create firewall rule", False, str(e)) def test_list_firewall_rules(): """Test listing firewall rules.""" try: headers = get_auth_headers() response = client.get("/api/firewall-rules?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 "firewall_rules" in data, "Response missing 'firewall_rules' field" log_test("List firewall rules", True) except Exception as e: log_test("List firewall rules", False, str(e)) def test_get_firewall_rule(): """Test getting a specific firewall rule.""" try: if firewall_rule_id is None: raise Exception("No firewall_rule_id available") headers = get_auth_headers() response = client.get(f"/api/firewall-rules/{firewall_rule_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == firewall_rule_id, f"Expected ID {firewall_rule_id}, got {data.get('id')}" log_test("Get firewall rule by ID", True) except Exception as e: log_test("Get firewall rule by ID", False, str(e)) def test_update_firewall_rule(): """Test updating a firewall rule.""" try: if firewall_rule_id is None: raise Exception("No firewall_rule_id available") headers = get_auth_headers() update_data = { "action": "deny", "notes": "Rule changed to deny" } response = client.put(f"/api/firewall-rules/{firewall_rule_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["action"] == "deny", "Update not reflected" log_test("Update firewall rule", True) except Exception as e: log_test("Update firewall rule", False, str(e)) def test_get_firewall_rules_by_infrastructure(): """Test getting firewall rules by infrastructure.""" try: headers = get_auth_headers() response = client.get(f"/api/firewall-rules/by-infrastructure/{infrastructure_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "firewall_rules" in data, "Response missing 'firewall_rules' field" log_test("Get firewall rules by infrastructure", True) except Exception as e: log_test("Get firewall rules by infrastructure", False, str(e)) # ============================================================================ # SECTION 9: Infrastructure Management - M365 Tenants API # ============================================================================ print("\n" + "="*70) print("SECTION 9: M365 Tenants API Tests") print("="*70 + "\n") m365_tenant_id = None def test_create_m365_tenant(): """Test creating an M365 tenant.""" global m365_tenant_id try: headers = get_auth_headers() m365_data = { "client_id": work_items_client_id, "tenant_name": f"Test Tenant {uuid4().hex[:8]}", "tenant_id": str(uuid4()), "primary_domain": f"test{uuid4().hex[:8]}.onmicrosoft.com", "admin_email": "admin@test.com" } response = client.post("/api/m365-tenants", json=m365_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, "Response missing 'id' field" m365_tenant_id = data["id"] created_entities["m365_tenants"].append(m365_tenant_id) print(f" Created M365 tenant with ID: {m365_tenant_id}") log_test("Create M365 tenant", True) except Exception as e: log_test("Create M365 tenant", False, str(e)) def test_list_m365_tenants(): """Test listing M365 tenants.""" try: headers = get_auth_headers() response = client.get("/api/m365-tenants?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 "m365_tenants" in data, "Response missing 'm365_tenants' field" log_test("List M365 tenants", True) except Exception as e: log_test("List M365 tenants", False, str(e)) def test_get_m365_tenant(): """Test getting a specific M365 tenant.""" try: if m365_tenant_id is None: raise Exception("No m365_tenant_id available") headers = get_auth_headers() response = client.get(f"/api/m365-tenants/{m365_tenant_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == m365_tenant_id, f"Expected ID {m365_tenant_id}, got {data.get('id')}" log_test("Get M365 tenant by ID", True) except Exception as e: log_test("Get M365 tenant by ID", False, str(e)) def test_update_m365_tenant(): """Test updating an M365 tenant.""" try: if m365_tenant_id is None: raise Exception("No m365_tenant_id available") headers = get_auth_headers() update_data = { "admin_email": "newadmin@test.com", "notes": "Admin email updated" } response = client.put(f"/api/m365-tenants/{m365_tenant_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["admin_email"] == "newadmin@test.com", "Update not reflected" log_test("Update M365 tenant", True) except Exception as e: log_test("Update M365 tenant", False, str(e)) def test_get_m365_tenants_by_client(): """Test getting M365 tenants by client.""" try: headers = get_auth_headers() response = client.get(f"/api/m365-tenants/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "m365_tenants" in data, "Response missing 'm365_tenants' field" log_test("Get M365 tenants by client", True) except Exception as e: log_test("Get M365 tenants by client", False, str(e)) # ============================================================================ # SECTION 10: Credentials Management - Credentials API (with Encryption!) # ============================================================================ print("\n" + "="*70) print("SECTION 10: Credentials API Tests (with Encryption)") print("="*70 + "\n") credential_id = None def test_create_credential_password(): """Test creating a password credential with encryption.""" global credential_id try: headers = get_auth_headers() credential_data = { "client_id": work_items_client_id, "service_id": service_id, "credential_type": "password", "service_name": f"Test Service Cred {uuid4().hex[:8]}", "username": "testuser", "password": "SuperSecretPassword123!", "requires_vpn": False, "requires_2fa": True, "is_active": True } response = client.post("/api/credentials", json=credential_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, "Response missing 'id' field" credential_id = data["id"] created_entities["credentials"].append(credential_id) # Verify password is decrypted in response assert data["password"] == "SuperSecretPassword123!", "Password not decrypted in response" print(f" Created credential with ID: {credential_id}") print(f" Password encrypted and decrypted successfully: {data['password']}") log_test("Create credential with password encryption", True) except Exception as e: log_test("Create credential with password encryption", False, str(e)) def test_create_credential_api_key(): """Test creating an API key credential with encryption.""" try: headers = get_auth_headers() credential_data = { "client_id": work_items_client_id, "credential_type": "api_key", "service_name": f"Test API Key {uuid4().hex[:8]}", "api_key": "sk-test-1234567890abcdef", "is_active": True } response = client.post("/api/credentials", json=credential_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, "Response missing 'id' field" created_entities["credentials"].append(data["id"]) # Verify API key is decrypted in response assert data["api_key"] == "sk-test-1234567890abcdef", "API key not decrypted in response" print(f" API key encrypted and decrypted successfully") log_test("Create credential with API key encryption", True) except Exception as e: log_test("Create credential with API key encryption", False, str(e)) def test_create_credential_oauth(): """Test creating an OAuth credential with encryption.""" try: headers = get_auth_headers() credential_data = { "client_id": work_items_client_id, "credential_type": "oauth", "service_name": f"Test OAuth {uuid4().hex[:8]}", "client_id_oauth": "oauth-client-id-123", "client_secret": "oauth-secret-xyz789", "tenant_id_oauth": "tenant-abc-456", "is_active": True } response = client.post("/api/credentials", json=credential_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, "Response missing 'id' field" created_entities["credentials"].append(data["id"]) # Verify client secret is decrypted in response assert data["client_secret"] == "oauth-secret-xyz789", "Client secret not decrypted in response" print(f" OAuth client secret encrypted and decrypted successfully") log_test("Create credential with OAuth encryption", True) except Exception as e: log_test("Create credential with OAuth encryption", False, str(e)) def test_list_credentials(): """Test listing credentials.""" try: headers = get_auth_headers() response = client.get("/api/credentials?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 "credentials" in data, "Response missing 'credentials' field" log_test("List credentials", True) except Exception as e: log_test("List credentials", False, str(e)) def test_get_credential(): """Test getting a specific credential (should create audit log).""" try: if credential_id is None: raise Exception("No credential_id available") headers = get_auth_headers() response = client.get(f"/api/credentials/{credential_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == credential_id, f"Expected ID {credential_id}, got {data.get('id')}" # Verify password is still decrypted assert data["password"] is not None, "Password not present in response" print(f" Credential retrieved and audit log created") log_test("Get credential by ID (creates audit log)", True) except Exception as e: log_test("Get credential by ID (creates audit log)", False, str(e)) def test_update_credential(): """Test updating a credential.""" try: if credential_id is None: raise Exception("No credential_id available") headers = get_auth_headers() update_data = { "password": "NewSuperSecretPassword456!", "requires_2fa": False } response = client.put(f"/api/credentials/{credential_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() # Verify new password is decrypted in response assert data["password"] == "NewSuperSecretPassword456!", "Password update not reflected" assert data["requires_2fa"] == False, "Update not reflected" print(f" Password re-encrypted successfully") log_test("Update credential (re-encrypts password)", True) except Exception as e: log_test("Update credential (re-encrypts password)", False, str(e)) def test_get_credentials_by_client(): """Test getting credentials by client.""" try: headers = get_auth_headers() response = client.get(f"/api/credentials/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "credentials" in data, "Response missing 'credentials' field" log_test("Get credentials by client", True) except Exception as e: log_test("Get credentials by client", False, str(e)) # ============================================================================ # SECTION 11: Credentials Management - Credential Audit Logs API (Read-Only) # ============================================================================ print("\n" + "="*70) print("SECTION 11: Credential Audit Logs API Tests (Read-Only)") print("="*70 + "\n") def test_list_credential_audit_logs(): """Test listing credential audit logs.""" try: headers = get_auth_headers() response = client.get("/api/credential-audit-logs?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 "logs" in data, "Response missing 'logs' field" # Should have at least some audit logs from credential operations assert data["total"] > 0, "No audit logs found (should have been created by credential operations)" print(f" Found {data['total']} audit log entries") log_test("List credential audit logs", True) except Exception as e: log_test("List credential audit logs", False, str(e)) def test_get_credential_audit_logs_by_credential(): """Test getting audit logs for a specific credential.""" try: if credential_id is None: raise Exception("No credential_id available") headers = get_auth_headers() response = client.get(f"/api/credential-audit-logs/by-credential/{credential_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "logs" in data, "Response missing 'logs' field" # Should have audit logs for CREATE, VIEW, and UPDATE actions assert data["total"] >= 3, f"Expected at least 3 audit logs (create, view, update), got {data['total']}" print(f" Found {data['total']} audit logs for credential") log_test("Get audit logs by credential", True) except Exception as e: log_test("Get audit logs by credential", False, str(e)) def test_get_credential_audit_logs_by_user(): """Test getting audit logs by user.""" try: headers = get_auth_headers() user_id = "test_user@claudetools.com" response = client.get(f"/api/credential-audit-logs/by-user/{user_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "logs" in data, "Response missing 'logs' field" print(f" Found {data['total']} audit logs for user") log_test("Get audit logs by user", True) except Exception as e: log_test("Get audit logs by user", False, str(e)) # ============================================================================ # SECTION 12: Credentials Management - Security Incidents API # ============================================================================ print("\n" + "="*70) print("SECTION 12: Security Incidents API Tests") print("="*70 + "\n") security_incident_id = None def test_create_security_incident(): """Test creating a security incident.""" global security_incident_id try: headers = get_auth_headers() from datetime import datetime incident_data = { "client_id": work_items_client_id, "incident_type": "unauthorized_access", "severity": "high", "description": "Testing security incident creation", "incident_date": datetime.now().isoformat(), "status": "investigating" } response = client.post("/api/security-incidents", json=incident_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, "Response missing 'id' field" security_incident_id = data["id"] created_entities["security_incidents"].append(security_incident_id) print(f" Created security incident with ID: {security_incident_id}") log_test("Create security incident", True) except Exception as e: log_test("Create security incident", False, str(e)) def test_list_security_incidents(): """Test listing security incidents.""" try: headers = get_auth_headers() response = client.get("/api/security-incidents?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 "incidents" in data, "Response missing 'incidents' field" log_test("List security incidents", True) except Exception as e: log_test("List security incidents", False, str(e)) def test_get_security_incident(): """Test getting a specific security incident.""" try: if security_incident_id is None: raise Exception("No security_incident_id available") headers = get_auth_headers() response = client.get(f"/api/security-incidents/{security_incident_id}", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["id"] == security_incident_id, f"Expected ID {security_incident_id}, got {data.get('id')}" log_test("Get security incident by ID", True) except Exception as e: log_test("Get security incident by ID", False, str(e)) def test_update_security_incident(): """Test updating a security incident.""" try: if security_incident_id is None: raise Exception("No security_incident_id available") headers = get_auth_headers() update_data = { "status": "resolved", "severity": "medium", "remediation_steps": "Issue resolved" } response = client.put(f"/api/security-incidents/{security_incident_id}", json=update_data, headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert data["status"] == "resolved", "Update not reflected" log_test("Update security incident", True) except Exception as e: log_test("Update security incident", False, str(e)) def test_get_security_incidents_by_client(): """Test getting security incidents by client.""" try: headers = get_auth_headers() response = client.get(f"/api/security-incidents/by-client/{work_items_client_id}?skip=0&limit=10", headers=headers) assert response.status_code == 200, f"Expected 200, got {response.status_code}" data = response.json() assert "incidents" in data, "Response missing 'incidents' field" log_test("Get security incidents by client", True) except Exception as e: log_test("Get security incidents by client", False, str(e)) # ============================================================================ # SECTION 13: Cleanup - Delete Test Data # ============================================================================ print("\n" + "="*70) print("SECTION 13: Cleanup - Delete Test Data") print("="*70 + "\n") def test_cleanup_all_entities(): """Clean up all created test entities in the correct order.""" try: headers = get_auth_headers() cleanup_count = 0 # Delete in reverse order of dependencies # Delete security incidents for entity_id in created_entities["security_incidents"]: response = client.delete(f"/api/security-incidents/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete credentials (audit logs remain) for entity_id in created_entities["credentials"]: response = client.delete(f"/api/credentials/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete M365 tenants for entity_id in created_entities["m365_tenants"]: response = client.delete(f"/api/m365-tenants/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete firewall rules for entity_id in created_entities["firewall_rules"]: response = client.delete(f"/api/firewall-rules/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete networks for entity_id in created_entities["networks"]: response = client.delete(f"/api/networks/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete services for entity_id in created_entities["services"]: response = client.delete(f"/api/services/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete infrastructure for entity_id in created_entities["infrastructure"]: response = client.delete(f"/api/infrastructure/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete billable time for entity_id in created_entities["billable_time"]: response = client.delete(f"/api/billable-time/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete tasks for entity_id in created_entities["tasks"]: response = client.delete(f"/api/tasks/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete work items for entity_id in created_entities["work_items"]: response = client.delete(f"/api/work-items/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete sessions for entity_id in created_entities["sessions"]: response = client.delete(f"/api/sessions/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete projects for entity_id in created_entities["projects"]: response = client.delete(f"/api/projects/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete sites for entity_id in created_entities["sites"]: response = client.delete(f"/api/sites/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 # Delete clients for entity_id in created_entities["clients"]: response = client.delete(f"/api/clients/{entity_id}", headers=headers) if response.status_code == 200: cleanup_count += 1 print(f" Cleaned up {cleanup_count} test entities") log_test(f"Cleanup all test entities ({cleanup_count} deleted)", True) except Exception as e: log_test("Cleanup all test entities", False, str(e)) # ============================================================================ # Run All Tests # ============================================================================ def run_all_tests(): """Run all test functions.""" print("\n" + "="*70) print("CLAUDETOOLS PHASE 5 API ENDPOINT TESTS") print("="*70) # Section 1: Work Items test_create_work_items_dependencies() test_create_work_item() test_list_work_items() test_get_work_item() test_update_work_item() test_get_work_items_by_client() # Section 2: Tasks test_create_task() test_list_tasks() test_get_task() test_update_task() test_get_tasks_filtering() # Section 3: Billable Time test_create_billable_time() test_list_billable_time() test_get_billable_time() test_update_billable_time() test_get_billable_time_by_session() # Section 4: Sites test_create_site() test_list_sites() test_get_site() test_update_site() test_get_sites_by_client() # Section 5: Infrastructure test_create_infrastructure() test_list_infrastructure() test_get_infrastructure() test_update_infrastructure() test_get_infrastructure_by_site() # Section 6: Services test_create_service() test_list_services() test_get_service() test_update_service() test_get_services_by_client() # Section 7: Networks test_create_network() test_list_networks() test_get_network() test_update_network() test_get_networks_by_site() # Section 8: Firewall Rules test_create_firewall_rule() test_list_firewall_rules() test_get_firewall_rule() test_update_firewall_rule() test_get_firewall_rules_by_infrastructure() # Section 9: M365 Tenants test_create_m365_tenant() test_list_m365_tenants() test_get_m365_tenant() test_update_m365_tenant() test_get_m365_tenants_by_client() # Section 10: Credentials (with encryption) test_create_credential_password() test_create_credential_api_key() test_create_credential_oauth() test_list_credentials() test_get_credential() test_update_credential() test_get_credentials_by_client() # Section 11: Credential Audit Logs (read-only) test_list_credential_audit_logs() test_get_credential_audit_logs_by_credential() test_get_credential_audit_logs_by_user() # Section 12: Security Incidents test_create_security_incident() test_list_security_incidents() test_get_security_incident() test_update_security_incident() test_get_security_incidents_by_client() # Section 13: Cleanup test_cleanup_all_entities() if __name__ == "__main__": print("\n>> Starting ClaudeTools Phase 5 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)