0ec20b9c23
- Add git mount merge function tests - Add profile resolution tests with git mounts - Add integration tests for CRUD with git mounts - Add glob expansion tests (patterns, limits, repo boundary) - Add branch checkout tests (success and failure) - Add error handling tests for missing repos/invalid UUIDs All 52 tests pass.
454 lines
16 KiB
Python
454 lines
16 KiB
Python
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
|
|
|
|
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
|
|
|
|
response = authenticated_client.post(
|
|
"/config-profiles",
|
|
json={
|
|
"name": "git-mount-profile",
|
|
"env_vars": {},
|
|
"files": {},
|
|
"git_mounts": [
|
|
{
|
|
"repo_id": repo_id,
|
|
"source_path": ".",
|
|
"target_path": "/app",
|
|
"branch": "main",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
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"
|
|
|
|
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(
|
|
"/config-profiles",
|
|
json={
|
|
"name": "update-git-mounts",
|
|
"env_vars": {},
|
|
"files": {},
|
|
},
|
|
)
|
|
profile_id = create_response.json()["id"]
|
|
|
|
# Update with git mounts
|
|
response = authenticated_client.put(
|
|
f"/config-profiles/{profile_id}",
|
|
json={
|
|
"git_mounts": [
|
|
{
|
|
"repo_id": repo_id,
|
|
"source_path": "config",
|
|
"target_path": "/config",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["git_mounts"]) == 1
|
|
assert data["git_mounts"][0]["source_path"] == "config"
|
|
|
|
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
|
|
|
|
response = authenticated_client.post(
|
|
"/config-profiles",
|
|
json={
|
|
"name": "bad-git-mount",
|
|
"env_vars": {},
|
|
"files": {},
|
|
"git_mounts": [
|
|
{
|
|
"repo_id": repo_id,
|
|
"source_path": "/absolute/path",
|
|
"target_path": "/app",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
assert response.status_code == 422
|
|
|
|
def test_create_config_profile_invalid_git_mount_target_path(self, authenticated_client: TestClient, test_project_and_repo) -> None:
|
|
"""Test that invalid git mount target paths are rejected."""
|
|
_project_id, repo_id = test_project_and_repo
|
|
|
|
response = authenticated_client.post(
|
|
"/config-profiles",
|
|
json={
|
|
"name": "bad-git-mount-target",
|
|
"env_vars": {},
|
|
"files": {},
|
|
"git_mounts": [
|
|
{
|
|
"repo_id": repo_id,
|
|
"source_path": ".",
|
|
"target_path": "relative/path",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
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(
|
|
"/config-profiles",
|
|
json={
|
|
"name": "preview-git-mounts",
|
|
"env_vars": {},
|
|
"files": {},
|
|
"git_mounts": [
|
|
{
|
|
"repo_id": repo_id,
|
|
"source_path": ".",
|
|
"target_path": "/app",
|
|
}
|
|
],
|
|
},
|
|
)
|
|
profile_id = create_response.json()["id"]
|
|
|
|
# Preview
|
|
response = authenticated_client.get(f"/config-profiles/{profile_id}/preview")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert len(data["git_mounts"]) == 1
|
|
assert data["git_mounts"][0]["repo_id"] == repo_id
|