Files
headquarter/apps/api/tests/integration/test_config_folders_api.py
T
Fusion dacf105200 test: add comprehensive tests for tool workshop functionality
Backend tests:
- Unit tests for docker_build service (successful/failed builds, context, paths)
- Unit tests for readiness_probe service (success, timeout, retries, edge cases)
- Integration tests for config_folders API (CRUD + project overrides)
- Integration tests for tool_types API with new fields
- Integration tests for tool_configs API with new fields

Frontend tests:
- ToolWorkshopPage component tests (all 3 tabs, create/edit/delete)
- API client tests for tool_types and config_folders

Fixes:
- Add field_validator import to tool_configs.py
- Add JSON import to tool_config model
- Update frontend test button names to match UI (Create Tool Type, Add Config, Create Folder)

Quality gates: backend unit tests passing (23/23)
2026-05-22 19:45:10 +02:00

547 lines
16 KiB
Python

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",
)
)
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()
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",
)
)
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()
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",
)
)
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()
asyncio.run(_run())
@pytest.mark.integration
def test_list_config_folders_requires_authentication() -> None:
_prepare_test_db()
app = _load_app()
client = TestClient(app)
response = client.get("/config-folders")
assert response.status_code == 401
@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))
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
@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))
response = client.get("/config-folders")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
assert data[0]["name"] == "user1-folder"
@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))
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
@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))
payload = {
"name": "existing-folder",
"mount_path": "/home/user",
"files": {},
}
response = client.post("/config-folders", json=payload)
assert response.status_code == 409
@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", {})