diff --git a/tests/api/routes/compute/test_docker_nodes.py b/tests/api/routes/compute/test_docker_nodes.py index 40cb31679..c706f34b3 100644 --- a/tests/api/routes/compute/test_docker_nodes.py +++ b/tests/api/routes/compute/test_docker_nodes.py @@ -226,6 +226,31 @@ class TestDockerNodesRoutes: assert response.status_code == status.HTTP_201_CREATED assert response.json()["type"] == "nio_udp" + async def test_docker_nio_create_bridge_requires_the_field_but_takes_null( + self, app: FastAPI, compute_client: AsyncClient, vm: dict + ) -> None: + """ + BridgeNIO.bridge is required-but-nullable: an omitted key is a 422 + (the NIO factories read it, and the routes drop unset fields — an + omitted key used to pass validation and die as a KeyError during + binding), while an explicit null is the absorbed-link shape. + """ + + url = app.url_path_for( + "compute:create_docker_node_nio", + project_id=vm["project_id"], + node_id=vm["node_id"], + adapter_number="0", + port_number="0", + ) + + response = await compute_client.post(url, json={"type": "nio_bridge"}) + assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT + + response = await compute_client.post(url, json={"type": "nio_bridge", "bridge": None}) + assert response.status_code == status.HTTP_201_CREATED + assert response.json()["bridge"] is None + async def test_docker_nio_batch_create(self, app: FastAPI, compute_client: AsyncClient, vm: dict) -> None: """ Exercise the project-wide batch NIO endpoint: bind two NIOs on the same diff --git a/tests/compute/docker/test_docker_kernel_datapath.py b/tests/compute/docker/test_docker_kernel_datapath.py index 853c1999a..def336b51 100644 --- a/tests/compute/docker/test_docker_kernel_datapath.py +++ b/tests/compute/docker/test_docker_kernel_datapath.py @@ -33,7 +33,7 @@ from gns3server.compute.docker import Docker from gns3server.compute.docker.docker_error import DockerError, DockerHttp404Error from gns3server.compute.docker.docker_vm import DockerVM from gns3server.compute.nios.nio_bridge import NIOBridge -from gns3server.compute.ubridge.ubridge_error import UbridgeError +from gns3server.compute.ubridge.ubridge_error import UbridgeError, UbridgeNamespaceError from tests.utils import AsyncioMagicMock BRIDGE = "gns3a1b2c3d4e5f" @@ -261,6 +261,32 @@ async def test_add_ubridge_kernel_connection_cleans_up_on_failure(vm): assert (0, 0) not in vm._kernel_veths +@pytest.mark.asyncio +async def test_create_veth_cleans_up_when_the_namespace_move_fails(vm): + """ + ``move_to_ns`` failing (the container died between create and move) + raises UbridgeNamespaceError — and must take the same cleanup path as + any other failure: the old branch re-raised before the delete_veth, + leaving the half-created pair behind until the next start's sweep. + """ + + vm._ubridge_hypervisor = MagicMock() + vm._namespace = 42 + + async def failing_send(command): + if command.startswith("docker move_to_ns"): + raise UbridgeError("Could not move interface: No such process") + + vm._ubridge_send = AsyncioMagicMock(side_effect=failing_send) + + with pytest.raises(UbridgeNamespaceError): + await vm._create_veth(0) + + host_ifc, _ = vm._veth_names(0, 0) + vm._ubridge_send.assert_any_call(f'docker delete_veth "{host_ifc}"') + assert vm._kernel_veths == {} + + # --------------------------------------------------------------------------- # Link attach / detach # ---------------------------------------------------------------------------