import uuid import pytest from fastapi.testclient import TestClient @pytest.mark.integration class TestConfigProfilesAPI: """Integration tests for config profiles API.""" def test_list_config_profiles_requires_authentication(self, test_client: TestClient) -> None: """Test that listing config profiles requires authentication.""" response = test_client.get("/config-profiles") assert response.status_code == 401 def test_list_config_profiles_returns_user_profiles(self, authenticated_client: TestClient) -> None: """Test that authenticated users can list their profiles.""" response = authenticated_client.get("/config-profiles") assert response.status_code == 200 data = response.json() assert isinstance(data, list) def test_create_config_profile_successfully(self, authenticated_client: TestClient) -> None: """Test creating a config profile.""" response = authenticated_client.post( "/config-profiles", json={ "name": "test-profile", "description": "Test profile", "env_vars": {"VAR": "value"}, "runtime_hints": {"start_command": "npm start"}, "mounts": [{"target": "/app", "mode": "rw", "files": {}}], "files": {"test.txt": "hello"}, }, ) assert response.status_code == 201 data = response.json() assert data["name"] == "test-profile" assert data["env_vars"] == {"VAR": "value"} assert data["files"] == {"test.txt": "hello"} assert data["mounts"][0]["target"] == "/app" def test_create_config_profile_duplicate_name(self, authenticated_client: TestClient) -> None: """Test that duplicate profile names are rejected.""" # Create first profile response = authenticated_client.post( "/config-profiles", json={ "name": "duplicate-profile", "env_vars": {}, "files": {}, }, ) assert response.status_code == 201 # Try to create second with same name response = authenticated_client.post( "/config-profiles", json={ "name": "duplicate-profile", "env_vars": {}, "files": {}, }, ) assert response.status_code == 409 def test_create_config_profile_exceeds_size_limit(self, authenticated_client: TestClient) -> None: """Test that profiles exceeding 10MB are rejected.""" large_content = "x" * (11 * 1024 * 1024) # 11MB response = authenticated_client.post( "/config-profiles", json={ "name": "large-profile", "env_vars": {}, "files": {"large.txt": large_content}, }, ) assert response.status_code == 413 def test_create_config_profile_invalid_file_path(self, authenticated_client: TestClient) -> None: """Test that invalid file paths are rejected.""" response = authenticated_client.post( "/config-profiles", json={ "name": "bad-profile", "env_vars": {}, "files": {"../../../etc/passwd": "malicious"}, }, ) assert response.status_code == 422 def test_create_config_profile_invalid_mount_target(self, authenticated_client: TestClient) -> None: """Test that invalid mount targets are rejected.""" response = authenticated_client.post( "/config-profiles", json={ "name": "bad-mount-profile", "env_vars": {}, "files": {}, "mounts": [{"target": "relative/path", "mode": "rw", "files": {}}], }, ) assert response.status_code == 422 def test_get_config_profile_by_id(self, authenticated_client: TestClient) -> None: """Test getting a config profile by ID.""" # Create profile first create_response = authenticated_client.post( "/config-profiles", json={ "name": "get-test", "env_vars": {}, "files": {}, }, ) profile_id = create_response.json()["id"] # Get it back response = authenticated_client.get(f"/config-profiles/{profile_id}") assert response.status_code == 200 data = response.json() assert data["name"] == "get-test" def test_get_config_profile_not_found(self, authenticated_client: TestClient) -> None: """Test getting a non-existent profile.""" response = authenticated_client.get(f"/config-profiles/{uuid.uuid4()}") assert response.status_code == 404 def test_update_config_profile_successfully(self, authenticated_client: TestClient) -> None: """Test updating a config profile.""" # Create profile first create_response = authenticated_client.post( "/config-profiles", json={ "name": "update-test", "env_vars": {}, "files": {}, }, ) profile_id = create_response.json()["id"] # Update it response = authenticated_client.put( f"/config-profiles/{profile_id}", json={ "name": "updated-name", "env_vars": {"NEW_VAR": "new_value"}, }, ) assert response.status_code == 200 data = response.json() assert data["name"] == "updated-name" assert data["env_vars"] == {"NEW_VAR": "new_value"} def test_delete_config_profile_successfully(self, authenticated_client: TestClient) -> None: """Test deleting a config profile.""" # Create profile first create_response = authenticated_client.post( "/config-profiles", json={ "name": "delete-test", "env_vars": {}, "files": {}, }, ) profile_id = create_response.json()["id"] # Delete it response = authenticated_client.delete(f"/config-profiles/{profile_id}") assert response.status_code == 204 # Verify it's gone get_response = authenticated_client.get(f"/config-profiles/{profile_id}") assert get_response.status_code == 404 def test_update_profile_includes_successfully(self, authenticated_client: TestClient) -> None: """Test updating profile includes.""" # Create base profile base_response = authenticated_client.post( "/config-profiles", json={ "name": "base-profile", "env_vars": {"BASE_VAR": "base_value"}, "files": {}, }, ) base_id = base_response.json()["id"] # Create child profile child_response = authenticated_client.post( "/config-profiles", json={ "name": "child-profile", "env_vars": {}, "files": {}, }, ) child_id = child_response.json()["id"] # Update includes response = authenticated_client.put( f"/config-profiles/{child_id}/includes", json={"includes": [base_id]}, ) assert response.status_code == 200 data = response.json() print(f"Response data: {data}") print(f"Includes: {data.get('includes', 'NO INCLUDES KEY')}") assert len(data["includes"]) == 1, f"Expected 1 include, got {len(data.get('includes', []))}: {data.get('includes', [])}" assert data["includes"][0]["included_profile_id"] == base_id def test_update_profile_includes_cycle_detection(self, authenticated_client: TestClient) -> None: """Test that include cycles are detected.""" # Create profile A a_response = authenticated_client.post( "/config-profiles", json={ "name": "profile-a", "env_vars": {}, "files": {}, }, ) a_id = a_response.json()["id"] # Create profile B b_response = authenticated_client.post( "/config-profiles", json={ "name": "profile-b", "env_vars": {}, "files": {}, }, ) b_id = b_response.json()["id"] # Make B include A authenticated_client.put( f"/config-profiles/{b_id}/includes", json={"includes": [a_id]}, ) # Try to make A include B (would create cycle) response = authenticated_client.put( f"/config-profiles/{a_id}/includes", json={"includes": [b_id]}, ) assert response.status_code == 400 def test_preview_config_profile_successfully(self, authenticated_client: TestClient) -> None: """Test previewing a resolved config profile.""" # Create base profile base_response = authenticated_client.post( "/config-profiles", json={ "name": "preview-base", "env_vars": {"BASE_VAR": "base"}, "files": {}, }, ) base_id = base_response.json()["id"] # Create child profile child_response = authenticated_client.post( "/config-profiles", json={ "name": "preview-child", "env_vars": {"CHILD_VAR": "child"}, "files": {}, }, ) child_id = child_response.json()["id"] # Make child include base authenticated_client.put( f"/config-profiles/{child_id}/includes", json={"includes": [base_id]}, ) # Preview child response = authenticated_client.get(f"/config-profiles/{child_id}/preview") assert response.status_code == 200 data = response.json() assert data["profile_name"] == "preview-child" assert data["env_vars"]["BASE_VAR"] == "base" assert data["env_vars"]["CHILD_VAR"] == "child" assert len(data["included_profiles"]) == 1 def test_resolve_default_profile(self, authenticated_client: TestClient) -> None: """Test resolving default profile for project/tool.""" # Create a global default profile (no project/tool scoping) authenticated_client.post( "/config-profiles", json={ "name": "default-profile", "env_vars": {}, "files": {}, "is_default": True, }, ) # Resolve default with random project/tool (should fall back to global) project_id = str(uuid.uuid4()) tool_type_id = str(uuid.uuid4()) response = authenticated_client.get( "/config-profiles/defaults/resolve", params={"project_id": project_id, "tool_type_id": tool_type_id}, ) assert response.status_code == 200 data = response.json() assert data["profile_name"] == "default-profile" def test_resolve_default_profile_no_match(self, authenticated_client: TestClient) -> None: """Test resolving default profile when no profiles exist.""" project_id = str(uuid.uuid4()) tool_type_id = str(uuid.uuid4()) response = authenticated_client.get( "/config-profiles/defaults/resolve", params={"project_id": project_id, "tool_type_id": tool_type_id}, ) assert response.status_code == 200 data = response.json() assert data["profile_id"] is None