fix: resolve stale backend test imports and schema drift
- Delete 4 obsolete unit tests tied to removed git mount/clone models - Update imports and assertions across unit/integration/service tests - Fix Settings defaults (postgres host, JWT props, cookie_samesite) - Add skip guards for PostgreSQL-dependent integration tests - Fix GitService env assertions and HealthMonitor state-change tests - Repair docker/container inspect assertions in test_docker_service - Fix ToolTypeCreate default_port validator ordering bug - Fix check_port_exposed substring false-positive for port 0 - Update test_tool_types_api_extended to use interface_type field Quality gates: pytest 311 passed, 34 skipped; npm typecheck/lint/test 87 passed
This commit is contained in:
@@ -10,7 +10,30 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from src.auth.session import create_session_cookie
|
||||
from src.config import Settings, build_database_url
|
||||
from src.models import Base
|
||||
from src.models.user import User
|
||||
from src.models.user.user import User
|
||||
|
||||
|
||||
def _postgres_available() -> bool:
|
||||
"""Check whether a PostgreSQL server is reachable for integration tests."""
|
||||
import asyncpg
|
||||
|
||||
async def _check() -> bool:
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host="localhost", port=5432, user="headquarter", password="headquarter", database="headquarter"
|
||||
)
|
||||
await conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return asyncio.run(_check())
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _postgres_available(),
|
||||
reason="PostgreSQL not available on localhost:5432",
|
||||
)
|
||||
|
||||
|
||||
def _prepare_auth_test_db() -> None:
|
||||
@@ -34,7 +57,7 @@ def _prepare_auth_test_db() -> None:
|
||||
|
||||
def _load_app():
|
||||
import src.database as database_module
|
||||
import src.api.auth as auth_module
|
||||
import src.api.user.auth as auth_module
|
||||
import src.main as main_module
|
||||
|
||||
importlib.reload(database_module)
|
||||
|
||||
@@ -10,15 +10,15 @@ from fastapi.testclient import TestClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.api import events as events_module
|
||||
from src.api.system import events as events_module
|
||||
from src.auth.session import decode_session_cookie
|
||||
from src.config import Settings
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.instance_event import InstanceEvent
|
||||
from src.models.project import Project
|
||||
from src.models.tool_instance import ToolInstance
|
||||
from src.models.tool_type import ToolType
|
||||
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
|
||||
from src.models.project.git_repository import GitRepository
|
||||
from src.models.system.instance_event import InstanceEvent
|
||||
from src.models.project.project import Project
|
||||
from src.models.tool.tool_instance import ToolInstance
|
||||
from src.models.tool.tool_type import ToolType
|
||||
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -75,7 +75,7 @@ def test_sse_enforces_connection_limit(authenticated_client: TestClient) -> None
|
||||
@pytest.mark.integration
|
||||
def test_sse_event_generator_format() -> None:
|
||||
"""Test the SSE endpoint is registered."""
|
||||
from src.api.events import router
|
||||
from src.api.system.events import router
|
||||
|
||||
route_paths = [getattr(r, "path", "") for r in router.routes]
|
||||
assert any("/stream" in str(p) for p in route_paths)
|
||||
@@ -142,7 +142,7 @@ async def test_lifecycle_hook_publishes_event_and_persists(
|
||||
|
||||
event_bus.subscribe("instance.created", subscriber)
|
||||
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
@@ -220,7 +220,7 @@ async def test_lifecycle_event_persists_audit_row(
|
||||
db_session.add(instance)
|
||||
await db_session.commit()
|
||||
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
|
||||
@@ -3,11 +3,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models import Base
|
||||
from src.models.base import TimestampMixin, UUIDPrimaryKeyMixin
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.project import Project
|
||||
from src.models.ssh_key import SSHKey
|
||||
from src.models.user import User
|
||||
from src.models.user_config import UserConfig
|
||||
from src.models.project.git_repository import GitRepository
|
||||
from src.models.project.project import Project
|
||||
from src.models.user.ssh_key import SSHKey
|
||||
from src.models.user.user import User
|
||||
from src.models.user.user_config import UserConfig
|
||||
|
||||
|
||||
@pytest.mark.integration
|
||||
@@ -39,6 +39,7 @@ def test_expected_tables_are_registered() -> None:
|
||||
"tool_types",
|
||||
"user_configs",
|
||||
"users",
|
||||
"workspaces",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models.user import User
|
||||
from src.models.user_config import UserConfig
|
||||
from src.services.notification_service import NotificationService
|
||||
from src.models.user.user import User
|
||||
from src.models.user.user_config import UserConfig
|
||||
from src.services.shared.notification_service import NotificationService
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
||||
@@ -9,14 +9,14 @@ import pytest_asyncio
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from src.models.git_repository import GitRepository
|
||||
from src.models.notification import Notification
|
||||
from src.models.project import Project
|
||||
from src.models.tool_instance import ToolInstance
|
||||
from src.models.tool_type import ToolType
|
||||
from src.models.user import User
|
||||
from src.services.event_bus import InstanceEventBus, InstanceEventPayload
|
||||
from src.services.health_monitor import HealthSnapshot
|
||||
from src.models.project.git_repository import GitRepository
|
||||
from src.models.system.notification import Notification
|
||||
from src.models.project.project import Project
|
||||
from src.models.tool.tool_instance import ToolInstance
|
||||
from src.models.tool.tool_type import ToolType
|
||||
from src.models.user.user import User
|
||||
from src.services.instance.event_bus import InstanceEventBus, InstanceEventPayload
|
||||
from src.services.instance.health_monitor import HealthSnapshot
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -78,6 +78,7 @@ async def test_instance(db_session: AsyncSession) -> ToolInstance:
|
||||
project_id=project.id,
|
||||
owner_id=user.id,
|
||||
status="running",
|
||||
container_id="container123",
|
||||
compose_path="/tmp/test-compose.yml",
|
||||
port=8080,
|
||||
)
|
||||
@@ -101,7 +102,7 @@ async def test_lifecycle_started_intermediate_skips_notification(
|
||||
|
||||
event_bus.subscribe("instance.started", subscriber)
|
||||
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
@@ -131,7 +132,7 @@ async def test_lifecycle_running_creates_notification(
|
||||
test_instance: ToolInstance,
|
||||
) -> None:
|
||||
"""Successful terminal state (running) creates a notification."""
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
@@ -162,10 +163,17 @@ async def test_health_monitor_error_creates_notification(
|
||||
event_bus: InstanceEventBus,
|
||||
test_instance: ToolInstance,
|
||||
) -> None:
|
||||
"""Simulating a health monitor crash creates an error notification."""
|
||||
from src.services.health_monitor import HealthMonitor
|
||||
"""Simulating a container exit creates an error notification."""
|
||||
from src.services.instance.health_monitor import HealthMonitor
|
||||
|
||||
monitor = HealthMonitor(event_bus)
|
||||
# Seed a different healthy prior state so the exit is treated as a change.
|
||||
monitor._last_known_state[test_instance.id] = HealthSnapshot(
|
||||
container_status="running",
|
||||
container_healthy=None,
|
||||
tunnel_healthy=True,
|
||||
exit_code=None,
|
||||
)
|
||||
|
||||
received: list[InstanceEventPayload] = []
|
||||
|
||||
@@ -175,7 +183,7 @@ async def test_health_monitor_error_creates_notification(
|
||||
event_bus.subscribe("instance.error", subscriber)
|
||||
|
||||
with patch(
|
||||
"src.services.health_monitor.get_container_status",
|
||||
"src.services.instance.health_monitor.get_container_status",
|
||||
return_value={"status": "exited", "exit_code": 137, "health": None},
|
||||
):
|
||||
await monitor._check_instance(db_session, test_instance)
|
||||
@@ -211,10 +219,10 @@ async def test_notification_failure_does_not_block_event_pipeline(
|
||||
|
||||
event_bus.subscribe("instance.started", subscriber)
|
||||
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
with patch(
|
||||
"src.services.lifecycle_hooks.notification_service.create_notification",
|
||||
"src.services.instance.lifecycle_hooks.notification_service.create_notification",
|
||||
side_effect=RuntimeError("DB is down"),
|
||||
):
|
||||
# Should not raise
|
||||
@@ -309,7 +317,7 @@ async def test_notification_ownership_matches_instance_owner(
|
||||
db_session.add(instance)
|
||||
await db_session.commit()
|
||||
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
@@ -336,7 +344,7 @@ async def test_lifecycle_error_creates_error_notification(
|
||||
test_instance: ToolInstance,
|
||||
) -> None:
|
||||
"""An instance.error lifecycle event creates a severity=error notification."""
|
||||
from src.services.lifecycle_hooks import publish_lifecycle_event
|
||||
from src.services.instance.lifecycle_hooks import publish_lifecycle_event
|
||||
|
||||
await publish_lifecycle_event(
|
||||
event_bus=event_bus,
|
||||
@@ -363,7 +371,7 @@ async def test_health_monitor_unhealthy_creates_warning_notification(
|
||||
test_instance: ToolInstance,
|
||||
) -> None:
|
||||
"""Health monitor marking instance unhealthy creates severity=warning notification."""
|
||||
from src.services.health_monitor import HealthMonitor
|
||||
from src.services.instance.health_monitor import HealthMonitor
|
||||
|
||||
monitor = HealthMonitor(event_bus)
|
||||
monitor._last_known_state[test_instance.id] = HealthSnapshot(
|
||||
@@ -376,11 +384,11 @@ async def test_health_monitor_unhealthy_creates_warning_notification(
|
||||
|
||||
with (
|
||||
patch(
|
||||
"src.services.health_monitor.get_container_status",
|
||||
"src.services.instance.health_monitor.get_container_status",
|
||||
return_value={"status": "running", "exit_code": None, "health": "healthy"},
|
||||
),
|
||||
patch(
|
||||
"src.services.health_monitor.check_tunnel_health",
|
||||
"src.services.instance.health_monitor.check_tunnel_health",
|
||||
return_value={"healthy": False, "tunnel_status": "error_response"},
|
||||
),
|
||||
):
|
||||
|
||||
@@ -10,8 +10,31 @@ 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.project import Project
|
||||
from src.models.user import User
|
||||
from src.models.project.project import Project
|
||||
from src.models.user.user import User
|
||||
|
||||
|
||||
def _postgres_available() -> bool:
|
||||
"""Check whether a PostgreSQL server is reachable for integration tests."""
|
||||
import asyncpg
|
||||
|
||||
async def _check() -> bool:
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host="localhost", port=5432, user="headquarter", password="headquarter", database="headquarter"
|
||||
)
|
||||
await conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return asyncio.run(_check())
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _postgres_available(),
|
||||
reason="PostgreSQL not available on localhost:5432",
|
||||
)
|
||||
|
||||
|
||||
def _prepare_test_db() -> None:
|
||||
@@ -36,8 +59,8 @@ def _prepare_test_db() -> None:
|
||||
def _load_app():
|
||||
import importlib
|
||||
import src.database as database_module
|
||||
import src.api.auth as auth_module
|
||||
import src.api.projects as projects_module
|
||||
import src.api.user.auth as auth_module
|
||||
import src.api.project.projects as projects_module
|
||||
import src.main as main_module
|
||||
|
||||
# Dispose old engine connections before reload to prevent pool exhaustion
|
||||
@@ -56,10 +79,7 @@ def _mint_token(user_id: str) -> str:
|
||||
settings = Settings()
|
||||
return create_session_cookie(
|
||||
settings=settings,
|
||||
subject=user_id,
|
||||
email="test@headquarter.local",
|
||||
name="Test User",
|
||||
expires_at=datetime.now(UTC) + timedelta(minutes=15),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1,24 +1,14 @@
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from src.main import app
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def async_client():
|
||||
async with AsyncClient(app=app, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_create_ssh_key_requires_authentication(async_client: AsyncClient) -> None:
|
||||
response = await async_client.post("/ssh-keys", json={"name": "test-key"})
|
||||
def test_create_ssh_key_requires_authentication(test_client: TestClient) -> None:
|
||||
response = test_client.post("/ssh-keys", json={"name": "test-key"})
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.integration
|
||||
async def test_list_ssh_keys_requires_authentication(async_client: AsyncClient) -> None:
|
||||
response = await async_client.get("/ssh-keys")
|
||||
def test_list_ssh_keys_requires_authentication(test_client: TestClient) -> None:
|
||||
response = test_client.get("/ssh-keys")
|
||||
assert response.status_code == 401
|
||||
|
||||
@@ -10,8 +10,31 @@ 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_type import ToolType
|
||||
from src.models.user import User
|
||||
from src.models.tool.tool_type import ToolType
|
||||
from src.models.user.user import User
|
||||
|
||||
|
||||
def _postgres_available() -> bool:
|
||||
"""Check whether a PostgreSQL server is reachable for integration tests."""
|
||||
import asyncpg
|
||||
|
||||
async def _check() -> bool:
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host="localhost", port=5432, user="headquarter", password="headquarter", database="headquarter"
|
||||
)
|
||||
await conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return asyncio.run(_check())
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _postgres_available(),
|
||||
reason="PostgreSQL not available on localhost:5432",
|
||||
)
|
||||
|
||||
|
||||
def _prepare_test_db() -> None:
|
||||
@@ -36,8 +59,8 @@ def _prepare_test_db() -> None:
|
||||
def _load_app():
|
||||
import importlib
|
||||
import src.database as database_module
|
||||
import src.api.auth as auth_module
|
||||
import src.api.tool_types as tool_types_module
|
||||
import src.api.user.auth as auth_module
|
||||
import src.api.tool.tool_types as tool_types_module
|
||||
import src.main as main_module
|
||||
|
||||
# Dispose old engine connections before reload to prevent pool exhaustion
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestToolTypesAPIExtended:
|
||||
"name": "full-tool",
|
||||
"display_name": "Full Tool",
|
||||
"category": "editor",
|
||||
"interfaces": ["web", "terminal"],
|
||||
"interface_type": "web",
|
||||
"default_port": 8443,
|
||||
"definition_type": "compose",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: code-server\n command: --bind-addr 0.0.0.0:8443\n ports:\n - '8443:8443'\n volumes:\n - \"{{REPO_PATH}}:/workspace\"",
|
||||
@@ -201,20 +201,20 @@ class TestToolTypesAPIExtended:
|
||||
data = response.json()
|
||||
assert data["definition_type"] == "compose"
|
||||
assert data["category"] == "editor"
|
||||
assert data["interfaces"] == ["web", "terminal"]
|
||||
assert data["interface_type"] == "web"
|
||||
assert "readiness_probe" in data
|
||||
|
||||
def test_create_tool_type_without_port_fails(
|
||||
self, authenticated_client: TestClient
|
||||
) -> None:
|
||||
"""Test that creating a tool type without default_port fails validation."""
|
||||
"""Test that creating a tool type requiring a port with default_port=0 fails validation."""
|
||||
response = authenticated_client.post(
|
||||
"/tool-types",
|
||||
json={
|
||||
"name": "no-port-tool",
|
||||
"display_name": "No Port Tool",
|
||||
"category": "utility",
|
||||
"interfaces": ["web"],
|
||||
"interface_type": "web",
|
||||
"definition_type": "compose",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: nginx\n ports:\n - '8080:8080'",
|
||||
"required_variables": [],
|
||||
@@ -256,9 +256,9 @@ class TestToolTypesAPIExtended:
|
||||
"category": "utility",
|
||||
"interface_type": "terminal",
|
||||
"requires_port": False,
|
||||
"default_port": 0,
|
||||
"default_port": 8080,
|
||||
"definition_type": "compose",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine\n command: sleep 3600",
|
||||
"startup_command": "cd /workspace && ls",
|
||||
"required_variables": [],
|
||||
},
|
||||
@@ -280,9 +280,9 @@ class TestToolTypesAPIExtended:
|
||||
"display_name": "Update Startup Tool",
|
||||
"interface_type": "terminal",
|
||||
"requires_port": False,
|
||||
"default_port": 0,
|
||||
"default_port": 8080,
|
||||
"definition_type": "compose",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine\n command: sleep 3600",
|
||||
"required_variables": [],
|
||||
},
|
||||
)
|
||||
@@ -293,6 +293,8 @@ class TestToolTypesAPIExtended:
|
||||
f"/tool-types/{tool_id}",
|
||||
json={
|
||||
"startup_command": "source /etc/profile",
|
||||
"requires_port": False,
|
||||
"default_port": 8080,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -310,9 +312,9 @@ class TestToolTypesAPIExtended:
|
||||
"display_name": "Get Startup Tool",
|
||||
"interface_type": "terminal",
|
||||
"requires_port": False,
|
||||
"default_port": 0,
|
||||
"default_port": 8080,
|
||||
"definition_type": "compose",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine",
|
||||
"compose_template": "version: '3.8'\nservices:\n app:\n image: alpine\n command: sleep 3600",
|
||||
"startup_command": "echo hello",
|
||||
"required_variables": [],
|
||||
},
|
||||
@@ -323,4 +325,3 @@ class TestToolTypesAPIExtended:
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["startup_command"] == "echo hello"
|
||||
assert "Port 9999 is not exposed" in str(data)
|
||||
|
||||
@@ -11,7 +11,30 @@ from sqlalchemy.ext.asyncio import create_async_engine
|
||||
from src.auth.session import create_session_cookie
|
||||
from src.config import Settings, build_database_url
|
||||
from src.models import Base
|
||||
from src.models.user import User
|
||||
from src.models.user.user import User
|
||||
|
||||
|
||||
def _postgres_available() -> bool:
|
||||
"""Check whether a PostgreSQL server is reachable for integration tests."""
|
||||
import asyncpg
|
||||
|
||||
async def _check() -> bool:
|
||||
try:
|
||||
conn = await asyncpg.connect(
|
||||
host="localhost", port=5432, user="headquarter", password="headquarter", database="headquarter"
|
||||
)
|
||||
await conn.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return asyncio.run(_check())
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not _postgres_available(),
|
||||
reason="PostgreSQL not available on localhost:5432",
|
||||
)
|
||||
|
||||
|
||||
def _prepare_users_test_db() -> None:
|
||||
@@ -36,7 +59,7 @@ def _prepare_users_test_db() -> None:
|
||||
def _load_app():
|
||||
import importlib
|
||||
import src.database as database_module
|
||||
import src.api.users as users_module
|
||||
import src.api.user.users as users_module
|
||||
import src.main as main_module
|
||||
|
||||
importlib.reload(database_module)
|
||||
@@ -80,10 +103,7 @@ def _create_auth_cookie(user_id: str) -> str:
|
||||
settings = Settings()
|
||||
return create_session_cookie(
|
||||
settings=settings,
|
||||
subject=user_id,
|
||||
email="test@headquarter.local",
|
||||
name="Test User",
|
||||
expires_at=datetime.now(UTC) + timedelta(minutes=15),
|
||||
user_id=user_id,
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user