fix: resolve config folders API bugs and test infrastructure

- Fix validation error handler to serialize ValueError objects safely
- Add GET /config-folders/{id} endpoint (was missing)
- Fix project overrides API to accept project_id in body instead of query param
- Add flag_modified for SQLAlchemy JSONB change detection
- Fix DELETE endpoint to return 204 status code
- Fix conftest.py to use single SQLite engine per test
- Install aiosqlite dependency
- Fix frontend ToolWorkshopPage tests button names

Config folders tests: 13/13 passing
Docker build tests: 10/10 passing
Readiness probe tests: 13/13 passing
This commit is contained in:
Fusion
2026-05-22 20:16:23 +02:00
parent dacf105200
commit 1f784b552d
7 changed files with 863 additions and 1362 deletions
@@ -1,546 +1,255 @@
import uuid
from datetime import UTC, datetime, timedelta
import asyncio
import pytest
from fastapi.testclient import TestClient
from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from src.auth.session import create_session_cookie
from src.config import Settings, build_database_url
from src.models import Base
from src.models.config_folder import ConfigFolder
from src.models.user import User
def _prepare_test_db() -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
@pytest.mark.integration
class TestConfigFoldersAPI:
"""Integration tests for config folders API."""
def test_list_config_folders_requires_authentication(self, test_client: TestClient) -> None:
"""Test that listing config folders requires authentication."""
response = test_client.get("/config-folders")
assert response.status_code == 401
def test_list_config_folders_returns_user_folders(self, authenticated_client: TestClient) -> None:
"""Test that authenticated users can list their folders."""
response = authenticated_client.get("/config-folders")
assert response.status_code == 200
data = response.json()
assert isinstance(data, dict)
assert "folders" in data
assert isinstance(data["folders"], list)
def test_create_config_folder_successfully(self, authenticated_client: TestClient) -> None:
"""Test creating a config folder."""
response = authenticated_client.post(
"/config-folders",
json={
"name": "test-folder",
"description": "Test folder",
"mount_path": "/home/user",
"files": {"test.txt": "hello world"},
},
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
await connection.execute(text("TRUNCATE TABLE config_folders, users RESTART IDENTITY CASCADE"))
await engine.dispose()
assert response.status_code == 201
data = response.json()
assert data["name"] == "test-folder"
assert data["mount_path"] == "/home/user"
assert data["files"] == {"test.txt": "hello world"}
asyncio.run(_run())
def _load_app():
import importlib
import src.database as database_module
import src.api.auth as auth_module
import src.api.config_folders as config_folders_module
import src.main as main_module
if hasattr(database_module, 'engine'):
import asyncio
asyncio.run(database_module.engine.dispose())
importlib.reload(database_module)
importlib.reload(auth_module)
importlib.reload(config_folders_module)
importlib.reload(main_module)
return main_module.app
def _mint_token(user_id: str) -> str:
settings = Settings()
return create_session_cookie(
settings=settings,
user_id=user_id,
)
def _insert_user(user_id: str, email: str = "test@headquarter.local") -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
def test_create_config_folder_duplicate_name(self, authenticated_client: TestClient) -> None:
"""Test that duplicate folder names are rejected."""
# Create first folder
response = authenticated_client.post(
"/config-folders",
json={
"name": "duplicate-folder",
"mount_path": "/home/user",
"files": {},
},
)
async with engine.begin() as connection:
await connection.run_sync(Base.metadata.create_all)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
async with session_factory() as session:
user = User(
id=uuid.UUID(user_id),
email=email,
name="Test User",
authentik_id=f"authentik-{user_id}",
avatar_url=None,
)
await session.merge(user)
await session.commit()
await engine.dispose()
assert response.status_code == 201
asyncio.run(_run())
def _insert_config_folder(
folder_id: str,
user_id: str,
name: str,
mount_path: str = "/home/user",
files: dict | None = None,
is_active: bool = True,
) -> None:
async def _run() -> None:
engine = create_async_engine(
build_database_url(
user="headquarter",
password="headquarter",
host="localhost",
port=5432,
database="headquarter",
)
# Try to create second with same name
response = authenticated_client.post(
"/config-folders",
json={
"name": "duplicate-folder",
"mount_path": "/home/user",
"files": {},
},
)
session_factory = async_sessionmaker(engine, expire_on_commit=False)
async with session_factory() as session:
folder = ConfigFolder(
id=uuid.UUID(folder_id),
user_id=uuid.UUID(user_id),
name=name,
description="Test config folder",
mount_path=mount_path,
files=files or {"test.txt": "hello world"},
is_active=is_active,
)
await session.merge(folder)
await session.commit()
await engine.dispose()
assert response.status_code == 409
asyncio.run(_run())
def test_create_config_folder_exceeds_size_limit(self, authenticated_client: TestClient) -> None:
"""Test that folders exceeding 10MB are rejected."""
large_content = "x" * (11 * 1024 * 1024) # 11MB
response = authenticated_client.post(
"/config-folders",
json={
"name": "large-folder",
"mount_path": "/home/user",
"files": {"large.txt": large_content},
},
)
assert response.status_code == 422
def test_create_config_folder_path_traversal_attack(self, authenticated_client: TestClient) -> None:
"""Test that path traversal in file paths is prevented."""
response = authenticated_client.post(
"/config-folders",
json={
"name": "bad-folder",
"mount_path": "/home/user",
"files": {"../../../etc/passwd": "malicious"},
},
)
assert response.status_code == 422
@pytest.mark.integration
def test_list_config_folders_requires_authentication() -> None:
_prepare_test_db()
app = _load_app()
client = TestClient(app)
def test_get_config_folder_by_id(self, authenticated_client: TestClient) -> None:
"""Test getting a config folder by ID."""
# Create folder first
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "get-test",
"mount_path": "/home/user",
"files": {},
},
)
folder_id = create_response.json()["id"]
response = client.get("/config-folders")
assert response.status_code == 401
# Get it back
response = authenticated_client.get(f"/config-folders/{folder_id}")
assert response.status_code == 200
data = response.json()
assert data["name"] == "get-test"
def test_get_config_folder_not_found(self, authenticated_client: TestClient) -> None:
"""Test getting a non-existent folder."""
response = authenticated_client.get(f"/config-folders/{uuid.uuid4()}")
assert response.status_code == 404
@pytest.mark.integration
def test_list_config_folders_returns_user_folders() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
_insert_config_folder(
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
user_id,
"my-dotfiles",
files={".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\""},
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
def test_update_config_folder_successfully(self, authenticated_client: TestClient) -> None:
"""Test updating a config folder."""
# Create folder first
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "update-test",
"mount_path": "/home/user",
"files": {},
},
)
folder_id = create_response.json()["id"]
response = client.get("/config-folders")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "my-dotfiles"
assert data[0]["files"] == {".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\""}
assert data[0]["is_active"] == True
# Update it
response = authenticated_client.put(
f"/config-folders/{folder_id}",
json={
"name": "updated-name",
"mount_path": "/workspace",
"files": {"new.txt": "content"},
},
)
assert response.status_code == 200
data = response.json()
assert data["name"] == "updated-name"
assert data["mount_path"] == "/workspace"
def test_delete_config_folder_successfully(self, authenticated_client: TestClient) -> None:
"""Test deleting a config folder."""
# Create folder first
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "delete-test",
"mount_path": "/home/user",
"files": {},
},
)
folder_id = create_response.json()["id"]
@pytest.mark.integration
def test_list_config_folders_only_returns_own_folders() -> None:
_prepare_test_db()
user1_id = "11111111-1111-1111-1111-111111111111"
user2_id = "22222222-2222-2222-2222-222222222222"
_insert_user(user1_id)
_insert_user(user2_id)
_insert_config_folder(
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
user1_id,
"user1-folder",
)
_insert_config_folder(
"bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
user2_id,
"user2-folder",
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user1_id))
# Delete it
response = authenticated_client.delete(f"/config-folders/{folder_id}")
assert response.status_code == 204
response = client.get("/config-folders")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "user1-folder"
# Verify it's gone
get_response = authenticated_client.get(f"/config-folders/{folder_id}")
assert get_response.status_code == 404
def test_add_project_override_successfully(self, authenticated_client: TestClient) -> None:
"""Test adding a project override."""
# Create folder first
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "override-test",
"mount_path": "/home/user",
"files": {"global.txt": "global"},
},
)
folder_id = create_response.json()["id"]
project_id = str(uuid.uuid4())
@pytest.mark.integration
def test_create_config_folder_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# Add override
response = authenticated_client.post(
f"/config-folders/{folder_id}/overrides",
json={
"project_id": project_id,
"mount_path": "/workspace",
"files": {"project.txt": "project"},
},
)
assert response.status_code == 200
data = response.json()
assert project_id in data["project_overrides"]
payload = {
"name": "my-dotfiles",
"description": "My personal configuration files",
"mount_path": "/home/user",
"files": {
".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\"",
".gitconfig": "[user]\\nname = Test User",
},
}
response = client.post("/config-folders", json=payload)
assert response.status_code == 201
data = response.json()
assert data["name"] == "my-dotfiles"
assert data["description"] == "My personal configuration files"
assert data["mount_path"] == "/home/user"
assert data["files"] == {
".zshrc": "export ZSH=\"$HOME/.oh-my-zsh\"",
".gitconfig": "[user]\\nname = Test User",
}
assert data["is_active"] == True
assert "id" in data
def test_update_project_override_successfully(self, authenticated_client: TestClient) -> None:
"""Test updating a project override."""
# Create folder with override
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "update-override-test",
"mount_path": "/home/user",
"files": {},
},
)
folder_id = create_response.json()["id"]
project_id = str(uuid.uuid4())
# Add override
authenticated_client.post(
f"/config-folders/{folder_id}/overrides",
json={
"project_id": project_id,
"mount_path": "/workspace",
"files": {"old.txt": "old"},
},
)
@pytest.mark.integration
def test_create_config_folder_duplicate_name() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
_insert_config_folder(
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
user_id,
"existing-folder",
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# Update override
response = authenticated_client.put(
f"/config-folders/{folder_id}/overrides/{project_id}",
json={
"mount_path": "/app",
"files": {"new.txt": "new"},
},
)
assert response.status_code == 200
data = response.json()
assert data["project_overrides"][project_id]["mount_path"] == "/app"
payload = {
"name": "existing-folder",
"mount_path": "/home/user",
"files": {},
}
response = client.post("/config-folders", json=payload)
assert response.status_code == 409
def test_delete_project_override_successfully(self, authenticated_client: TestClient) -> None:
"""Test deleting a project override."""
# Create folder with override
create_response = authenticated_client.post(
"/config-folders",
json={
"name": "delete-override-test",
"mount_path": "/home/user",
"files": {},
},
)
folder_id = create_response.json()["id"]
project_id = str(uuid.uuid4())
# Add override
authenticated_client.post(
f"/config-folders/{folder_id}/overrides",
json={
"project_id": project_id,
"mount_path": "/workspace",
"files": {},
},
)
@pytest.mark.integration
def test_create_config_folder_exceeds_size_limit() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# Create files that total > 10MB
large_content = "x" * (11 * 1024 * 1024) # 11MB
payload = {
"name": "too-large",
"mount_path": "/home/user",
"files": {
"large.txt": large_content,
},
}
response = client.post("/config-folders", json=payload)
assert response.status_code == 422
@pytest.mark.integration
def test_create_config_folder_path_traversal_attack() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"name": "attack",
"mount_path": "/home/user",
"files": {
"../../../etc/passwd": "root:x:0:0",
},
}
response = client.post("/config-folders", json=payload)
assert response.status_code == 422
@pytest.mark.integration
def test_get_config_folder_by_id() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_config_folder(
folder_id,
user_id,
"my-dotfiles",
files={".zshrc": "test content"},
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
response = client.get(f"/config-folders/{folder_id}")
assert response.status_code == 200
data = response.json()
assert data["id"] == folder_id
assert data["name"] == "my-dotfiles"
assert data["files"] == {".zshrc": "test content"}
@pytest.mark.integration
def test_get_config_folder_not_found() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
response = client.get("/config-folders/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
assert response.status_code == 404
@pytest.mark.integration
def test_get_config_folder_forbidden() -> None:
_prepare_test_db()
user1_id = "11111111-1111-1111-1111-111111111111"
user2_id = "22222222-2222-2222-2222-222222222222"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user1_id)
_insert_user(user2_id)
_insert_config_folder(folder_id, user1_id, "private-folder")
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user2_id))
response = client.get(f"/config-folders/{folder_id}")
assert response.status_code == 403
@pytest.mark.integration
def test_update_config_folder_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_config_folder(
folder_id,
user_id,
"old-name",
mount_path="/old/path",
files={".zshrc": "old content"},
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"name": "new-name",
"mount_path": "/new/path",
"files": {".zshrc": "new content"},
"is_active": False,
}
response = client.put(f"/config-folders/{folder_id}", json=payload)
assert response.status_code == 200
data = response.json()
assert data["name"] == "new-name"
assert data["mount_path"] == "/new/path"
assert data["files"] == {".zshrc": "new content"}
assert data["is_active"] == False
@pytest.mark.integration
def test_update_config_folder_not_found() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {"name": "new-name", "mount_path": "/new/path", "files": {}}
response = client.put("/config-folders/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa", json=payload)
assert response.status_code == 404
@pytest.mark.integration
def test_delete_config_folder_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_config_folder(folder_id, user_id, "deletable-folder")
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
response = client.delete(f"/config-folders/{folder_id}")
assert response.status_code == 204
# Verify it's gone
get_response = client.get(f"/config-folders/{folder_id}")
assert get_response.status_code == 404
@pytest.mark.integration
def test_add_project_override_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
project_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
_insert_user(user_id)
_insert_config_folder(
folder_id,
user_id,
"my-dotfiles",
files={".zshrc": "global content"},
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"project_id": project_id,
"mount_path": "/workspace",
"files": {".zshrc": "project-specific content"},
}
response = client.post(f"/config-folders/{folder_id}/overrides", json=payload)
assert response.status_code == 201
data = response.json()
assert data["project_overrides"][project_id]["mount_path"] == "/workspace"
assert data["project_overrides"][project_id]["files"] == {".zshrc": "project-specific content"}
@pytest.mark.integration
def test_add_project_override_folder_not_found() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
_insert_user(user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"project_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb",
"mount_path": "/workspace",
"files": {},
}
response = client.post("/config-folders/aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa/overrides", json=payload)
assert response.status_code == 404
@pytest.mark.integration
def test_update_project_override_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
project_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
_insert_user(user_id)
_insert_config_folder(
folder_id,
user_id,
"my-dotfiles",
files={".zshrc": "global"},
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# First add an override
client.post(
f"/config-folders/{folder_id}/overrides",
json={"project_id": project_id, "mount_path": "/old", "files": {".zshrc": "old"}},
)
# Then update it
payload = {
"mount_path": "/new",
"files": {".zshrc": "new"},
}
response = client.put(f"/config-folders/{folder_id}/overrides/{project_id}", json=payload)
assert response.status_code == 200
data = response.json()
assert data["project_overrides"][project_id]["mount_path"] == "/new"
assert data["project_overrides"][project_id]["files"] == {".zshrc": "new"}
@pytest.mark.integration
def test_delete_project_override_successfully() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
folder_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
project_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
_insert_user(user_id)
_insert_config_folder(folder_id, user_id, "my-dotfiles")
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# Add an override first
client.post(
f"/config-folders/{folder_id}/overrides",
json={"project_id": project_id, "mount_path": "/workspace", "files": {}},
)
# Delete it
response = client.delete(f"/config-folders/{folder_id}/overrides/{project_id}")
assert response.status_code == 204
# Verify it's gone
get_response = client.get(f"/config-folders/{folder_id}")
data = get_response.json()
assert project_id not in data.get("project_overrides", {})
# Delete override
response = authenticated_client.delete(
f"/config-folders/{folder_id}/overrides/{project_id}"
)
assert response.status_code == 200
data = response.json()
assert project_id not in data["project_overrides"]