diff --git a/backend/app/db/database.py b/backend/app/db/database.py index 3831ef4..d3329d9 100644 --- a/backend/app/db/database.py +++ b/backend/app/db/database.py @@ -94,6 +94,16 @@ async def init_db() -> None: with suppress(OperationalError): sql = "UPDATE edges SET animated = 'none' WHERE animated = '0' OR animated = 0 OR animated IS NULL" await conn.exec_driver_sql(sql) + # Ensure existing proxmox nodes have container_mode=1 (they were always containers before the flag existed) + with suppress(OperationalError): + await conn.exec_driver_sql( + "UPDATE nodes SET container_mode = 1 WHERE type = 'proxmox' AND container_mode = 0" + ) + # Rename legacy 'docker' type → 'docker_container' + with suppress(OperationalError): + await conn.exec_driver_sql( + "UPDATE nodes SET type = 'docker_container' WHERE type = 'docker'" + ) async def get_db() -> AsyncGenerator[AsyncSession, None]: diff --git a/backend/tests/test_docker_migration.py b/backend/tests/test_docker_migration.py new file mode 100644 index 0000000..c8672f1 --- /dev/null +++ b/backend/tests/test_docker_migration.py @@ -0,0 +1,96 @@ +""" +Tests for docker type rename and proxmox container_mode migrations. +""" +import os + +os.environ.setdefault("SECRET_KEY", "test-only-secret-key-not-for-production") + +import pytest +from sqlalchemy.ext.asyncio import create_async_engine + +TEST_DB_URL = "sqlite+aiosqlite:///:memory:" + + +async def _setup_table(conn): + await conn.exec_driver_sql(""" + CREATE TABLE IF NOT EXISTS nodes ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL DEFAULT 'generic', + label TEXT NOT NULL DEFAULT '', + container_mode BOOLEAN NOT NULL DEFAULT 0 + ) + """) + + +async def _run_migrations(conn): + await conn.exec_driver_sql( + "UPDATE nodes SET container_mode = 1 WHERE type = 'proxmox' AND container_mode = 0" + ) + await conn.exec_driver_sql( + "UPDATE nodes SET type = 'docker_container' WHERE type = 'docker'" + ) + + +@pytest.mark.asyncio +async def test_proxmox_container_mode_set_to_true(): + engine = create_async_engine(TEST_DB_URL) + async with engine.begin() as conn: + await _setup_table(conn) + await conn.exec_driver_sql( + "INSERT INTO nodes (id, type, label, container_mode) VALUES ('p1', 'proxmox', 'PVE', 0)" + ) + await _run_migrations(conn) + row = (await conn.exec_driver_sql("SELECT container_mode FROM nodes WHERE id = 'p1'")).fetchone() + assert row[0] == 1 + + +@pytest.mark.asyncio +async def test_proxmox_already_true_unchanged(): + engine = create_async_engine(TEST_DB_URL) + async with engine.begin() as conn: + await _setup_table(conn) + await conn.exec_driver_sql( + "INSERT INTO nodes (id, type, label, container_mode) VALUES ('p2', 'proxmox', 'PVE', 1)" + ) + await _run_migrations(conn) + row = (await conn.exec_driver_sql("SELECT container_mode FROM nodes WHERE id = 'p2'")).fetchone() + assert row[0] == 1 + + +@pytest.mark.asyncio +async def test_non_proxmox_container_mode_untouched(): + engine = create_async_engine(TEST_DB_URL) + async with engine.begin() as conn: + await _setup_table(conn) + await conn.exec_driver_sql( + "INSERT INTO nodes (id, type, label, container_mode) VALUES ('s1', 'server', 'Srv', 0)" + ) + await _run_migrations(conn) + row = (await conn.exec_driver_sql("SELECT container_mode FROM nodes WHERE id = 's1'")).fetchone() + assert row[0] == 0 + + +@pytest.mark.asyncio +async def test_docker_type_renamed_to_docker_container(): + engine = create_async_engine(TEST_DB_URL) + async with engine.begin() as conn: + await _setup_table(conn) + await conn.exec_driver_sql( + "INSERT INTO nodes (id, type, label) VALUES ('d1', 'docker', 'My Docker')" + ) + await _run_migrations(conn) + row = (await conn.exec_driver_sql("SELECT type FROM nodes WHERE id = 'd1'")).fetchone() + assert row[0] == 'docker_container' + + +@pytest.mark.asyncio +async def test_docker_host_type_untouched(): + engine = create_async_engine(TEST_DB_URL) + async with engine.begin() as conn: + await _setup_table(conn) + await conn.exec_driver_sql( + "INSERT INTO nodes (id, type, label) VALUES ('d2', 'docker_host', 'Docker Host')" + ) + await _run_migrations(conn) + row = (await conn.exec_driver_sql("SELECT type FROM nodes WHERE id = 'd2'")).fetchone() + assert row[0] == 'docker_host' diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 8a8f16b..2130cf5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -33,7 +33,7 @@ import type { NodeData, EdgeData } from '@/types' const STANDALONE = import.meta.env.VITE_STANDALONE === 'true' const STANDALONE_STORAGE_KEY = 'homelable_canvas' -const CONTAINER_MODE_TYPES = new Set(['proxmox', 'vm', 'lxc', 'docker_host']) +const CONTAINER_MODE_TYPES = new Set(['proxmox', 'docker_host']) export default function App() { const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, nodes, edges, snapshotHistory, undo, redo, copySelectedNodes, pasteNodes } = useCanvasStore() diff --git a/frontend/src/stores/__tests__/canvasStore.test.ts b/frontend/src/stores/__tests__/canvasStore.test.ts index 65af38e..40fdf4f 100644 --- a/frontend/src/stores/__tests__/canvasStore.test.ts +++ b/frontend/src/stores/__tests__/canvasStore.test.ts @@ -99,7 +99,7 @@ describe('canvasStore', () => { it('updateNode clearing parent_id converts position to absolute and clears parentId', () => { const proxmox = { ...makeNode('px1', { type: 'proxmox', container_mode: true }), position: { x: 100, y: 100 } } - const lxc = { ...makeNode('lxc1', { type: 'lxc', parent_id: 'px1' }), position: { x: 30, y: 40 }, parentId: 'px1', extent: 'parent' as const } + const lxc = { ...makeNode('lxc1', { type: 'lxc', parent_id: 'px1' }), position: { x: 130, y: 140 }, parentId: 'px1', extent: 'parent' as const } useCanvasStore.getState().addNode(proxmox) useCanvasStore.getState().addNode(lxc) useCanvasStore.getState().updateNode('lxc1', { parent_id: undefined }) @@ -229,7 +229,7 @@ describe('canvasStore', () => { }) it('deleteNode also removes children with matching parentId', () => { - useCanvasStore.getState().addNode(makeNode('parent')) + useCanvasStore.getState().addNode(makeNode('parent', { container_mode: true })) useCanvasStore.getState().addNode(makeNode('child', { parent_id: 'parent' })) useCanvasStore.getState().deleteNode('parent') const { nodes } = useCanvasStore.getState() @@ -238,7 +238,7 @@ describe('canvasStore', () => { }) it('addNode with parent_id sets parentId and extent', () => { - useCanvasStore.getState().addNode(makeNode('parent')) + useCanvasStore.getState().addNode(makeNode('parent', { container_mode: true })) useCanvasStore.getState().addNode(makeNode('child', { parent_id: 'parent' })) const child = useCanvasStore.getState().nodes.find((n) => n.id === 'child') expect(child?.parentId).toBe('parent')