"""Integration tests for config profiles API.""" 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, dict) assert "profiles" in data assert isinstance(data["profiles"], 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", }, ) assert response.status_code == 201 data = response.json() assert data["name"] == "test-profile" assert data["description"] == "Test profile" def test_create_config_profile_duplicate_name(self, authenticated_client: TestClient) -> None: """Test that duplicate profile names are rejected.""" authenticated_client.post( "/config-profiles", json={"name": "duplicate-profile"}, ) response = authenticated_client.post( "/config-profiles", json={"name": "duplicate-profile"}, ) assert response.status_code == 409 def test_create_config_profile_empty_name(self, authenticated_client: TestClient) -> None: """Test that empty profile names are rejected.""" response = authenticated_client.post( "/config-profiles", json={"name": " "}, ) 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_response = authenticated_client.post( "/config-profiles", json={"name": "get-test"}, ) profile_id = create_response.json()["id"] response = authenticated_client.get(f"/config-profiles/{profile_id}") assert response.status_code == 200 data = response.json() assert data["name"] == "get-test" assert "includes" in data assert "mounts" in data 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_response = authenticated_client.post( "/config-profiles", json={"name": "update-test"}, ) profile_id = create_response.json()["id"] response = authenticated_client.put( f"/config-profiles/{profile_id}", json={"name": "updated-name", "description": "updated desc"}, ) assert response.status_code == 200 data = response.json() assert data["name"] == "updated-name" assert data["description"] == "updated desc" def test_delete_config_profile_successfully(self, authenticated_client: TestClient) -> None: """Test deleting a config profile.""" create_response = authenticated_client.post( "/config-profiles", json={"name": "delete-test"}, ) profile_id = create_response.json()["id"] response = authenticated_client.delete(f"/config-profiles/{profile_id}") assert response.status_code == 204 get_response = authenticated_client.get(f"/config-profiles/{profile_id}") assert get_response.status_code == 404 def test_profile_access_check(self, authenticated_client: TestClient) -> None: """Test that users can only access their own profiles.""" # Create a profile create_response = authenticated_client.post( "/config-profiles", json={"name": "access-test"}, ) profile_id = create_response.json()["id"] # The profile should be accessible response = authenticated_client.get(f"/config-profiles/{profile_id}") assert response.status_code == 200 @pytest.mark.integration class TestConfigProfileIncludes: """Integration tests for config profile includes.""" def test_add_include_successfully(self, authenticated_client: TestClient) -> None: """Test adding an include to a profile.""" # Create two profiles profile1 = authenticated_client.post( "/config-profiles", json={"name": "profile-1"}, ).json() profile2 = authenticated_client.post( "/config-profiles", json={"name": "profile-2"}, ).json() # Add include response = authenticated_client.post( f"/config-profiles/{profile1['id']}/includes", json={"included_profile_id": profile2["id"], "order_index": 0}, ) assert response.status_code == 201 data = response.json() assert data["included_profile_id"] == profile2["id"] assert data["included_profile_name"] == "profile-2" def test_add_self_include_rejected(self, authenticated_client: TestClient) -> None: """Test that self-includes are rejected.""" profile = authenticated_client.post( "/config-profiles", json={"name": "self-include-test"}, ).json() response = authenticated_client.post( f"/config-profiles/{profile['id']}/includes", json={"included_profile_id": profile["id"], "order_index": 0}, ) assert response.status_code == 400 def test_add_include_cycle_rejected(self, authenticated_client: TestClient) -> None: """Test that circular includes are rejected.""" profile1 = authenticated_client.post( "/config-profiles", json={"name": "cycle-1"}, ).json() profile2 = authenticated_client.post( "/config-profiles", json={"name": "cycle-2"}, ).json() # Add profile1 includes profile2 authenticated_client.post( f"/config-profiles/{profile1['id']}/includes", json={"included_profile_id": profile2["id"], "order_index": 0}, ) # Try to add profile2 includes profile1 (creates cycle) response = authenticated_client.post( f"/config-profiles/{profile2['id']}/includes", json={"included_profile_id": profile1["id"], "order_index": 0}, ) assert response.status_code == 400 def test_add_deep_cycle_rejected(self, authenticated_client: TestClient) -> None: """Test that deep circular includes are rejected.""" p1 = authenticated_client.post( "/config-profiles", json={"name": "deep-1"} ).json() p2 = authenticated_client.post( "/config-profiles", json={"name": "deep-2"} ).json() p3 = authenticated_client.post( "/config-profiles", json={"name": "deep-3"} ).json() # p1 -> p2 -> p3 authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 0}, ) authenticated_client.post( f"/config-profiles/{p2['id']}/includes", json={"included_profile_id": p3["id"], "order_index": 0}, ) # Try p3 -> p1 (creates cycle) response = authenticated_client.post( f"/config-profiles/{p3['id']}/includes", json={"included_profile_id": p1["id"], "order_index": 0}, ) assert response.status_code == 400 def test_add_duplicate_include_rejected(self, authenticated_client: TestClient) -> None: """Test that duplicate includes are rejected.""" p1 = authenticated_client.post( "/config-profiles", json={"name": "dup-1"} ).json() p2 = authenticated_client.post( "/config-profiles", json={"name": "dup-2"} ).json() authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 0}, ) response = authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 1}, ) assert response.status_code == 409 def test_list_includes(self, authenticated_client: TestClient) -> None: """Test listing includes for a profile.""" p1 = authenticated_client.post( "/config-profiles", json={"name": "list-inc-1"} ).json() p2 = authenticated_client.post( "/config-profiles", json={"name": "list-inc-2"} ).json() authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 0}, ) response = authenticated_client.get(f"/config-profiles/{p1['id']}/includes") assert response.status_code == 200 data = response.json() assert len(data["includes"]) == 1 def test_update_include_order(self, authenticated_client: TestClient) -> None: """Test updating include order index.""" p1 = authenticated_client.post( "/config-profiles", json={"name": "order-1"} ).json() p2 = authenticated_client.post( "/config-profiles", json={"name": "order-2"} ).json() inc = authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 0}, ).json() response = authenticated_client.put( f"/config-profiles/{p1['id']}/includes/{inc['id']}", json={"order_index": 5}, ) assert response.status_code == 200 assert response.json()["order_index"] == 5 def test_remove_include(self, authenticated_client: TestClient) -> None: """Test removing an include.""" p1 = authenticated_client.post( "/config-profiles", json={"name": "rem-1"} ).json() p2 = authenticated_client.post( "/config-profiles", json={"name": "rem-2"} ).json() inc = authenticated_client.post( f"/config-profiles/{p1['id']}/includes", json={"included_profile_id": p2["id"], "order_index": 0}, ).json() response = authenticated_client.delete( f"/config-profiles/{p1['id']}/includes/{inc['id']}" ) assert response.status_code == 204 @pytest.mark.integration class TestConfigProfileMounts: """Integration tests for config profile mounts.""" def test_add_mount_successfully(self, authenticated_client: TestClient) -> None: """Test adding a mount to a profile.""" profile = authenticated_client.post( "/config-profiles", json={"name": "mount-test"}, ).json() response = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/etc/config", "files": {"test.txt": "hello"}, "order_index": 0}, ) assert response.status_code == 201 data = response.json() assert data["target_path"] == "/etc/config" assert data["files"] == {"test.txt": "hello"} def test_add_mount_relative_path_rejected(self, authenticated_client: TestClient) -> None: """Test that relative mount paths are rejected.""" profile = authenticated_client.post( "/config-profiles", json={"name": "rel-path-test"}, ).json() response = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "etc/config", "files": {"test.txt": "hello"}}, ) assert response.status_code == 422 def test_add_target_path_traversal_rejected(self, authenticated_client: TestClient) -> None: """Test that path traversal in mount paths is rejected.""" profile = authenticated_client.post( "/config-profiles", json={"name": "traversal-test"}, ).json() response = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/etc/../passwd", "files": {"test.txt": "hello"}}, ) assert response.status_code == 422 def test_add_duplicate_mount_rejected(self, authenticated_client: TestClient) -> None: """Test that duplicate mount paths are rejected.""" profile = authenticated_client.post( "/config-profiles", json={"name": "dup-mount-test"}, ).json() authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/etc/config", "files": {"test.txt": "hello"}}, ) response = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/etc/config", "files": {"test.txt": "world"}}, ) assert response.status_code == 409 def test_update_mount(self, authenticated_client: TestClient) -> None: """Test updating a mount.""" profile = authenticated_client.post( "/config-profiles", json={"name": "update-mount-test"}, ).json() mount = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/old/path", "files": {"test.txt": "old"}}, ).json() response = authenticated_client.put( f"/config-profiles/{profile['id']}/mounts/{mount['id']}", json={"target_path": "/new/path", "files": {"test.txt": "new"}, "order_index": 2}, ) assert response.status_code == 200 data = response.json() assert data["target_path"] == "/new/path" assert data["files"] == {"test.txt": "new"} assert data["order_index"] == 2 def test_remove_mount(self, authenticated_client: TestClient) -> None: """Test removing a mount.""" profile = authenticated_client.post( "/config-profiles", json={"name": "rem-mount-test"}, ).json() mount = authenticated_client.post( f"/config-profiles/{profile['id']}/mounts", json={"target_path": "/tmp/test", "files": {"test.txt": "x"}}, ).json() response = authenticated_client.delete( f"/config-profiles/{profile['id']}/mounts/{mount['id']}" ) assert response.status_code == 204 @pytest.mark.integration class TestConfigProfileDefaults: """Integration tests for default profile APIs.""" def test_get_default_profiles_empty(self, authenticated_client: TestClient) -> None: """Test getting default profiles when none are set.""" response = authenticated_client.get("/config-profiles/defaults") assert response.status_code == 200 data = response.json() assert data["default_profiles"] == {} def test_set_default_profiles(self, authenticated_client: TestClient) -> None: """Test setting default profiles.""" profile = authenticated_client.post( "/config-profiles", json={"name": "default-test"}, ).json() response = authenticated_client.put( "/config-profiles/defaults", json={"default_profiles": {"code-server": profile["id"]}}, ) assert response.status_code == 200 data = response.json() assert data["default_profiles"]["code-server"] == profile["id"] def test_set_default_profiles_invalid_profile(self, authenticated_client: TestClient) -> None: """Test setting default profiles with invalid profile ID.""" response = authenticated_client.put( "/config-profiles/defaults", json={"default_profiles": {"code-server": str(uuid.uuid4())}}, ) assert response.status_code == 404 def test_get_default_profile_for_tool_type(self, authenticated_client: TestClient) -> None: """Test getting default profile for a specific tool type.""" profile = authenticated_client.post( "/config-profiles", json={"name": "tool-default-test"}, ).json() authenticated_client.put( "/config-profiles/defaults", json={"default_profiles": {"jupyter-notebook": profile["id"]}}, ) response = authenticated_client.get("/config-profiles/defaults/jupyter-notebook") assert response.status_code == 200 data = response.json() assert data["tool_type_id"] == "jupyter-notebook" assert data["profile_id"] == profile["id"] def test_get_default_profile_for_tool_type_not_set(self, authenticated_client: TestClient) -> None: """Test getting default profile when not set.""" response = authenticated_client.get("/config-profiles/defaults/opencode") assert response.status_code == 200 data = response.json() assert data["tool_type_id"] == "opencode" assert data["profile_id"] is None