|
|
|
@@ -1,4 +1,7 @@
|
|
|
|
|
"""Integration tests for config profiles API."""
|
|
|
|
|
|
|
|
|
|
import uuid
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
|
|
|
@@ -17,7 +20,9 @@ class TestConfigProfilesAPI:
|
|
|
|
|
response = authenticated_client.get("/config-profiles")
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert isinstance(data, list)
|
|
|
|
|
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."""
|
|
|
|
@@ -26,99 +31,48 @@ class TestConfigProfilesAPI:
|
|
|
|
|
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"
|
|
|
|
|
assert data["description"] == "Test profile"
|
|
|
|
|
|
|
|
|
|
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(
|
|
|
|
|
authenticated_client.post(
|
|
|
|
|
"/config-profiles",
|
|
|
|
|
json={
|
|
|
|
|
"name": "duplicate-profile",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "duplicate-profile"},
|
|
|
|
|
)
|
|
|
|
|
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": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "duplicate-profile"},
|
|
|
|
|
)
|
|
|
|
|
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
|
|
|
|
|
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": "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": {}}],
|
|
|
|
|
},
|
|
|
|
|
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 profile first
|
|
|
|
|
create_response = authenticated_client.post(
|
|
|
|
|
"/config-profiles",
|
|
|
|
|
json={
|
|
|
|
|
"name": "get-test",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "get-test"},
|
|
|
|
|
)
|
|
|
|
|
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"
|
|
|
|
|
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."""
|
|
|
|
@@ -127,327 +81,381 @@ class TestConfigProfilesAPI:
|
|
|
|
|
|
|
|
|
|
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": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "update-test"},
|
|
|
|
|
)
|
|
|
|
|
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"},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "updated-name", "description": "updated desc"},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert data["name"] == "updated-name"
|
|
|
|
|
assert data["env_vars"] == {"NEW_VAR": "new_value"}
|
|
|
|
|
assert data["description"] == "updated desc"
|
|
|
|
|
|
|
|
|
|
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": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "delete-test"},
|
|
|
|
|
)
|
|
|
|
|
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(
|
|
|
|
|
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": "base-profile",
|
|
|
|
|
"env_vars": {"BASE_VAR": "base_value"},
|
|
|
|
|
"files": {},
|
|
|
|
|
},
|
|
|
|
|
json={"name": "access-test"},
|
|
|
|
|
)
|
|
|
|
|
base_id = base_response.json()["id"]
|
|
|
|
|
profile_id = create_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]},
|
|
|
|
|
)
|
|
|
|
|
# The profile should be accessible
|
|
|
|
|
response = authenticated_client.get(f"/config-profiles/{profile_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(
|
|
|
|
|
|
|
|
|
|
@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-a",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
a_id = a_response.json()["id"]
|
|
|
|
|
|
|
|
|
|
# Create profile B
|
|
|
|
|
b_response = authenticated_client.post(
|
|
|
|
|
json={"name": "profile-1"},
|
|
|
|
|
).json()
|
|
|
|
|
profile2 = 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
|
|
|
|
|
|
|
|
|
|
def test_create_config_profile_with_git_mounts(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
|
|
|
"""Test creating a config profile with git mounts."""
|
|
|
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
|
json={"name": "profile-2"},
|
|
|
|
|
).json()
|
|
|
|
|
|
|
|
|
|
# Add include
|
|
|
|
|
response = authenticated_client.post(
|
|
|
|
|
"/config-profiles",
|
|
|
|
|
json={
|
|
|
|
|
"name": "git-mount-profile",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
"git_mounts": [
|
|
|
|
|
{
|
|
|
|
|
"remote_url": "https://github.com/user/repo.git",
|
|
|
|
|
"source_path": ".",
|
|
|
|
|
"target_path": "/app",
|
|
|
|
|
"branch": "main",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
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["name"] == "git-mount-profile"
|
|
|
|
|
assert len(data["git_mounts"]) == 1
|
|
|
|
|
assert data["git_mounts"][0]["target_path"] == "/app"
|
|
|
|
|
assert data["git_mounts"][0]["branch"] == "main"
|
|
|
|
|
assert data["included_profile_id"] == profile2["id"]
|
|
|
|
|
assert data["included_profile_name"] == "profile-2"
|
|
|
|
|
|
|
|
|
|
def test_update_config_profile_git_mounts(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
|
|
|
"""Test updating git mounts on a config profile."""
|
|
|
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
|
|
|
|
|
|
# Create profile first
|
|
|
|
|
create_response = authenticated_client.post(
|
|
|
|
|
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": "update-git-mounts",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
profile_id = create_response.json()["id"]
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
# Update with git mounts
|
|
|
|
|
response = authenticated_client.put(
|
|
|
|
|
f"/config-profiles/{profile_id}",
|
|
|
|
|
json={
|
|
|
|
|
"git_mounts": [
|
|
|
|
|
{
|
|
|
|
|
"remote_url": "https://github.com/user/repo.git",
|
|
|
|
|
"source_path": "config",
|
|
|
|
|
"target_path": "/config",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
f"/config-profiles/{p1['id']}/includes/{inc['id']}",
|
|
|
|
|
json={"order_index": 5},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
data = response.json()
|
|
|
|
|
assert len(data["git_mounts"]) == 1
|
|
|
|
|
assert data["git_mounts"][0]["source_path"] == "config"
|
|
|
|
|
assert response.json()["order_index"] == 5
|
|
|
|
|
|
|
|
|
|
def test_create_config_profile_invalid_git_mount_source_path(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
|
|
|
"""Test that invalid git mount source paths are rejected."""
|
|
|
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
|
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": "bad-git-mount",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
"git_mounts": [
|
|
|
|
|
{
|
|
|
|
|
"remote_url": "https://github.com/user/repo.git",
|
|
|
|
|
"source_path": "/absolute/path",
|
|
|
|
|
"target_path": "/app",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
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_create_config_profile_invalid_git_mount_target_path_traversal(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
|
|
|
"""Test that git mount target paths with traversal are rejected."""
|
|
|
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
|
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(
|
|
|
|
|
"/config-profiles",
|
|
|
|
|
json={
|
|
|
|
|
"name": "bad-git-mount-target",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
"git_mounts": [
|
|
|
|
|
{
|
|
|
|
|
"remote_url": "https://github.com/user/repo.git",
|
|
|
|
|
"source_path": ".",
|
|
|
|
|
"target_path": "../../../etc/passwd",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
f"/config-profiles/{profile['id']}/mounts",
|
|
|
|
|
json={"target_path": "/etc/../passwd", "files": {"test.txt": "hello"}},
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 422
|
|
|
|
|
|
|
|
|
|
def test_preview_config_profile_with_git_mounts(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
|
|
|
"""Test previewing a profile with git mounts."""
|
|
|
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
|
|
|
|
|
|
# Create profile with git mounts
|
|
|
|
|
create_response = authenticated_client.post(
|
|
|
|
|
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": "preview-git-mounts",
|
|
|
|
|
"env_vars": {},
|
|
|
|
|
"files": {},
|
|
|
|
|
"git_mounts": [
|
|
|
|
|
{
|
|
|
|
|
"remote_url": "https://github.com/user/repo.git",
|
|
|
|
|
"source_path": ".",
|
|
|
|
|
"target_path": "/app",
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
profile_id = create_response.json()["id"]
|
|
|
|
|
json={"name": "dup-mount-test"},
|
|
|
|
|
).json()
|
|
|
|
|
|
|
|
|
|
# Preview
|
|
|
|
|
response = authenticated_client.get(f"/config-profiles/{profile_id}/preview")
|
|
|
|
|
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 len(data["git_mounts"]) == 1
|
|
|
|
|
assert data["git_mounts"][0]["remote_url"] == "https://github.com/user/repo.git"
|
|
|
|
|
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
|
|
|
|
|