dacf105200
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)
547 lines
16 KiB
Python
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", {})
|