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"] == []