Files
headquarter/apps/api/tests/integration/test_tool_configs_api_extended.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

382 lines
12 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.tool_config import ToolConfig
from src.models.tool_type import ToolType
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 tool_configs, tool_types, 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.tool_configs as tool_configs_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(tool_configs_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_tool_type(
tool_type_id: str,
name: str,
display_name: str,
created_by_id: str | None = None,
) -> 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:
tool_type = ToolType(
id=uuid.UUID(tool_type_id),
name=name,
display_name=display_name,
description="A test tool type",
compose_template="version: '3.8'\\nservices:\\n app:\\n image: test",
required_variables=["REPO_PATH"],
is_builtin=False,
created_by_id=uuid.UUID(created_by_id) if created_by_id else None,
)
await session.merge(tool_type)
await session.commit()
await engine.dispose()
asyncio.run(_run())
def _insert_tool_config(
config_id: str,
user_id: str,
tool_type_id: str,
key: str,
value: str,
config_type: str = "env",
**kwargs,
) -> 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:
config = ToolConfig(
id=uuid.UUID(config_id),
user_id=uuid.UUID(user_id),
tool_type_id=uuid.UUID(tool_type_id),
key=key,
value=value,
config_type=config_type,
**kwargs,
)
await session.merge(config)
await session.commit()
await engine.dispose()
asyncio.run(_run())
@pytest.mark.integration
def test_create_tool_config_with_new_fields() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"tool_type_id": tool_type_id,
"key": "advanced-config",
"value": "test-value",
"config_type": "env",
"port_override": 9090,
"start_command": "python app.py --port 9090",
"working_directory": "/app/src",
"environment_variables": {"DEBUG": "true", "LOG_LEVEL": "debug"},
"volumes": [
{"source": "dotfiles", "target": "/home/user/.config", "type": "config_folder"},
],
}
response = client.post("/tool-configs", json=payload)
assert response.status_code == 201
data = response.json()
assert data["key"] == "advanced-config"
assert data["port_override"] == 9090
assert data["start_command"] == "python app.py --port 9090"
assert data["working_directory"] == "/app/src"
assert data["environment_variables"] == {"DEBUG": "true", "LOG_LEVEL": "debug"}
assert len(data["volumes"]) == 1
assert data["volumes"][0]["source"] == "dotfiles"
@pytest.mark.integration
def test_create_tool_config_invalid_port() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"tool_type_id": tool_type_id,
"key": "bad-config",
"value": "test",
"port_override": 99999, # Invalid port
}
response = client.post("/tool-configs", json=payload)
assert response.status_code == 422
@pytest.mark.integration
def test_create_tool_config_invalid_volume_structure() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"tool_type_id": tool_type_id,
"key": "bad-config",
"value": "test",
"volumes": [
{"invalid_key": "value"}, # Missing required fields
],
}
response = client.post("/tool-configs", json=payload)
assert response.status_code == 422
@pytest.mark.integration
def test_update_tool_config_with_new_fields() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
config_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
_insert_tool_config(config_id, user_id, tool_type_id, "my-config", "old-value")
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
payload = {
"value": "new-value",
"port_override": 8080,
"start_command": "npm start",
"working_directory": "/app",
"environment_variables": {"NODE_ENV": "production"},
"volumes": [
{"source": "config", "target": "/app/config", "type": "bind"},
],
}
response = client.put(f"/tool-configs/{config_id}", json=payload)
assert response.status_code == 200
data = response.json()
assert data["value"] == "new-value"
assert data["port_override"] == 8080
assert data["start_command"] == "npm start"
assert data["working_directory"] == "/app"
assert data["environment_variables"] == {"NODE_ENV": "production"}
assert len(data["volumes"]) == 1
@pytest.mark.integration
def test_list_tool_configs_returns_new_fields() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
config_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
_insert_tool_config(
config_id,
user_id,
tool_type_id,
"advanced-config",
"test-value",
port_override=9090,
start_command="python app.py",
working_directory="/app",
environment_variables={"DEBUG": "true"},
volumes=[{"source": "data", "target": "/data", "type": "bind"}],
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
response = client.get("/tool-configs")
assert response.status_code == 200
data = response.json()
assert len(data) == 1
config = data[0]
assert config["port_override"] == 9090
assert config["start_command"] == "python app.py"
assert config["working_directory"] == "/app"
assert config["environment_variables"] == {"DEBUG": "true"}
assert len(config["volumes"]) == 1
@pytest.mark.integration
def test_get_tool_config_defaults() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_tool_type(
tool_type_id,
"test-tool",
"Test Tool",
created_by_id=user_id,
)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
response = client.get(f"/tool-configs/defaults/{tool_type_id}")
assert response.status_code == 200
data = response.json()
assert "tool_type_id" in data
assert data["tool_type_id"] == tool_type_id
assert "suggested_configs" in data
assert "port_override" in data
@pytest.mark.integration
def test_tool_config_backward_compatibility() -> None:
_prepare_test_db()
user_id = "11111111-1111-1111-1111-111111111111"
tool_type_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
_insert_user(user_id)
_insert_tool_type(tool_type_id, "test-tool", "Test Tool", created_by_id=user_id)
app = _load_app()
client = TestClient(app)
client.cookies.set("session", _mint_token(user_id))
# Create config without new fields (old API usage)
payload = {
"tool_type_id": tool_type_id,
"key": "simple-config",
"value": "simple-value",
"config_type": "env",
}
response = client.post("/tool-configs", json=payload)
assert response.status_code == 201
data = response.json()
assert data["key"] == "simple-config"
# New fields should have default values
assert data["port_override"] is None
assert data["start_command"] is None
assert data["working_directory"] is None
assert data["environment_variables"] == {}
assert data["volumes"] == []