Merge origin/3.1 into image-manager-out-of-sync

Preserve image reconciliation, locking, and regression coverage while integrating upstream release, dependency, typing, and formatting updates. Align feature code with the new Ruff checks.
This commit is contained in:
Cristi committed 2026-09-28 23:16:24 +03:00
commit 42d30651da
497 files changed
+22962 -24152

No files matched your search

@@ -172,49 +172,38 @@ class TestAccessTicketWebSocketAuth:
monkeypatch.setattr(
HTTPClient,
"get_client",
classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws))
classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)),
)
return compute_ws
async def test_console_ws_accepts_valid_ticket(
self,
app: FastAPI,
base_client: AsyncClient,
compute_credentials,
project: Project,
node: Node,
monkeypatch
self, app: FastAPI, base_client: AsyncClient, compute_credentials, project: Project, node: Node, monkeypatch
) -> None:
# admin is the seeded superadmin, so the RBAC privilege check is skipped
ticket = access_ticket_service.mint("admin", 0, project_id=project.id, node_id=node.id)
self._forward_compute_ws(monkeypatch, [
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None),
])
self._forward_compute_ws(
monkeypatch,
[
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None),
],
)
async with self._ws_client(app, base_client) as client:
async with aconnect_ws(
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws",
client,
params={"token": ticket}
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket}
) as ws:
# reaching the compute forwarding loop means ticket auth succeeded
assert await ws.receive_text() == "device output"
async def test_console_ws_rejects_ticket_bound_to_other_node(
self,
app: FastAPI,
base_client: AsyncClient,
project: Project,
node: Node
self, app: FastAPI, base_client: AsyncClient, project: Project, node: Node
) -> None:
other_node = "00000000-0000-0000-0000-000000000000"
ticket = access_ticket_service.mint("admin", 0, project_id=project.id, node_id=other_node)
async with self._ws_client(app, base_client) as client:
async with aconnect_ws(
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws",
client,
params={"token": ticket}
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket}
) as ws:
notification = await ws.receive_json()
assert notification["event"]["message"] == (
@@ -224,28 +213,18 @@ class TestAccessTicketWebSocketAuth:
)
async def test_console_ws_rejects_ticket_with_stale_token_version(
self,
app: FastAPI,
base_client: AsyncClient,
project: Project,
node: Node
self, app: FastAPI, base_client: AsyncClient, project: Project, node: Node
) -> None:
# logging out bumps the user's token_version: outstanding tickets must die with it
ticket = access_ticket_service.mint("admin", 999, project_id=project.id, node_id=node.id)
async with self._ws_client(app, base_client) as client:
async with aconnect_ws(
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws",
client,
params={"token": ticket}
f"/v3/projects/{project.id}/nodes/{node.id}/console/ws", client, params={"token": ticket}
) as ws:
notification = await ws.receive_json()
assert "Token has been revoked for 'admin'" in notification["event"]["message"]
async def test_ticket_rejected_on_non_console_websocket(
self,
app: FastAPI,
base_client: AsyncClient
) -> None:
async def test_ticket_rejected_on_non_console_websocket(self, app: FastAPI, base_client: AsyncClient) -> None:
# the controller notification stream shares the WS auth dependency but has
# no node binding: a console ticket must not authenticate it
ticket = access_ticket_service.mint("admin", 0, project_id="p1", node_id="n1")
@@ -263,42 +242,26 @@ class TestAccessTicketRestAuth:
pytestmark = pytest.mark.asyncio
async def test_rest_accepts_path_bound_ticket(
self,
app: FastAPI,
base_client: AsyncClient
) -> None:
async def test_rest_accepts_path_bound_ticket(self, app: FastAPI, base_client: AsyncClient) -> None:
# base_client carries no Authorization header, so the ?token= parameter is used
ticket = access_ticket_service.mint("admin", 0, path="/v3/projects")
response = await base_client.get("/v3/projects", params={"token": ticket})
assert response.status_code == status.HTTP_200_OK
async def test_rest_rejects_ticket_bound_to_other_path(
self,
app: FastAPI,
base_client: AsyncClient
) -> None:
async def test_rest_rejects_ticket_bound_to_other_path(self, app: FastAPI, base_client: AsyncClient) -> None:
ticket = access_ticket_service.mint("admin", 0, path="/v3/symbols/router.svg/raw")
response = await base_client.get("/v3/projects", params={"token": ticket})
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert "Invalid or expired access ticket" in response.text
async def test_rest_rejects_node_bound_ticket(
self,
app: FastAPI,
base_client: AsyncClient
) -> None:
async def test_rest_rejects_node_bound_ticket(self, app: FastAPI, base_client: AsyncClient) -> None:
# node-bound (console) tickets must not authenticate REST resources
ticket = access_ticket_service.mint("admin", 0, project_id="p1", node_id="n1")
response = await base_client.get("/v3/projects", params={"token": ticket})
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_rest_rejects_ticket_with_stale_token_version(
self,
app: FastAPI,
base_client: AsyncClient
) -> None:
async def test_rest_rejects_ticket_with_stale_token_version(self, app: FastAPI, base_client: AsyncClient) -> None:
# logging out bumps the user's token_version: outstanding tickets must die with it
ticket = access_ticket_service.mint("admin", 999, path="/v3/projects")
response = await base_client.get("/v3/projects", params={"token": ticket})
+20 -68
View File
@@ -32,7 +32,6 @@ pytestmark = pytest.mark.asyncio
class TestACLRoutes:
@pytest_asyncio.fixture
async def group_id(self, db_session: AsyncSession) -> str:
@@ -50,33 +49,18 @@ class TestACLRoutes:
return role_id
async def test_create_ace(
self,
app: FastAPI,
authorized_client: AsyncClient,
db_session: AsyncSession,
test_user: User,
role_id: str
self, app: FastAPI, authorized_client: AsyncClient, db_session: AsyncSession, test_user: User, role_id: str
) -> None:
# allow the user to create an ACE
rbac_repo = RbacRepository(db_session)
admin_role_id = (await rbac_repo.get_role_by_name("Administrator")).role_id
ace = ACECreate(
path="/access/acl",
ace_type="user",
user_id=test_user.user_id,
role_id=admin_role_id
)
ace = ACECreate(path="/access/acl", ace_type="user", user_id=test_user.user_id, role_id=admin_role_id)
await rbac_repo.create_ace(ace)
# add an ACE on /projects to allow user to create a project
path = f"/projects"
new_ace = {
"path": path,
"ace_type": "user",
"user_id": str(test_user.user_id),
"role_id": role_id
}
new_ace = {"path": path, "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id}
response = await authorized_client.post(app.url_path_for("create_ace"), json=new_ace)
assert response.status_code == status.HTTP_201_CREATED
@@ -87,21 +71,11 @@ class TestACLRoutes:
response = await authorized_client.post(app.url_path_for("create_project"), json={"name": "test"})
assert response.status_code == status.HTTP_201_CREATED
async def test_create_ace_not_existing_endpoint(
self,
app: FastAPI,
client: AsyncClient,
group_id: str,
role_id: str
self, app: FastAPI, client: AsyncClient, group_id: str, role_id: str
) -> None:
new_ace = {
"path": "/projects/invalid",
"ace_type": "group",
"group_id": group_id,
"role_id": role_id
}
new_ace = {"path": "/projects/invalid", "ace_type": "group", "group_id": group_id, "role_id": role_id}
response = await client.post(app.url_path_for("create_ace"), json=new_ace)
assert response.status_code == status.HTTP_400_BAD_REQUEST
@@ -137,35 +111,23 @@ class TestACLRoutes:
assert len(response.json()) == 2
async def test_update_ace(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
test_user: User,
role_id: str
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, test_user: User, role_id: str
) -> None:
rbac_repo = RbacRepository(db_session)
ace_in_db = await rbac_repo.get_ace_by_path(f"/projects")
update_ace = {
"path": f"/appliances",
"ace_type": "user",
"user_id": str(test_user.user_id),
"role_id": role_id
}
response = await client.put(
app.url_path_for("update_ace", ace_id=ace_in_db.ace_id),
json=update_ace
)
update_ace = {"path": f"/appliances", "ace_type": "user", "user_id": str(test_user.user_id), "role_id": role_id}
response = await client.put(app.url_path_for("update_ace", ace_id=ace_in_db.ace_id), json=update_ace)
assert response.status_code == status.HTTP_200_OK
updated_ace_in_db = await rbac_repo.get_ace(ace_in_db.ace_id)
assert updated_ace_in_db.path == f"/appliances"
async def test_delete_ace(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
) -> None:
rbac_repo = RbacRepository(db_session)
@@ -174,22 +136,17 @@ class TestACLRoutes:
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_ace_cleanup(
self,
app: FastAPI,
authorized_client: AsyncClient,
db_session: AsyncSession,
test_user: User,
role_id: str,
self,
app: FastAPI,
authorized_client: AsyncClient,
db_session: AsyncSession,
test_user: User,
role_id: str,
) -> None:
# allow the user to create projects
rbac_repo = RbacRepository(db_session)
ace = ACECreate(
path="/projects",
ace_type="user",
user_id=test_user.user_id,
role_id=role_id
)
ace = ACECreate(path="/projects", ace_type="user", user_id=test_user.user_id, role_id=role_id)
await rbac_repo.create_ace(ace)
response = await authorized_client.post(app.url_path_for("create_project"), json={"name": "test2"})
@@ -197,12 +154,7 @@ class TestACLRoutes:
project_id = response.json()["project_id"]
path = f"/projects/{project_id}"
ace = ACECreate(
path=path,
ace_type="user",
user_id=test_user.user_id,
role_id=role_id
)
ace = ACECreate(path=path, ace_type="user", user_id=test_user.user_id, role_id=role_id)
await rbac_repo.create_ace(ace)
assert await rbac_repo.get_ace_by_path(path)
+14 -17
View File
@@ -27,7 +27,6 @@ pytestmark = pytest.mark.asyncio
class TestApplianceRoutes:
# @pytest.fixture(autouse=True)
# def _install_builtin_appliances(self, controller: Controller):
#
@@ -63,7 +62,9 @@ class TestApplianceRoutes:
response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id), params=params)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_qemu_appliance_install_with_version(self, app: FastAPI, client: AsyncClient, images_dir: str) -> None:
async def test_qemu_appliance_install_with_version(
self, app: FastAPI, client: AsyncClient, images_dir: str
) -> None:
shutil.copy("tests/resources/empty8G.qcow2", os.path.join(images_dir, "QEMU", "empty8G.qcow2"))
appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance
@@ -73,7 +74,9 @@ class TestApplianceRoutes:
assert response.json()["name"] == "Empty VM"
assert response.json()["version"] == "8G"
async def test_qemu_appliance_install_without_version(self, app: FastAPI, client: AsyncClient, images_dir: str) -> None:
async def test_qemu_appliance_install_without_version(
self, app: FastAPI, client: AsyncClient, images_dir: str
) -> None:
appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance
response = await client.post(app.url_path_for("install_appliance", appliance_id=appliance_id))
@@ -82,24 +85,18 @@ class TestApplianceRoutes:
async def test_add_version_appliance(self, app: FastAPI, client: AsyncClient) -> None:
appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance
new_version = {
"name": "99G",
"images": {
"hda_disk_image": "empty99G.qcow2"
}
}
response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version)
new_version = {"name": "99G", "images": {"hda_disk_image": "empty99G.qcow2"}}
response = await client.post(
app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version
)
assert response.status_code == status.HTTP_201_CREATED
assert new_version in response.json()["versions"]
async def test_add_existing_version_appliance(self, app: FastAPI, client: AsyncClient) -> None:
appliance_id = "1cfdf900-7c30-4cb7-8f03-3f61d2581633" # Empty VM appliance
new_version = {
"name": "8G",
"images": {
"hda_disk_image": "empty8G.qcow2"
}
}
response = await client.post(app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version)
new_version = {"name": "8G", "images": {"hda_disk_image": "empty8G.qcow2"}}
response = await client.post(
app.url_path_for("add_appliance_version", appliance_id=appliance_id), json=new_version
)
assert response.status_code == status.HTTP_409_CONFLICT
+14 -43
View File
@@ -30,15 +30,9 @@ from tests.utils import asyncio_patch, AsyncioMagicMock
class TestComputeRoutes:
async def test_compute_create(self, app: FastAPI, client: AsyncClient) -> None:
params = {
"protocol": "http",
"host": "localhost",
"port": 84,
"user": "julien",
"password": "secure"}
params = {"protocol": "http", "host": "localhost", "port": 84, "user": "julien", "password": "secure"}
response = await client.post(app.url_path_for("create_compute"), json=params)
assert response.status_code == status.HTTP_201_CREATED
@@ -57,7 +51,8 @@ class TestComputeRoutes:
"host": "127.0.0.1",
"port": 84,
"user": "julien",
"password": "secure"}
"password": "secure",
}
response = await client.post(app.url_path_for("create_compute"), json=params)
assert response.status_code == status.HTTP_201_CREATED
@@ -82,7 +77,8 @@ class TestComputeRoutes:
async def test_compute_get_local(self, app: FastAPI, client: AsyncClient, controller) -> None:
await controller.add_compute(
compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False)
compute_id="local", name="local", host="127.0.0.1", port=3080, force=True, connect=False
)
response = await client.get(app.url_path_for("get_compute", compute_id="local"))
assert response.status_code == status.HTTP_200_OK
@@ -91,13 +87,7 @@ class TestComputeRoutes:
async def test_compute_update(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None:
params = {
"protocol": "http",
"host": "localhost",
"port": 42,
"user": "julien",
"password": "secure"
}
params = {"protocol": "http", "host": "localhost", "port": 42, "user": "julien", "password": "secure"}
response = await client.post(app.url_path_for("create_compute"), json=params)
assert response.status_code == status.HTTP_201_CREATED
@@ -118,29 +108,22 @@ class TestComputeRoutes:
class TestComputeFeatures:
async def test_compute_list_docker_images(self, app: FastAPI, client: AsyncClient) -> None:
params = {
"protocol": "http",
"host": "localhost",
"port": 84,
"user": "julien",
"password": "secure"
}
params = {"protocol": "http", "host": "localhost", "port": 84, "user": "julien", "password": "secure"}
response = await client.post(app.url_path_for("create_compute"), json=params)
assert response.status_code == status.HTTP_201_CREATED
compute_id = response.json()["compute_id"]
with asyncio_patch("gns3server.controller.compute.Compute.forward", return_value=[{"image": "docker1"}, {"image": "docker2"}]) as mock:
with asyncio_patch(
"gns3server.controller.compute.Compute.forward", return_value=[{"image": "docker1"}, {"image": "docker2"}]
) as mock:
response = await client.get(app.url_path_for("docker_get_images", compute_id=compute_id))
mock.assert_called_with("GET", "docker", "images")
assert response.json() == [{"image": "docker1"}, {"image": "docker2"}]
async def test_compute_pull_docker_image(
self, app: FastAPI, client: AsyncClient, test_compute: Compute
) -> None:
async def test_compute_pull_docker_image(self, app: FastAPI, client: AsyncClient, test_compute: Compute) -> None:
compute = MagicMock()
compute.forward = AsyncioMagicMock(return_value={})
@@ -148,20 +131,14 @@ class TestComputeFeatures:
controller.return_value.get_compute.return_value = compute
response = await client.post(
app.url_path_for("docker_pull_image", compute_id=test_compute.compute_id),
json={"image": "nginx:latest"}
json={"image": "nginx:latest"},
)
compute.forward.assert_called_with("POST", "docker", "images/pull", data={"image": "nginx:latest"})
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_compute_list_virtualbox_vms(self, app: FastAPI, client: AsyncClient) -> None:
params = {
"protocol": "http",
"host": "localhost",
"port": 4242,
"user": "julien",
"password": "secure"
}
params = {"protocol": "http", "host": "localhost", "port": 4242, "user": "julien", "password": "secure"}
response = await client.post(app.url_path_for("get_computes"), json=params)
assert response.status_code == status.HTTP_201_CREATED
compute_id = response.json()["compute_id"]
@@ -173,13 +150,7 @@ class TestComputeFeatures:
async def test_compute_list_vmware_vms(self, app: FastAPI, client: AsyncClient) -> None:
params = {
"protocol": "http",
"host": "localhost",
"port": 4243,
"user": "julien",
"password": "secure"
}
params = {"protocol": "http", "host": "localhost", "port": 4243, "user": "julien", "password": "secure"}
response = await client.post(app.url_path_for("get_computes"), json=params)
assert response.status_code == status.HTTP_201_CREATED
compute_id = response.json()["compute_id"]
@@ -28,7 +28,6 @@ pytestmark = pytest.mark.asyncio
class TestControllerRoutes:
async def test_shutdown_local(self, app: FastAPI, client: AsyncClient, config: Config, monkeypatch) -> None:
# monkeypatch (not bare assignment): a global `os.kill = MagicMock()`
@@ -41,14 +40,12 @@ class TestControllerRoutes:
response = await client.post(app.url_path_for("shutdown"))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert kill_mock.called
async def test_shutdown_non_local(self, app: FastAPI, client: AsyncClient, config: Config) -> None:
response = await client.post(app.url_path_for("shutdown"))
assert response.status_code == status.HTTP_403_FORBIDDEN
# @pytest.mark.asyncio
# async def test_debug(controller_api, config, tmpdir):
#
@@ -68,9 +65,8 @@ class TestControllerRoutes:
# config.set("Server", "local", False)
# response = await controller_api.post('/debug')
# assert response.status_code == 403
async def test_statistics_output(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("statistics"))
assert response.status_code == status.HTTP_200_OK
+25 -38
View File
@@ -27,90 +27,77 @@ pytestmark = pytest.mark.asyncio
class TestDrawingsRoutes:
async def test_create_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
params = {
"svg": '<svg height="210" width="500"><line x1="0" y1="0" x2="200" y2="200" style="stroke:rgb(255,0,0);stroke-width:2" /></svg>',
"x": 10,
"y": 20,
"z": 0
"z": 0,
}
response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["drawing_id"] is not None
async def test_get_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
params = {
"svg": '<svg height="210" width="500"><line x1="0" y1="0" x2="200" y2="200" style="stroke:rgb(255,0,0);stroke-width:2" /></svg>',
"x": 10,
"y": 20,
"z": 0
"z": 0,
}
response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params)
response = await client.get(app.url_path_for(
"get_drawing",
project_id=project.id,
drawing_id=response.json()["drawing_id"])
response = await client.get(
app.url_path_for("get_drawing", project_id=project.id, drawing_id=response.json()["drawing_id"])
)
assert response.status_code == 200
assert response.json()["x"] == 10
async def test_update_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
params = {
"svg": '<svg height="210" width="500"><line x1="0" y1="0" x2="200" y2="200" style="stroke:rgb(255,0,0);stroke-width:2" /></svg>',
"x": 10,
"y": 20,
"z": 0
"z": 0,
}
response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params)
response = await client.put(app.url_path_for(
"update_drawing",
project_id=project.id,
drawing_id=response.json()["drawing_id"]),
json={"x": 42}
response = await client.put(
app.url_path_for("update_drawing", project_id=project.id, drawing_id=response.json()["drawing_id"]),
json={"x": 42},
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["x"] == 42
async def test_all_drawings(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
params = {
"svg": '<svg height="210" width="500"><line x1="0" y1="0" x2="200" y2="200" style="stroke:rgb(255,0,0);stroke-width:2" /></svg>',
"x": 10,
"y": 20,
"z": 0
"z": 0,
}
await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params)
response = await client.get(app.url_path_for("get_drawings", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
# test listing links from a closed project
await project.close(ignore_notification=True)
response = await client.get(app.url_path_for("get_drawings", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
async def test_delete_drawing(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
drawing = Drawing(project)
project._drawings = {drawing.id: drawing}
response = await client.delete(app.url_path_for(
"delete_drawing",
project_id=project.id,
drawing_id=drawing.id)
)
response = await client.delete(app.url_path_for("delete_drawing", project_id=project.id, drawing_id=drawing.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert drawing.id not in project.drawings
+10 -16
View File
@@ -25,33 +25,27 @@ pytestmark = pytest.mark.asyncio
class TestGNS3VMRoutes:
async def test_list_vms(self, app: FastAPI, client: AsyncClient) -> None:
with asyncio_patch("gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", return_value=[{"vmname": "test"}]):
with asyncio_patch(
"gns3server.controller.gns3vm.vmware_gns3_vm.VMwareGNS3VM.list", return_value=[{"vmname": "test"}]
):
response = await client.get(app.url_path_for("get_vms", engine="vmware"))
assert response.status_code == status.HTTP_200_OK
assert response.json() == [
{
"vmname": "test"
}
]
assert response.json() == [{"vmname": "test"}]
async def test_engines(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_engines"))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) > 0
async def test_put_gns3vm(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.put(app.url_path_for("update_gns3vm_settings"), json={"vmname": "TEST VM"})
assert response.status_code == status.HTTP_200_OK
assert response.json()["vmname"] == "TEST VM"
async def test_get_gns3vm(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_gns3vm_settings"))
assert response.status_code == status.HTTP_200_OK
+13 -59
View File
@@ -28,7 +28,6 @@ pytestmark = pytest.mark.asyncio
class TestGroupRoutes:
async def test_create_group(self, app: FastAPI, client: AsyncClient) -> None:
new_group = {"name": "group1"}
@@ -56,47 +55,30 @@ class TestGroupRoutes:
update_group = {"name": "group42"}
response = await client.put(
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id),
json=update_group
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), json=update_group
)
assert response.status_code == status.HTTP_200_OK
updated_group_in_db = await user_repo.get_user_group(group_in_db.user_group_id)
assert updated_group_in_db.name == "group42"
async def test_cannot_update_admin_group(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_cannot_update_admin_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
group_in_db = await user_repo.get_user_group_by_name("Administrators")
update_group = {"name": "Hackers"}
response = await client.put(
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id),
json=update_group
app.url_path_for("update_user_group", user_group_id=group_in_db.user_group_id), json=update_group
)
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_delete_group(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_delete_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
group_in_db = await user_repo.get_user_group_by_name("group42")
response = await client.delete(app.url_path_for("delete_user_group", user_group_id=group_in_db.user_group_id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_cannot_delete_admin_group(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_cannot_delete_admin_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
group_in_db = await user_repo.get_user_group_by_name("Administrators")
@@ -105,32 +87,21 @@ class TestGroupRoutes:
class TestGroupMembersRoutes:
async def test_add_to_group_already_member(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession
) -> None:
user_repo = UsersRepository(db_session)
group_in_db = await user_repo.get_user_group_by_name("Users")
response = await client.put(
app.url_path_for(
"add_member_to_group",
user_group_id=group_in_db.user_group_id,
user_id=str(test_user.user_id)
"add_member_to_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id)
)
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_add_member_to_group(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession
) -> None:
user_repo = UsersRepository(db_session)
@@ -140,9 +111,7 @@ class TestGroupMembersRoutes:
group_in_db = await user_repo.create_user_group(new_user_group)
response = await client.put(
app.url_path_for(
"add_member_to_group",
user_group_id=group_in_db.user_group_id,
user_id=str(test_user.user_id)
"add_member_to_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id)
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -150,29 +119,16 @@ class TestGroupMembersRoutes:
assert len(members) == 1
assert members[0].username == test_user.username
async def test_get_user_group_members(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_get_user_group_members(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
group_in_db = await user_repo.get_user_group_by_name("test_group")
response = await client.get(
app.url_path_for(
"get_user_group_members",
user_group_id=group_in_db.user_group_id)
)
response = await client.get(app.url_path_for("get_user_group_members", user_group_id=group_in_db.user_group_id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
async def test_remove_member_from_group(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, test_user: User, db_session: AsyncSession
) -> None:
user_repo = UsersRepository(db_session)
@@ -180,9 +136,7 @@ class TestGroupMembersRoutes:
response = await client.delete(
app.url_path_for(
"remove_member_from_group",
user_group_id=group_in_db.user_group_id,
user_id=str(test_user.user_id)
"remove_member_from_group", user_group_id=group_in_db.user_group_id, user_id=str(test_user.user_id)
),
)
assert response.status_code == status.HTTP_204_NO_CONTENT
+20 -12
View File
@@ -61,6 +61,7 @@ class TestImageSyncRoutes:
async def test_overlapping_sync_is_conflict(self, client, sync_service):
from gns3server.utils.image_inventory import ImageLock
async with ImageLock("image-inventory"):
response = await client.post("/v3/images/sync", json={})
assert response.status_code == 409
@@ -99,7 +100,9 @@ class TestImageSyncRoutes:
async def test_sync_requires_allocate_privilege(self, client, sync_service, test_user):
token = auth_service.create_access_token(test_user.username, secret_key=DEFAULT_JWT_SECRET_KEY)
with patch("gns3server.db.repositories.rbac.RbacRepository.check_user_has_privilege", new=AsyncMock(return_value=False)):
with patch(
"gns3server.db.repositories.rbac.RbacRepository.check_user_has_privilege", new=AsyncMock(return_value=False)
):
response = await client.post("/v3/images/sync", json={}, headers={"Authorization": f"Bearer {token}"})
assert response.status_code == 403
assert "Image.Allocate" in response.text
@@ -110,21 +113,26 @@ class TestImageSyncRoutes:
assert response.status_code == 401
async def test_template_can_detach_missing_image(self, app, client, sync_service, db_session):
uploaded = await client.post('/v3/images/upload/detach-missing.qcow2', content=QCOW)
uploaded = await client.post("/v3/images/upload/detach-missing.qcow2", content=QCOW)
assert uploaded.status_code == 201
image = uploaded.json()
image_id = (await ImagesRepository(db_session).get_image(image['path'])).image_id
created = await client.post('/v3/templates', json={
'name': 'Detach missing image', 'compute_id': 'local',
'template_type': 'qemu', 'hda_disk_image': image['filename'],
})
image_id = (await ImagesRepository(db_session).get_image(image["path"])).image_id
created = await client.post(
"/v3/templates",
json={
"name": "Detach missing image",
"compute_id": "local",
"template_type": "qemu",
"hda_disk_image": image["filename"],
},
)
assert created.status_code == 201
template_id = created.json()['template_id']
os.unlink(image['path'])
await client.post('/v3/images/sync', json={})
template_id = created.json()["template_id"]
os.unlink(image["path"])
await client.post("/v3/images/sync", json={})
await sync_service.task
updated = await client.put(f'/v3/templates/{template_id}', json={'hda_disk_image': ''})
updated = await client.put(f"/v3/templates/{template_id}", json={"hda_disk_image": ""})
assert updated.status_code == 200
assert not await ImagesRepository(db_session).get_image_templates(image_id)
# Detaching a template does not silently remove the missing catalog row.
assert await ImagesRepository(db_session).get_image(image['path']) is not None
assert await ImagesRepository(db_session).get_image(image["path"]) is not None
+82 -97
View File
@@ -43,7 +43,7 @@ def iou_32_bit_image(tmpdir) -> str:
path = os.path.join(tmpdir, "iou_32bit.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x01\x01')
f.write(b"\x7fELF\x01\x01\x01")
return path
@@ -55,7 +55,7 @@ def iou_64_bit_image(tmpdir) -> str:
path = os.path.join(tmpdir, "iou_64bit.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x02\x01\x01')
f.write(b"\x7fELF\x02\x01\x01")
return path
@@ -67,7 +67,7 @@ def ios_image(tmpdir) -> str:
path = os.path.join(tmpdir, "ios_image.bin")
with open(path, "wb+") as f:
f.write(b'\x7fELF\x01\x02\x01')
f.write(b"\x7fELF\x01\x02\x01")
return path
@@ -79,7 +79,7 @@ def qcow2_image(tmpdir) -> str:
path = os.path.join(tmpdir, "image.qcow2")
with open(path, "wb+") as f:
f.write(b'QFI\xfb\x00\x00\x00')
f.write(b"QFI\xfb\x00\x00\x00")
return path
@@ -91,7 +91,7 @@ def invalid_image(tmpdir) -> str:
path = os.path.join(tmpdir, "invalid_image.bin")
with open(path, "wb+") as f:
f.write(b'\x01\x01\x01\x01')
f.write(b"\x01\x01\x01\x01")
return path
@@ -103,23 +103,23 @@ def empty_image(tmpdir) -> str:
path = os.path.join(tmpdir, "empty_image.bin")
with open(path, "wb+") as f:
f.write(b'')
f.write(b"")
return path
class TestImageRoutes:
async def test_create_image(self, app: FastAPI, client: AsyncClient, images_dir) -> None:
async def create_disk(path, options):
with open(path, "wb") as f:
f.write(b'QFI\xfb\x00\x00\x00')
f.write(b"QFI\xfb\x00\x00\x00")
Qemu.instance().create_disk_image = AsyncioMagicMock(side_effect=create_disk)
path = os.path.join(os.path.join(images_dir, "QEMU", "new_image.qcow2"))
image_name = os.path.basename(path)
response = await client.post(
app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30})
app.url_path_for("create_qemu_image", image_path=image_name), json={"format": "qcow2", "size": 30}
)
assert response.status_code == status.HTTP_201_CREATED
assert os.path.isfile(path)
@@ -132,18 +132,18 @@ class TestImageRoutes:
("ios", "ios_image", True),
("ios", "invalid_image", False),
("qemu", "qcow2_image", True),
("qemu", "empty_image", False)
("qemu", "empty_image", False),
),
)
async def test_upload_image(
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
image_type: str,
fixture_name: str,
valid_request: bool,
request
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
image_type: str,
fixture_name: str,
valid_request: bool,
request,
) -> None:
image_path = request.getfixturevalue(fixture_name)
@@ -153,9 +153,7 @@ class TestImageRoutes:
image_data = f.read()
image_checksum.update(image_data)
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
if valid_request:
assert response.status_code == status.HTTP_201_CREATED
@@ -182,21 +180,15 @@ class TestImageRoutes:
with open(qcow2_image, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path="image1.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
# same image with same name is uploaded again, it should return 409 Conflict
response = await client.post(
app.url_path_for("upload_image", image_path="image1.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image1.qcow2"), content=image_data)
assert response.status_code == status.HTTP_409_CONFLICT
# same image with different name but same checksum is uploaded again, it should return 409 Conflict
response = await client.post(
app.url_path_for("upload_image", image_path="image2.qcow2"),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path="image2.qcow2"), content=image_data)
assert response.status_code == status.HTTP_409_CONFLICT
async def test_image_delete(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
@@ -211,14 +203,14 @@ class TestImageRoutes:
response = await client.get(app.url_path_for("get_image", image_path=image_name))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_image_deleted_on_disk(self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str) -> None:
async def test_image_deleted_on_disk(
self, app: FastAPI, client: AsyncClient, images_dir: str, qcow2_image: str
) -> None:
image_name = os.path.basename(qcow2_image)
with open(qcow2_image, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
response = await client.delete(app.url_path_for("delete_image", image_path=image_name))
@@ -233,42 +225,30 @@ class TestImageRoutes:
),
)
async def test_upload_image_subdir(
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
qcow2_image: str,
subdir: str,
expected_result: int,
db_session: AsyncSession
self,
app: FastAPI,
client: AsyncClient,
images_dir: str,
qcow2_image: str,
subdir: str,
expected_result: int,
db_session: AsyncSession,
) -> None:
image_name = os.path.basename(qcow2_image)
with open(qcow2_image, "rb") as f:
image_data = f.read()
image_path = os.path.join(subdir, image_name)
response = await client.post(
app.url_path_for("upload_image", image_path=image_path),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_path), content=image_data)
assert response.status_code == expected_result
async def test_image_delete_multiple_match(
self,
app: FastAPI,
client: AsyncClient,
qcow2_image: str
) -> None:
async def test_image_delete_multiple_match(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
image_name = os.path.basename(qcow2_image)
response = await client.delete(app.url_path_for("delete_image", image_path=image_name))
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_image_delete_with_subdir(
self,
app: FastAPI,
client: AsyncClient,
qcow2_image: str
) -> None:
async def test_image_delete_with_subdir(self, app: FastAPI, client: AsyncClient, qcow2_image: str) -> None:
image_name = os.path.basename(qcow2_image)
image_path = os.path.join("subdir", image_name)
@@ -305,17 +285,20 @@ class TestImageRoutes:
with open(os.path.join(project_dir, f"{name}.gns3"), "w+") as f:
json.dump(topology, f)
return await controller.add_project(
project_id=project_id, name=name, path=project_dir,
filename=f"{name}.gns3", status="closed",
project_id=project_id,
name=name,
path=project_dir,
filename=f"{name}.gns3",
status="closed",
)
async def test_image_delete_used_by_project(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
) -> None:
"""
An image referenced by a node in a project must not be deleted,
@@ -324,15 +307,21 @@ class TestImageRoutes:
image_path = os.path.join(tmpdir, "used.qcow2")
with open(image_path, "wb+") as f:
f.write(b'\x42\x42\x42\x42')
f.write(b"\x42\x42\x42\x42")
images_repo = ImagesRepository(db_session)
await images_repo.add_image("used.qcow2", "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5")
guarded_project = await self._add_closed_project_with_node(
controller, "Guarded",
{"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local",
"name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}},
controller,
"Guarded",
{
"node_id": str(uuid.uuid4()),
"node_type": "qemu",
"compute_id": "local",
"name": "n1",
"properties": {"hda_disk_image_backing_file": "used.qcow2"},
},
)
response = await client.delete(app.url_path_for("delete_image", image_path="used.qcow2"))
@@ -347,12 +336,12 @@ class TestImageRoutes:
assert not os.path.exists(image_path)
async def test_prune_images_keeps_project_referenced(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
db_session: AsyncSession,
tmpdir: str,
) -> None:
"""
Pruning must keep images referenced by a project node while
@@ -363,13 +352,19 @@ class TestImageRoutes:
for filename in ("used.qcow2", "unused.qcow2"):
image_path = os.path.join(tmpdir, filename)
with open(image_path, "wb+") as f:
f.write(b'\x42\x42\x42\x42')
f.write(b"\x42\x42\x42\x42")
await images_repo.add_image(filename, "qemu", 42, image_path, "e342eb86c1229b6c154367a5476969b5", "md5")
await self._add_closed_project_with_node(
controller, "Guarded",
{"node_id": str(uuid.uuid4()), "node_type": "qemu", "compute_id": "local",
"name": "n1", "properties": {"hda_disk_image_backing_file": "used.qcow2"}},
controller,
"Guarded",
{
"node_id": str(uuid.uuid4()),
"node_type": "qemu",
"compute_id": "local",
"name": "n1",
"properties": {"hda_disk_image_backing_file": "used.qcow2"},
},
)
response = await client.delete(app.url_path_for("prune_images"))
@@ -381,10 +376,7 @@ class TestImageRoutes:
assert not os.path.exists(os.path.join(tmpdir, "unused.qcow2"))
async def test_image_upload_create_appliance(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
await controller.appliance_manager.install_builtin_appliances()
@@ -396,7 +388,8 @@ class TestImageRoutes:
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
params={"install_appliances": "true"},
content=image_data)
content=image_data,
)
assert response.status_code == status.HTTP_201_CREATED
templates_repo = TemplatesRepository(db_session)
@@ -407,19 +400,14 @@ class TestImageRoutes:
await templates_repo.delete_template(templates[0].template_id)
async def test_install_all(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
image_path = "tests/resources/empty100G.qcow2"
image_name = os.path.basename(image_path)
with open(image_path, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=image_name),
content=image_data)
response = await client.post(app.url_path_for("upload_image", image_path=image_name), content=image_data)
assert response.status_code == status.HTTP_201_CREATED
controller.appliance_manager.load_appliances() # make sure appliances are loaded
@@ -440,10 +428,7 @@ class TestImageRoutes:
await templates_repo.delete_template(templates[0].template_id)
async def test_install_all_skips_existing_template_name(
self, app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
controller: Controller
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, controller: Controller
) -> None:
# two images matching two versions of the same appliance must not
# produce two templates with the same name
@@ -458,8 +443,8 @@ class TestImageRoutes:
with open(image_path, "rb") as f:
image_data = f.read()
response = await client.post(
app.url_path_for("upload_image", image_path=os.path.basename(image_path)),
content=image_data)
app.url_path_for("upload_image", image_path=os.path.basename(image_path)), content=image_data
)
assert response.status_code == status.HTTP_201_CREATED
controller.appliance_manager.load_appliances() # make sure appliances are loaded
+158 -255
View File
@@ -37,57 +37,43 @@ pytestmark = pytest.mark.asyncio
class TestLinkRoutes:
@pytest_asyncio.fixture
async def nodes(self, compute: Compute, project: Project) -> Tuple[Node, Node]:
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
node1 = await project.add_node(compute, "node1", None, node_type="qemu")
node1._ports = [EthernetPort("E0", 0, 0, 3)]
node2 = await project.add_node(compute, "node2", None, node_type="qemu")
node2._ports = [EthernetPort("E0", 0, 2, 4)]
return node1, node2
async def test_create_link(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
nodes: Tuple[Node, Node]
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
) -> None:
node1, node2 = nodes
filters = {
"delay": [10, 0],
"frequency_drop": [50]
}
filters = {"delay": [10, 0], "frequency_drop": [50]}
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Text",
"x": 42,
"y": 0
}
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
],
"filters": filters
})
response = await client.post(
app.url_path_for("create_link", project_id=project.id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {"text": "Text", "x": 42, "y": 0},
},
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
],
"filters": filters,
},
)
assert mock.called
assert response.status_code == status.HTTP_201_CREATED
@@ -96,264 +82,193 @@ class TestLinkRoutes:
assert response.json()["nodes"][0]["label"]["x"] == 42
assert len(project.links) == 1
assert list(project.links.values())[0].filters == filters
async def test_create_link_failure(
self,
app: FastAPI,
client: AsyncClient,
compute: Compute,
project: Project
self, app: FastAPI, client: AsyncClient, compute: Compute, project: Project
) -> None:
"""
Make sure the link is deleted if we failed to create it.
The failure is triggered by connecting the link to itself
"""
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
node1 = await project.add_node(compute, "node1", None, node_type="qemu")
node1._ports = [EthernetPort("E0", 0, 0, 3), EthernetPort("E0", 0, 0, 4)]
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Text",
"x": 42,
"y": 0
}
},
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 4
}
]
})
assert response.status_code == status.HTTP_409_CONFLICT
assert len(project.links) == 0
async def test_get_link(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
nodes: Tuple[Node, Node]
) -> None:
node1, node2 = nodes
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
response = await client.post(
app.url_path_for("create_link", project_id=project.id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Text",
"x": 42,
"y": 0
}
"label": {"text": "Text", "x": 42, "y": 0},
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
{"node_id": node1.id, "adapter_number": 0, "port_number": 4},
]
})
},
)
assert response.status_code == status.HTTP_409_CONFLICT
assert len(project.links) == 0
async def test_get_link(
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
) -> None:
node1, node2 = nodes
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = await client.post(
app.url_path_for("create_link", project_id=project.id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {"text": "Text", "x": 42, "y": 0},
},
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
]
},
)
assert mock.called
link_id = response.json()["link_id"]
assert response.json()["nodes"][0]["label"]["x"] == 42
response = await client.get(app.url_path_for("get_link", project_id=project.id, link_id=link_id))
assert response.status_code == status.HTTP_200_OK
assert response.json()["nodes"][0]["label"]["x"] == 42
async def test_update_link_suspend(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
nodes: Tuple[Node, Node]
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
) -> None:
node1, node2 = nodes
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
response = await client.post(
app.url_path_for("create_link", project_id=project.id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {"text": "Text", "x": 42, "y": 0},
},
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
]
},
)
assert mock.called
link_id = response.json()["link_id"]
assert response.json()["nodes"][0]["label"]["x"] == 42
response = await client.put(
app.url_path_for("update_link", project_id=project.id, link_id=link_id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Text",
"x": 42,
"y": 0
}
"label": {"text": "Hello", "x": 64, "y": 0},
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
]
})
assert mock.called
link_id = response.json()["link_id"]
assert response.json()["nodes"][0]["label"]["x"] == 42
response = await client.put(app.url_path_for("update_link", project_id=project.id, link_id=link_id), json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Hello",
"x": 64,
"y": 0
}
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
],
"suspend": True
})
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
],
"suspend": True,
},
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["nodes"][0]["label"]["x"] == 64
assert response.json()["suspend"]
assert response.json()["filters"] == {}
async def test_update_link(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
nodes: Tuple[Node, Node]
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
) -> None:
filters = {
"delay": [10, 0],
"frequency_drop": [50]
}
filters = {"delay": [10, 0], "frequency_drop": [50]}
node1, node2 = nodes
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
response = await client.post(app.url_path_for("create_link", project_id=project.id), json={
response = await client.post(
app.url_path_for("create_link", project_id=project.id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {"text": "Text", "x": 42, "y": 0},
},
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
]
},
)
assert mock.called
link_id = response.json()["link_id"]
assert response.json()["nodes"][0]["label"]["x"] == 42
response = await client.put(
app.url_path_for("update_link", project_id=project.id, link_id=link_id),
json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Text",
"x": 42,
"y": 0
}
"label": {"text": "Hello", "x": 64, "y": 0},
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
]
})
assert mock.called
link_id = response.json()["link_id"]
assert response.json()["nodes"][0]["label"]["x"] == 42
response = await client.put(app.url_path_for("update_link", project_id=project.id, link_id=link_id), json={
"nodes": [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3,
"label": {
"text": "Hello",
"x": 64,
"y": 0
}
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
],
"filters": filters
})
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
],
"filters": filters,
},
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["nodes"][0]["label"]["x"] == 64
assert list(project.links.values())[0].filters == filters
async def test_list_link(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
nodes: Tuple[Node, Node]
self, app: FastAPI, client: AsyncClient, project: Project, nodes: Tuple[Node, Node]
) -> None:
filters = {
"delay": [10, 0],
"frequency_drop": [50]
}
filters = {"delay": [10, 0], "frequency_drop": [50]}
node1, node2 = nodes
nodes = [
{
"node_id": node1.id,
"adapter_number": 0,
"port_number": 3
},
{
"node_id": node2.id,
"adapter_number": 2,
"port_number": 4
}
{"node_id": node1.id, "adapter_number": 0, "port_number": 3},
{"node_id": node2.id, "adapter_number": 2, "port_number": 4},
]
with asyncio_patch("gns3server.controller.udp_link.UDPLink.create") as mock:
await client.post(app.url_path_for("create_link", project_id=project.id), json={
"nodes": nodes,
"filters": filters
})
await client.post(
app.url_path_for("create_link", project_id=project.id), json={"nodes": nodes, "filters": filters}
)
assert mock.called
response = await client.get(app.url_path_for("get_links", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
assert response.json()[0]["filters"] == filters
# test listing links from a closed project
await project.close(ignore_notification=True)
response = await client.get(app.url_path_for("get_links", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
assert response.json()[0]["filters"] == filters
async def test_reset_link(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = UDPLink(project)
project._links = {link.id: link}
with asyncio_patch("gns3server.controller.udp_link.UDPLink.delete") as delete_mock:
@@ -362,28 +277,27 @@ class TestLinkRoutes:
assert delete_mock.called
assert create_mock.called
assert response.status_code == status.HTTP_200_OK
async def test_start_capture(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = Link(project)
project._links = {link.id: link}
with asyncio_patch("gns3server.controller.link.Link.start_capture") as mock:
response = await client.post(app.url_path_for("start_capture", project_id=project.id, link_id=link.id), json={})
response = await client.post(
app.url_path_for("start_capture", project_id=project.id, link_id=link.id), json={}
)
assert mock.called
assert response.status_code == status.HTTP_201_CREATED
async def test_stop_capture(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = Link(project)
project._links = {link.id: link}
with asyncio_patch("gns3server.controller.link.Link.stop_capture") as mock:
response = await client.post(app.url_path_for("stop_capture", project_id=project.id, link_id=link.id))
assert mock.called
assert response.status_code == status.HTTP_204_NO_CONTENT
# async def test_pcap(controller_api, http_client, project):
#
# async def pcap_capture():
@@ -403,20 +317,18 @@ class TestLinkRoutes:
# assert mock.called
# assert response.status_code == 200
# assert b'hello' == response.body
async def test_delete_link(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = Link(project)
project._links = {link.id: link}
with asyncio_patch("gns3server.controller.link.Link.delete") as mock:
response = await client.delete(app.url_path_for("delete_link", project_id=project.id, link_id=link.id))
assert mock.called
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_list_filters(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = Link(project)
project._links = {link.id: link}
with patch("gns3server.controller.link.Link.available_filters", return_value=FILTERS) as mock:
@@ -425,7 +337,6 @@ class TestLinkRoutes:
assert response.status_code == status.HTTP_200_OK
assert response.json() == FILTERS
async def test_get_udp_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
"""
Test getting UDP tunnel interface information from a link.
@@ -433,12 +344,12 @@ class TestLinkRoutes:
link = Link(project)
project._links = {link.id: link}
cloud_node = MagicMock()
cloud_node.node_type = "cloud"
cloud_node.id = str(uuid.uuid4())
cloud_node.name = "Cloud1"
compute = MagicMock()
response = MagicMock()
response.json = {
@@ -449,15 +360,15 @@ class TestLinkRoutes:
"lport": 20000,
"rhost": "127.0.0.1",
"rport": 30000,
"name": "UDP tunnel 1"
"name": "UDP tunnel 1",
}
]
}
compute.get = AsyncioMagicMock(return_value=response)
cloud_node.compute = compute
link._nodes = [{"node": cloud_node, "port_number": 1}]
response = await client.get(app.url_path_for("get_iface", project_id=project.id, link_id=link.id))
assert response.status_code == status.HTTP_200_OK
@@ -468,7 +379,6 @@ class TestLinkRoutes:
assert result["rport"] == 30000
assert result["type"] == "udp"
async def test_get_ethernet_interface(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
"""
Test getting ethernet interface information from a link.
@@ -484,14 +394,7 @@ class TestLinkRoutes:
compute = MagicMock()
response = MagicMock()
response.json = {
"ports_mapping": [
{
"port_number": 1,
"type": "ethernet",
"interface": "eth0",
"name": "Ethernet 1"
}
]
"ports_mapping": [{"port_number": 1, "type": "ethernet", "interface": "eth0", "name": "Ethernet 1"}]
}
compute.get = AsyncioMagicMock(return_value=response)
cloud_node.compute = compute
@@ -504,4 +407,4 @@ class TestLinkRoutes:
result = response.json()
assert result["node_id"] == cloud_node.id
assert result["interface"] == "eth0"
assert result["type"] == "ethernet"
assert result["type"] == "ethernet"
+130 -75
View File
@@ -55,27 +55,29 @@ def _add_marker(project, tag, enabled, node_id, frames=None):
"""Create a paused/capturing link+marker and optionally its pcap."""
link = UDPLink(project)
link._markers["icmp"] = {"bpf": "icmp", "tag": tag, "enabled": enabled, "color": None,
"highlight_duration": None, "capture_node_id": node_id,
"direction": None, "data_link_type": "DLT_EN10MB"}
link._markers["icmp"] = {
"bpf": "icmp",
"tag": tag,
"enabled": enabled,
"color": None,
"highlight_duration": None,
"capture_node_id": node_id,
"direction": None,
"data_link_type": "DLT_EN10MB",
}
project._links[link.id] = link
if frames is not None:
_write_pcap(
f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames
)
_write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames)
return link
class TestReplayRoutes:
async def test_range_409_while_capturing(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
_add_marker(project, tag=7, enabled=False, node_id="n1")
running = _add_marker(project, tag=7, enabled=True, node_id="n2")
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=7)
)
response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7))
assert response.status_code == status.HTTP_409_CONFLICT
assert "icmp" in response.json()["message"]
assert running.id in response.json()["message"]
@@ -84,23 +86,25 @@ class TestReplayRoutes:
_add_marker(project, tag=7, enabled=False, node_id="n1")
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=99)
)
response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=99))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_range_501_without_sharkd(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
# A non-empty pcap: the engine must be consulted, and without sharkd
# the whole feature is unavailable (hard requirement, no degraded mode).
_add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
])
_add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
],
)
with patch("gns3server.controller.marker_replay.shutil.which", return_value=None):
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=7)
)
response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7))
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
# The app's HTTPException handler unifies the body as {"message": …}.
assert "sharkd" in response.json()["message"]
@@ -115,9 +119,16 @@ class TestReplayRoutes:
project._links.update({r1.id: r1, r2.id: r2})
def _wire(link, node_id, frames):
link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None,
"highlight_duration": None, "capture_node_id": node_id,
"direction": None, "data_link_type": "DLT_EN10MB"}
link._markers["icmp"] = {
"bpf": "icmp",
"tag": 7,
"enabled": False,
"color": None,
"highlight_duration": None,
"capture_node_id": node_id,
"direction": None,
"data_link_type": "DLT_EN10MB",
}
_write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames)
# r1→r2 captures at t1 and t3; r2→r3 captures at t2 and t3 (same µs
@@ -132,9 +143,7 @@ class TestReplayRoutes:
monkeypatch.setattr(marker_replay, "_columns_for", fake_columns)
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=7)
)
response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7))
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert body["tag"] == 7
@@ -143,8 +152,10 @@ class TestReplayRoutes:
assert body["end"] == "1693472002.000000"
assert [f["node_id"] for f in body["frames"]] == ["n1", "n2", "n1", "n2"]
assert [f["ts"] for f in body["frames"]] == [
"1693472000.500000", "1693472001.000000",
"1693472002.000000", "1693472002.000000",
"1693472000.500000",
"1693472001.000000",
"1693472002.000000",
"1693472002.000000",
]
# Wireshark-style columns ride along on every frame entry.
assert body["frames"][0]["src"] == "10.0.0.1"
@@ -156,9 +167,15 @@ class TestReplayRoutes:
self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch
) -> None:
_add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 0, b"a" * 60),
])
_add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 0, b"a" * 60),
],
)
async def fake_columns(pcap, filter_expr):
return {1: _cols()}
@@ -172,14 +189,18 @@ class TestReplayRoutes:
assert response.status_code == status.HTTP_200_OK
assert response.json() == {"frames": []}
async def test_frames_window_hit(
self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch
) -> None:
async def test_frames_window_hit(self, app: FastAPI, client: AsyncClient, project: Project, monkeypatch) -> None:
_add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 0, b"a" * 60),
(1693472000, 150000, b"a" * 60),
])
_add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 0, b"a" * 60),
(1693472000, 150000, b"a" * 60),
],
)
async def fake_columns(pcap, filter_expr):
return {1: _cols(), 2: _cols()}
@@ -191,15 +212,19 @@ class TestReplayRoutes:
params={"ts": "1693472000.000000", "window_ms": 150},
)
assert response.status_code == status.HTTP_200_OK
assert [f["ts"] for f in response.json()["frames"]] == [
"1693472000.000000", "1693472000.150000"
]
assert [f["ts"] for f in response.json()["frames"]] == ["1693472000.000000", "1693472000.150000"]
async def test_detail_404_on_ts_mismatch(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
])
link = _add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
],
)
response = await client.get(
app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7),
@@ -216,9 +241,16 @@ class TestReplayRoutes:
project._links.update({r1.id: r1, r2.id: r2})
def _wire(link, node_id, frames):
link._markers["icmp"] = {"bpf": "icmp", "tag": 7, "enabled": False, "color": None,
"highlight_duration": None, "capture_node_id": node_id,
"direction": None, "data_link_type": "DLT_EN10MB"}
link._markers["icmp"] = {
"bpf": "icmp",
"tag": 7,
"enabled": False,
"color": None,
"highlight_duration": None,
"capture_node_id": node_id,
"direction": None,
"data_link_type": "DLT_EN10MB",
}
_write_pcap(f"{project.markers_directory}/{node_id}_{link.id}_icmp.pcap", frames)
_wire(r1, "n1", [(1693472000, 0, b"a" * 60), (1693472002, 0, b"a" * 60)])
@@ -254,15 +286,19 @@ class TestReplayRoutes:
self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions
) -> None:
_add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
(1693472001, 0, _tcp_syn_frame()),
])
_add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
(1693472001, 0, _tcp_syn_frame()),
],
)
# Unfiltered: both frames with real engine columns.
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=7)
)
response = await client.get(app.url_path_for("replay_tag_range", project_id=project.id, tag=7))
assert response.status_code == status.HTTP_200_OK
frames = response.json()["frames"]
assert [f["proto"] for f in frames] == ["ICMP", "TCP"]
@@ -285,9 +321,15 @@ class TestReplayRoutes:
self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions
) -> None:
_add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
])
_add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
],
)
response = await client.get(
app.url_path_for("replay_tag_range", project_id=project.id, tag=7),
@@ -308,14 +350,19 @@ class TestReplayRoutes:
self, app: FastAPI, client: AsyncClient, project: Project, no_residual_sessions
) -> None:
link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
])
link = _add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
],
)
response = await client.get(
app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7),
params={"ts": "1693472000.123456", "node_id": "n1",
"link_id": link.id, "marker": "icmp"},
params={"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"},
)
assert response.status_code == status.HTTP_200_OK
body = response.json()
@@ -345,12 +392,17 @@ class TestReplayRoutes:
# Two frames in the same microsecond: only the explicit frame number
# (from the frame list entry) tells them apart.
link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
(1693472000, 123456, _tcp_syn_frame()),
])
common = {"ts": "1693472000.123456", "node_id": "n1",
"link_id": link.id, "marker": "icmp"}
link = _add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
(1693472000, 123456, _tcp_syn_frame()),
],
)
common = {"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"}
response = await client.get(
app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7),
@@ -369,18 +421,21 @@ class TestReplayRoutes:
assert response.json()["hex"] == _tcp_syn_frame().hex()
@sharkd_present
async def test_detail_501_when_sharkd_disappears(
self, app: FastAPI, client: AsyncClient, project: Project
) -> None:
async def test_detail_501_when_sharkd_disappears(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = _add_marker(project, tag=7, enabled=False, node_id="n1", frames=[
(1693472000, 123456, _icmp_frame()),
])
link = _add_marker(
project,
tag=7,
enabled=False,
node_id="n1",
frames=[
(1693472000, 123456, _icmp_frame()),
],
)
with patch("gns3server.controller.marker_replay.shutil.which", return_value=None):
response = await client.get(
app.url_path_for("replay_tag_frame_detail", project_id=project.id, tag=7),
params={"ts": "1693472000.123456", "node_id": "n1",
"link_id": link.id, "marker": "icmp"},
params={"ts": "1693472000.123456", "node_id": "n1", "link_id": link.id, "marker": "icmp"},
)
assert response.status_code == status.HTTP_501_NOT_IMPLEMENTED
+34 -19
View File
@@ -35,14 +35,17 @@ pytestmark = pytest.mark.asyncio
def _inherited(link, name="arp"):
"""Inject an inherited marker so the controller's inheritance guard can fire."""
link._markers[f"global-{name}"] = {
"bpf": name, "tag": None, "enabled": True, "color": None,
"highlight_duration": None, "capture_node_id": "node-id",
"bpf": name,
"tag": None,
"enabled": True,
"color": None,
"highlight_duration": None,
"capture_node_id": "node-id",
"inherited_from": name,
}
class TestMarkerRoutes:
# -----------------------------------------------------------------------
# Per-link markers
# -----------------------------------------------------------------------
@@ -81,7 +84,9 @@ class TestMarkerRoutes:
_, kwargs = mock.call_args
assert kwargs["name"] == "web"
async def test_create_marker_global_prefix_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
async def test_create_marker_global_prefix_rejected(
self, app: FastAPI, client: AsyncClient, project: Project
) -> None:
link = UDPLink(project)
project._links = {link.id: link}
@@ -105,7 +110,9 @@ class TestMarkerRoutes:
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
async def test_create_marker_name_too_long_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
async def test_create_marker_name_too_long_rejected(
self, app: FastAPI, client: AsyncClient, project: Project
) -> None:
link = UDPLink(project)
project._links = {link.id: link}
@@ -119,13 +126,17 @@ class TestMarkerRoutes:
async def test_get_markers(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = UDPLink(project)
link._markers["web"] = {"bpf": "tcp port 80", "tag": None, "enabled": True,
"color": None, "highlight_duration": 800, "capture_node_id": "n1"}
link._markers["web"] = {
"bpf": "tcp port 80",
"tag": None,
"enabled": True,
"color": None,
"highlight_duration": 800,
"capture_node_id": "n1",
}
project._links = {link.id: link}
response = await client.get(
app.url_path_for("get_markers", project_id=project.id, link_id=link.id)
)
response = await client.get(app.url_path_for("get_markers", project_id=project.id, link_id=link.id))
assert response.status_code == status.HTTP_200_OK
assert response.json()["web"]["highlight_duration"] == 800
@@ -197,7 +208,9 @@ class TestMarkerRoutes:
assert kwargs["bpf"] == "arp"
assert kwargs["highlight_duration"] == 1200
async def test_create_marker_definition_global_prefix_rejected(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
async def test_create_marker_definition_global_prefix_rejected(
self, app: FastAPI, client: AsyncClient, project: Project
) -> None:
with asyncio_patch("gns3server.controller.project.Project.create_marker_definition") as mock:
response = await client.post(
@@ -235,9 +248,7 @@ class TestMarkerRoutes:
}
project._links = {}
response = await client.get(
app.url_path_for("get_marker_definitions", project_id=project.id)
)
response = await client.get(app.url_path_for("get_marker_definitions", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert body["arp"]["bpf"] == "arp"
@@ -251,13 +262,17 @@ class TestMarkerRoutes:
async def test_get_project_markers(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
link = UDPLink(project)
link._markers["icmp"] = {"bpf": "icmp", "tag": 1, "enabled": True, "color": "#ff5722",
"highlight_duration": 800, "capture_node_id": "node-1"}
link._markers["icmp"] = {
"bpf": "icmp",
"tag": 1,
"enabled": True,
"color": "#ff5722",
"highlight_duration": 800,
"capture_node_id": "node-1",
}
project._links = {link.id: link}
response = await client.get(
app.url_path_for("get_project_markers", project_id=project.id)
)
response = await client.get(app.url_path_for("get_project_markers", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
body = response.json()
key = f"{link.id}/icmp"
@@ -24,7 +24,6 @@ pytestmark = pytest.mark.asyncio
class TestNetmikoRoutes:
async def test_device_types(self, app: FastAPI, client: AsyncClient) -> None:
"""
Test listing the device types supported by the installed Netmiko library.
+231 -429
View File
@@ -49,74 +49,69 @@ pytestmark = pytest.mark.asyncio
class TestNodeRoutes:
@pytest.fixture
def node(self, project: Project, compute: Compute) -> Node:
node = Node(project, compute, "test", node_type="vpcs")
project._nodes[node.id] = node
return node
async def test_create_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
response = await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {
"startup_script": "echo test"
}
})
response = await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {"startup_script": "echo test"},
},
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "test"
assert "name" not in response.json()["properties"]
async def test_list_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {
"startup_script": "echo test"
}
})
await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {"startup_script": "echo test"},
},
)
response = await client.get(app.url_path_for("get_nodes", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert response.json()[0]["name"] == "test"
# test listing nodes from a closed project
await project.close(ignore_notification=True)
response = await client.get(app.url_path_for("get_nodes", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert response.json()[0]["name"] == "test"
async def test_list_node_with_missing_image(
self, app: FastAPI, client: AsyncClient, project: Project, node: Node
) -> None:
node._missing_images = [
{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}
]
node._missing_images = [{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}]
response = await client.get(app.url_path_for("get_nodes", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
data = response.json()[0]
assert data["missing_image"] is True
assert data["missing_images"] == [
{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}
]
assert data["missing_images"] == [{"property": "path", "image": "i86bi-linux-l2.bin", "image_type": "iou"}]
@pytest.mark.parametrize(
"tags, expected_match",
@@ -129,37 +124,33 @@ class TestNodeRoutes:
),
)
async def test_list_nodes_with_tags(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
tags: list,
expected_match: bool
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, tags: list, expected_match: bool
) -> None:
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag1", "tag2"],
"properties": {
"startup_script": "echo test"
}
})
await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag1", "tag2"],
"properties": {"startup_script": "echo test"},
},
)
await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test2",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag3", "tag4"],
"properties": {
"startup_script": "echo test"
}
})
await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test2",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag3", "tag4"],
"properties": {"startup_script": "echo test"},
},
)
params = {"tags": tags}
response = await client.get(app.url_path_for("get_nodes", project_id=project.id), params=params)
@@ -169,161 +160,106 @@ class TestNodeRoutes:
else:
assert len(response.json()) == 0
async def test_get_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
) -> None:
async def test_get_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
response = await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {
"startup_script": "echo test"
}
})
response = await client.get(app.url_path_for("get_node", project_id=project.id, node_id=response.json()["node_id"]))
response = await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {"startup_script": "echo test"},
},
)
response = await client.get(
app.url_path_for("get_node", project_id=project.id, node_id=response.json()["node_id"])
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["name"] == "test"
async def test_update_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
response.json = {"console": 2048}
compute.put = AsyncioMagicMock(return_value=response)
response = await client.put(app.url_path_for("update_node", project_id=project.id, node_id=node.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag1", "tag2"],
"properties": {
"startup_script": "echo test"
}
})
response = await client.put(
app.url_path_for("update_node", project_id=project.id, node_id=node.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"tags": ["tag1", "tag2"],
"properties": {"startup_script": "echo test"},
},
)
assert response.status_code == 200
assert response.json()["name"] == "test"
assert "name" not in response.json()["properties"]
assert response.json()["tags"] == ["tag1", "tag2"]
async def test_start_all_nodes(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
async def test_start_all_nodes(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("start_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_stop_all_nodes(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
) -> None:
async def test_stop_all_nodes(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("stop_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_suspend_all_nodes(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("suspend_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_reload_all_nodes(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("reload_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_reset_console_all_nodes(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("reset_console_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_start_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("start_node", project_id=project.id, node_id=node.id), json={})
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_stop_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("stop_node", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_suspend_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.post = AsyncioMagicMock()
@@ -331,12 +267,7 @@ class TestNodeRoutes:
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_suspend_node_unsupported(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
# node types without suspend support (e.g. VPCS, IOU) must surface the
@@ -348,12 +279,7 @@ class TestNodeRoutes:
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
async def test_suspend_all_nodes_tolerates_unsupported(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
# suspending all nodes of a mixed project stays best-effort: nodes
@@ -363,259 +289,162 @@ class TestNodeRoutes:
)
response = await client.post(app.url_path_for("suspend_all_nodes", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_reload_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
):
async def test_reload_node(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node):
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("reload_node", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_isolate_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
):
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("isolate_node", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_unisolate_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.post = AsyncioMagicMock()
response = await client.post(app.url_path_for("unisolate_node", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_duplicate_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
response.json({"console": 2035})
compute.post = AsyncioMagicMock(return_value=response)
response = await client.post(app.url_path_for("duplicate_node", project_id=project.id, node_id=node.id),
json={"x": 10, "y": 5, "z": 0})
response = await client.post(
app.url_path_for("duplicate_node", project_id=project.id, node_id=node.id), json={"x": 10, "y": 5, "z": 0}
)
assert response.status_code == status.HTTP_201_CREATED
async def test_delete_node(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.post = AsyncioMagicMock()
response = await client.delete(app.url_path_for("delete_node", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_dynamips_idle_pc(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
response.json = {"idlepc": "0x60606f54"}
compute.get = AsyncioMagicMock(return_value=response)
node._node_type = "dynamips" # force Dynamips node type
response = await client.get(app.url_path_for("auto_idlepc", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_200_OK
assert response.json()["idlepc"] == "0x60606f54"
async def test_dynamips_idle_pc_wrong_node_type(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = await client.get(app.url_path_for("auto_idlepc", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_dynamips_idlepc_proposals(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
response.json = ["0x60606f54", "0x33805a22"]
compute.get = AsyncioMagicMock(return_value=response)
node._node_type = "dynamips" # force Dynamips node type
response = await client.get(app.url_path_for("idlepc_proposals", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_200_OK
assert response.json() == ["0x60606f54", "0x33805a22"]
async def test_dynamips_idlepc_proposals_wrong_node_type(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = await client.get(app.url_path_for("idlepc_proposals", project_id=project.id, node_id=node.id))
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_qemu_disk_image_create(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
compute.post = AsyncioMagicMock(return_value=response)
node._node_type = "qemu" # force Qemu node type
response = await client.post(
app.url_path_for("create_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"),
json={"format": "qcow2", "size": 30}
json={"format": "qcow2", "size": 30},
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_qemu_disk_image_create_wrong_node_type(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = await client.post(
app.url_path_for("create_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"),
json={"format": "qcow2", "size": 30}
json={"format": "qcow2", "size": 30},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_qemu_disk_image_update(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
compute.put = AsyncioMagicMock(return_value=response)
node._node_type = "qemu" # force Qemu node type
response = await client.put(
app.url_path_for("update_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"),
json={"extend": 10}
json={"extend": 10},
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_qemu_disk_image_update_wrong_node_type(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = await client.put(
app.url_path_for("update_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2"),
json={"extend": 10}
json={"extend": 10},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_qemu_disk_image_delete(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = MagicMock()
compute.delete = AsyncioMagicMock(return_value=response)
node._node_type = "qemu" # force Qemu node type
response = await client.delete(
app.url_path_for("delete_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2")
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_qemu_disk_image_delete_wrong_node_type(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
response = await client.delete(
app.url_path_for("delete_disk_image", project_id=project.id, node_id=node.id, disk_name="hda_disk.qcow2")
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_get_file(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
# Mock the streaming response
@@ -632,56 +461,52 @@ class TestNodeRoutes:
compute.http_query = AsyncioMagicMock(return_value=mock_response)
response = await client.get(app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello"))
response = await client.get(
app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="hello")
)
assert response.status_code == status.HTTP_200_OK
assert response.content == b'world'
assert response.content == b"world"
compute.http_query.assert_called_with(
"GET",
"/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(
project_id=project.id,
node_id=node.id),
project_id=project.id, node_id=node.id
),
timeout=None,
stream=True)
stream=True,
)
response = await client.get(app.url_path_for(
"get_file",
project_id=project.id,
node_id=node.id,
file_path="../hello"))
response = await client.get(
app.url_path_for("get_file", project_id=project.id, node_id=node.id, file_path="../hello")
)
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_post_file(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
compute.http_query = AsyncioMagicMock()
response = await client.post(app.url_path_for(
"post_file",
project_id=project.id,
node_id=node.id,
file_path="hello"), content=b"hello")
response = await client.post(
app.url_path_for("post_file", project_id=project.id, node_id=node.id, file_path="hello"), content=b"hello"
)
assert response.status_code == status.HTTP_201_CREATED
# Verify http_query was called with stream parameter
compute.http_query.assert_called_once()
call_args = compute.http_query.call_args
assert call_args[0][0] == "POST"
assert call_args[0][1] == "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(project_id=project.id, node_id=node.id)
assert call_args[0][1] == "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello".format(
project_id=project.id, node_id=node.id
)
assert call_args[1]["timeout"] is None
# data should be an async generator from request.stream()
assert hasattr(call_args[1]["data"], "__aiter__")
response = await client.get("/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id))
response = await client.get(
"/projects/{project_id}/nodes/{node_id}/files/../hello".format(project_id=project.id, node_id=node.id)
)
assert response.status_code == status.HTTP_404_NOT_FOUND
# @pytest.mark.asyncio
# async def test_get_and_post_with_nested_paths_normalization(controller_api, project, node, compute):
#
@@ -701,8 +526,6 @@ class TestNodeRoutes:
# compute.http_query.assert_called_with("POST", "/projects/{project_id}/files/project-files/vpcs/{node_id}/hello/nested".format(project_id=project.id, node_id=node.id), data=b'hello', timeout=None, raw=True)
class FakeComputeConsoleWebSocket:
"""
Stand-in for the aiohttp ClientWebSocketResponse returned when connecting
@@ -823,21 +646,21 @@ class TestNodeConsoleWebSocketRoutes:
monkeypatch.setattr(
HTTPClient,
"get_client",
classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws))
classmethod(lambda cls: SimpleNamespace(ws_connect=lambda *args, **kwargs: compute_ws)),
)
return compute_ws
async def test_console_forwards_compute_output_to_client(
self,
compute_credentials,
node: Node,
monkeypatch
self, compute_credentials, node: Node, monkeypatch
) -> None:
compute_ws = self._forward_compute_ws(monkeypatch, [
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None),
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x00\x01", None),
])
compute_ws = self._forward_compute_ws(
monkeypatch,
[
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "device output", None),
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x00\x01", None),
],
)
websocket = FakeClientWebSocket()
await ws_console(websocket, current_user=MagicMock(), node=node)
@@ -846,19 +669,18 @@ class TestNodeConsoleWebSocketRoutes:
assert compute_ws.closed
async def test_console_client_disconnect_mid_stream(
self,
compute_credentials,
node: Node,
monkeypatch,
caplog
self, compute_credentials, node: Node, monkeypatch, caplog
) -> None:
# regression test: the client disconnects while the compute is still
# streaming console output, send must not leak WebSocketDisconnect
compute_ws = self._forward_compute_ws(monkeypatch, [
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 1", None),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 2", None),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 3", None),
])
compute_ws = self._forward_compute_ws(
monkeypatch,
[
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 1", None),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 2", None),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "line 3", None),
],
)
websocket = FakeClientWebSocket(fail_after=1)
with caplog.at_level(logging.INFO):
@@ -868,17 +690,15 @@ class TestNodeConsoleWebSocketRoutes:
assert compute_ws.closed
assert any("has disconnected from controller console WebSocket" in record.message for record in caplog.records)
async def test_vnc_console_client_disconnect_mid_stream(
self,
compute_credentials,
node: Node,
monkeypatch
) -> None:
async def test_vnc_console_client_disconnect_mid_stream(self, compute_credentials, node: Node, monkeypatch) -> None:
# regression test: same as above for the VNC console forwarding loop
compute_ws = self._forward_compute_ws(monkeypatch, [
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x01\x02", None),
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x03\x04", None),
])
compute_ws = self._forward_compute_ws(
monkeypatch,
[
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x01\x02", None),
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"\x03\x04", None),
],
)
websocket = FakeClientWebSocket(fail_after=0)
await vnc_console(websocket, current_user=MagicMock(), node=node)
@@ -978,10 +798,9 @@ class TestConsoleWebSocketRoutes:
stack = contextlib.ExitStack()
http_client = MagicMock()
http_client.ws_connect = MagicMock(return_value=_FakeComputeWebSocketContext(fake_ws))
stack.enter_context(patch(
"gns3server.api.routes.controller.nodes.HTTPClient.get_client",
return_value=http_client
))
stack.enter_context(
patch("gns3server.api.routes.controller.nodes.HTTPClient.get_client", return_value=http_client)
)
stack.enter_context(patch.object(ASGIWebSocketAsyncNetworkStream, "send", _conforming_stream_send))
return stack
@@ -1000,13 +819,7 @@ class TestConsoleWebSocketRoutes:
await asyncio.sleep(0.05)
async def test_console_ws_client_disconnect(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node,
caplog
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node, caplog
) -> None:
"""
A client disconnecting while the compute keeps streaming console output
@@ -1033,27 +846,23 @@ class TestConsoleWebSocketRoutes:
assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled"
assert fake_ws.closed, "the compute console WebSocket should have been closed"
assert any(
"has disconnected from controller console WebSocket" in record.getMessage()
for record in caplog.records
"has disconnected from controller console WebSocket" in record.getMessage() for record in caplog.records
)
async def test_console_ws_compute_closes_session(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node
) -> None:
"""
When the compute closes the console WebSocket the client should receive
the frames sent before the close, then the close itself.
"""
fake_ws = _FakeComputeWebSocket(messages=[
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"output", ""),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "done", ""),
])
fake_ws = _FakeComputeWebSocket(
messages=[
aiohttp.WSMessage(aiohttp.WSMsgType.BINARY, b"output", ""),
aiohttp.WSMessage(aiohttp.WSMsgType.TEXT, "done", ""),
]
)
with self._patches(fake_ws):
async with AsyncClient(base_url="http://test-api", transport=ASGIWebSocketTransport(app=app)) as ws_client:
async with aconnect_ws(
@@ -1069,13 +878,7 @@ class TestConsoleWebSocketRoutes:
assert fake_ws.closed
async def test_vnc_console_ws_client_disconnect(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
compute: Compute,
node: Node,
caplog
self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute, node: Node, caplog
) -> None:
"""
Same as the console test, for the VNC endpoint (binary frames only).
@@ -1097,6 +900,5 @@ class TestConsoleWebSocketRoutes:
assert fake_ws.stream_cancelled, "the compute -> client forwarding task should have been cancelled"
assert fake_ws.closed, "the compute VNC console WebSocket should have been closed"
assert any(
"has disconnected from controller VNC console WebSocket" in record.getMessage()
for record in caplog.records
"has disconnected from controller VNC console WebSocket" in record.getMessage() for record in caplog.records
)
+13 -42
View File
@@ -32,7 +32,6 @@ pytestmark = pytest.mark.asyncio
class TestPoolRoutes:
async def test_resource_pool(self, app: FastAPI, client: AsyncClient) -> None:
new_group = {"name": "pool1"}
@@ -60,28 +59,23 @@ class TestPoolRoutes:
update_pool = {"name": "pool42"}
response = await client.put(
app.url_path_for("update_resource_pool", resource_pool_id=pool_in_db.resource_pool_id),
json=update_pool
app.url_path_for("update_resource_pool", resource_pool_id=pool_in_db.resource_pool_id), json=update_pool
)
assert response.status_code == status.HTTP_200_OK
updated_pool_in_db = await pools_repo.get_resource_pool(pool_in_db.resource_pool_id)
assert updated_pool_in_db.name == "pool42"
async def test_resource_group(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_resource_group(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
pools_repo = ResourcePoolsRepository(db_session)
pool_in_db = await pools_repo.get_resource_pool_by_name("pool42")
response = await client.delete(app.url_path_for("delete_resource_pool", resource_pool_id=pool_in_db.resource_pool_id))
response = await client.delete(
app.url_path_for("delete_resource_pool", resource_pool_id=pool_in_db.resource_pool_id)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
class TestResourcesPoolRoutes:
@pytest_asyncio.fixture
async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project:
project_id = str(uuid.uuid4())
@@ -90,11 +84,7 @@ class TestResourcesPoolRoutes:
return controller.get_project(project_id)
async def test_add_resource_to_pool(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
project: Project
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project
) -> None:
pools_repo = ResourcePoolsRepository(db_session)
@@ -104,9 +94,7 @@ class TestResourcesPoolRoutes:
pool_in_db = await pools_repo.create_resource_pool(new_resource_pool)
response = await client.put(
app.url_path_for(
"add_resource_to_pool",
resource_pool_id=pool_in_db.resource_pool_id,
resource_id=str(project.id)
"add_resource_to_pool", resource_pool_id=pool_in_db.resource_pool_id, resource_id=str(project.id)
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -115,11 +103,7 @@ class TestResourcesPoolRoutes:
assert str(resources[0].resource_id) == project.id
async def test_add_to_resource_already_in_resource_pool(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
project: Project
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project
) -> None:
pools_repo = ResourcePoolsRepository(db_session)
@@ -132,34 +116,23 @@ class TestResourcesPoolRoutes:
app.url_path_for(
"add_resource_to_pool",
resource_pool_id=pool_in_db.resource_pool_id,
resource_id=str(resource.resource_id)
resource_id=str(resource.resource_id),
)
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
async def test_get_pool_resources(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_get_pool_resources(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
pools_repo = ResourcePoolsRepository(db_session)
pool_in_db = await pools_repo.get_resource_pool_by_name("pool1")
response = await client.get(
app.url_path_for(
"get_pool_resources",
resource_pool_id=pool_in_db.resource_pool_id)
app.url_path_for("get_pool_resources", resource_pool_id=pool_in_db.resource_pool_id)
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 2
async def test_remove_resource_from_pool(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
project: Project
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession, project: Project
) -> None:
pools_repo = ResourcePoolsRepository(db_session)
@@ -173,9 +146,7 @@ class TestResourcesPoolRoutes:
response = await client.delete(
app.url_path_for(
"remove_resource_from_pool",
resource_pool_id=pool_in_db.resource_pool_id,
resource_id=str(project.id)
"remove_resource_from_pool", resource_pool_id=pool_in_db.resource_pool_id, resource_id=str(project.id)
),
)
assert response.status_code == status.HTTP_204_NO_CONTENT
@@ -19,7 +19,6 @@ pytestmark = pytest.mark.asyncio
class TestPrivilegesRoute:
async def test_get_privileges(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_privileges"))
assert response.status_code == status.HTTP_200_OK
+217 -291
View File
@@ -35,202 +35,157 @@ pytestmark = pytest.mark.asyncio
class TestControllerProjectRoutes:
@pytest_asyncio.fixture
async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project:
project_id = str(uuid.uuid4())
params = {"name": "test", "project_id": project_id}
await client.post(app.url_path_for("create_project"), json=params)
return controller.get_project(project_id)
async def test_create_project_with_path(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller,
config
self, app: FastAPI, client: AsyncClient, controller: Controller, config
) -> None:
params = {"name": "test", "path": str(config.settings.Server.projects_path), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"}
params = {
"name": "test",
"path": str(config.settings.Server.projects_path),
"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f",
}
response = await client.post(app.url_path_for("create_project"), json=params)
# The projects directory itself must never become a project directory
assert response.status_code == status.HTTP_403_FORBIDDEN
params = {"name": "test", "path": os.path.join(str(config.settings.Server.projects_path), "custom"), "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"}
params = {
"name": "test",
"path": os.path.join(str(config.settings.Server.projects_path), "custom"),
"project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f",
}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "test"
assert response.json()["project_id"] == "00010203-0405-0607-0809-0a0b0c0d0e0f"
assert response.json()["status"] == "opened"
async def test_create_project_without_dir(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller
) -> None:
async def test_create_project_without_dir(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["project_id"] == "10010203-0405-0607-0809-0a0b0c0d0e0f"
assert response.json()["name"] == "test"
async def test_create_project_with_uuid(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller
) -> None:
async def test_create_project_with_uuid(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f"}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == 201
assert response.json()["project_id"] == "30010203-0405-0607-0809-0a0b0c0d0e0f"
assert response.json()["name"] == "test"
async def test_create_project_with_variables(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller
self, app: FastAPI, client: AsyncClient, controller: Controller
) -> None:
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}]
params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == 201
assert response.json()["variables"] == [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
assert response.json()["variables"] == [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}]
async def test_create_project_with_supplier(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller
self, app: FastAPI, client: AsyncClient, controller: Controller
) -> None:
supplier = {
'logo': 'logo.png',
'url': 'http://example.com/'
}
supplier = {"logo": "logo.png", "url": "http://example.com/"}
params = {"name": "test", "project_id": "30010203-0405-0607-0809-0a0b0c0d0e0f", "supplier": supplier}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["supplier"] == supplier
async def test_update_project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f"}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["project_id"] == "10010203-0405-0607-0809-0a0b0c0d0e0f"
assert response.json()["name"] == "test"
params = {"name": "test2"}
response = await client.put(app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"),
json=params)
response = await client.put(
app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), json=params
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["name"] == "test2"
async def test_update_project_with_variables(
self,
app: FastAPI,
client: AsyncClient,
controller: Controller
self, app: FastAPI, client: AsyncClient, controller: Controller
) -> None:
variables = [
{"name": "TEST1"},
{"name": "TEST2", "value": "value1"}
]
variables = [{"name": "TEST1"}, {"name": "TEST2", "value": "value1"}]
params = {"name": "test", "project_id": "10010203-0405-0607-0809-0a0b0c0d0e0f", "variables": variables}
response = await client.post(app.url_path_for("create_project"), json=params)
assert response.status_code == status.HTTP_201_CREATED
params = {"name": "test2"}
response = await client.put(app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"),
json=params)
response = await client.put(
app.url_path_for("update_project", project_id="10010203-0405-0607-0809-0a0b0c0d0e0f"), json=params
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["variables"] == variables
async def test_list_projects(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
params = {"name": "test", "project_id": "00010203-0405-0607-0809-0a0b0c0d0e0f"}
await client.post(app.url_path_for("create_project"), json=params)
response = await client.get(app.url_path_for("get_projects"))
assert response.status_code == status.HTTP_200_OK
projects = response.json()
assert projects[0]["name"] == "test"
async def test_get_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
response = await client.get(app.url_path_for("get_project", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert response.json()["name"] == "test"
async def test_delete_project(
self,
app: FastAPI,
client: AsyncClient,
project: Project,
controller: Controller
self, app: FastAPI, client: AsyncClient, project: Project, controller: Controller
) -> None:
with asyncio_patch("gns3server.controller.project.Project.delete", return_value=True) as mock:
response = await client.delete(app.url_path_for("delete_project", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert mock.called
assert project not in controller.projects
async def test_delete_project_invalid_uuid(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.delete(app.url_path_for("delete_project", project_id=str(uuid.uuid4())))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_close_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
with asyncio_patch("gns3server.controller.project.Project.close", return_value=True) as mock:
response = await client.post(app.url_path_for("close_project", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
assert mock.called
async def test_open_project(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
with asyncio_patch("gns3server.controller.project.Project.open", return_value=True) as mock:
response = await client.post(app.url_path_for("open_project", project_id=project.id))
assert response.status_code == status.HTTP_201_CREATED
assert mock.called
async def test_load_project(self, app: FastAPI, client: AsyncClient, project: Project, config) -> None:
with asyncio_patch("gns3server.controller.Controller.load_project", return_value=project) as mock:
response = await client.post(app.url_path_for("load_project"), json={"path": "/tmp/test.gns3"})
assert response.status_code == status.HTTP_201_CREATED
mock.assert_called_with("/tmp/test.gns3")
assert response.json()["project_id"] == project.id
# @pytest.mark.asyncio
# async def test_notification(controller_api, http_client, project, controller):
#
@@ -250,8 +205,7 @@ class TestControllerProjectRoutes:
#
# response = await controller_api.get("/projects/{project_id}/notifications".format(project_id=uuid.uuid4()))
# assert response.status_code == 404
# @pytest.mark.asyncio
# async def test_notification_ws(controller_api, http_client, controller, project):
#
@@ -269,179 +223,155 @@ class TestControllerProjectRoutes:
# await ws.close()
#
# assert project.status_code == "opened"
async def test_export_with_images(
self,
app: FastAPI,
client: AsyncClient,
tmpdir,
project: Project
) -> None:
async def test_export_with_images(self, app: FastAPI, client: AsyncClient, tmpdir, project: Project) -> None:
project.dump = MagicMock()
os.makedirs(project.path, exist_ok=True)
with open(os.path.join(project.path, 'a'), 'w+') as f:
f.write('hello')
with open(os.path.join(project.path, "a"), "w+") as f:
f.write("hello")
os.makedirs(str(tmpdir / "IOS"))
with open(str(tmpdir / "IOS" / "test.image"), "w+") as f:
f.write("AAA")
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "test.image"
},
"node_type": "dynamips"
}
]
}
}
with open(os.path.join(project.path, "test.gns3"), 'w+') as f:
topology = {"topology": {"nodes": [{"properties": {"image": "test.image"}, "node_type": "dynamips"}]}}
with open(os.path.join(project.path, "test.gns3"), "w+") as f:
json.dump(topology, f)
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS")):
response = await client.get(app.url_path_for("export_project", project_id=project.id),
params={"include_images": "yes"})
response = await client.get(
app.url_path_for("export_project", project_id=project.id), params={"include_images": "yes"}
)
assert response.status_code == status.HTTP_200_OK
assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f:
assert response.headers["CONTENT-TYPE"] == "application/gns3project"
assert response.headers[
"CONTENT-DISPOSITION"
] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format(
name=project.name
)
with open(str(tmpdir / "project.zip"), "wb+") as f:
f.write(response.content)
with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip:
with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip:
with myzip.open("a") as myfile:
content = myfile.read()
assert content == b"hello"
myzip.getinfo("images/IOS/test.image")
async def test_export_without_images(self, app: FastAPI, client: AsyncClient, tmpdir, project: Project) -> None:
project.dump = MagicMock()
os.makedirs(project.path, exist_ok=True)
with open(os.path.join(project.path, 'a'), 'w+') as f:
f.write('hello')
with open(os.path.join(project.path, "a"), "w+") as f:
f.write("hello")
os.makedirs(str(tmpdir / "IOS"))
with open(str(tmpdir / "IOS" / "test.image"), "w+") as f:
f.write("AAA")
topology = {
"topology": {
"nodes": [
{
"properties": {
"image": "test.image"
},
"node_type": "dynamips"
}
]
}
}
with open(os.path.join(project.path, "test.gns3"), 'w+') as f:
topology = {"topology": {"nodes": [{"properties": {"image": "test.image"}, "node_type": "dynamips"}]}}
with open(os.path.join(project.path, "test.gns3"), "w+") as f:
json.dump(topology, f)
with patch("gns3server.compute.Dynamips.get_images_directory", return_value=str(tmpdir / "IOS"),):
response = await client.get(app.url_path_for("export_project", project_id=project.id),
params={"include_images": "0"})
with patch(
"gns3server.compute.Dynamips.get_images_directory",
return_value=str(tmpdir / "IOS"),
):
response = await client.get(
app.url_path_for("export_project", project_id=project.id), params={"include_images": "0"}
)
assert response.status_code == status.HTTP_200_OK
assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f:
assert response.headers["CONTENT-TYPE"] == "application/gns3project"
assert response.headers[
"CONTENT-DISPOSITION"
] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format(
name=project.name
)
with open(str(tmpdir / "project.zip"), "wb+") as f:
f.write(response.content)
with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip:
with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip:
with myzip.open("a") as myfile:
content = myfile.read()
assert content == b"hello"
# Image should not exported
with pytest.raises(KeyError):
myzip.getinfo("images/IOS/test.image")
@pytest.mark.parametrize(
"compression, compression_level, status_code",
(
("none", None, status.HTTP_200_OK),
("none", 4, status.HTTP_400_BAD_REQUEST),
("zip", None, status.HTTP_200_OK),
("zip", 1, status.HTTP_200_OK),
("zip", 12, status.HTTP_400_BAD_REQUEST),
("bzip2", None, status.HTTP_200_OK),
("bzip2", 1, status.HTTP_200_OK),
("bzip2", 13, status.HTTP_400_BAD_REQUEST),
("lzma", None, status.HTTP_200_OK),
("lzma", 1, status.HTTP_400_BAD_REQUEST),
("zstd", None, status.HTTP_200_OK),
("zstd", 12, status.HTTP_200_OK),
("zstd", 23, status.HTTP_400_BAD_REQUEST),
)
("none", None, status.HTTP_200_OK),
("none", 4, status.HTTP_400_BAD_REQUEST),
("zip", None, status.HTTP_200_OK),
("zip", 1, status.HTTP_200_OK),
("zip", 12, status.HTTP_400_BAD_REQUEST),
("bzip2", None, status.HTTP_200_OK),
("bzip2", 1, status.HTTP_200_OK),
("bzip2", 13, status.HTTP_400_BAD_REQUEST),
("lzma", None, status.HTTP_200_OK),
("lzma", 1, status.HTTP_400_BAD_REQUEST),
("zstd", None, status.HTTP_200_OK),
("zstd", 12, status.HTTP_200_OK),
("zstd", 23, status.HTTP_400_BAD_REQUEST),
),
)
async def test_export_compression(
self,
app: FastAPI,
client: AsyncClient,
tmpdir,
project: Project,
compression: str,
compression_level: int,
status_code: int
self,
app: FastAPI,
client: AsyncClient,
tmpdir,
project: Project,
compression: str,
compression_level: int,
status_code: int,
) -> None:
project.dump = MagicMock()
os.makedirs(project.path, exist_ok=True)
topology = {
"topology": {
"nodes": [
{
"node_type": "qemu"
}
]
}
}
with open(os.path.join(project.path, "test.gns3"), 'w+') as f:
topology = {"topology": {"nodes": [{"node_type": "qemu"}]}}
with open(os.path.join(project.path, "test.gns3"), "w+") as f:
json.dump(topology, f)
params = {"compression": compression}
if compression_level:
params["compression_level"] = compression_level
response = await client.get(app.url_path_for("export_project", project_id=project.id), params=params)
assert response.status_code == status_code
if response.status_code == status.HTTP_200_OK:
assert response.headers['CONTENT-TYPE'] == 'application/gns3project'
assert response.headers['CONTENT-DISPOSITION'] == 'attachment; filename="{name}.gns3project"; filename*=UTF-8\'\'{name}.gns3project'.format(name=project.name)
with open(str(tmpdir / 'project.zip'), 'wb+') as f:
assert response.headers["CONTENT-TYPE"] == "application/gns3project"
assert response.headers[
"CONTENT-DISPOSITION"
] == "attachment; filename=\"{name}.gns3project\"; filename*=UTF-8''{name}.gns3project".format(
name=project.name
)
with open(str(tmpdir / "project.zip"), "wb+") as f:
f.write(response.content)
with zipfile_zstd.ZipFile(str(tmpdir / 'project.zip')) as myzip:
with zipfile_zstd.ZipFile(str(tmpdir / "project.zip")) as myzip:
with myzip.open("project.gns3") as myfile:
myfile.read()
async def test_get_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
os.makedirs(project.path, exist_ok=True)
with open(os.path.join(project.path, 'hello'), 'w+') as f:
f.write('world')
with open(os.path.join(project.path, "hello"), "w+") as f:
f.write("world")
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="hello"))
assert response.status_code == status.HTTP_200_OK
assert response.content == b"world"
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="false"))
assert response.status_code == status.HTTP_404_NOT_FOUND
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="../hello"))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_get_project_gns3_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
response = await client.get(app.url_path_for("get_project_gns3_file", project_id=project.id))
@@ -451,7 +381,6 @@ class TestControllerProjectRoutes:
assert topology["name"] == "test"
assert "topology" in topology
async def test_get_project_gns3_file_raw_content(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
# the endpoint must serve the raw file content, without the project being opened
@@ -470,145 +399,142 @@ class TestControllerProjectRoutes:
assert response.status_code == status.HTTP_200_OK
assert response.json() == topology
async def test_get_project_gns3_file_project_not_found(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_project_gns3_file", project_id=str(uuid.uuid4())))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_get_file_forbidden_location(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
file_path = "foo/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd"
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path=file_path))
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_write_file(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="hello"),
content=b"world")
response = await client.post(
app.url_path_for("write_file", project_id=project.id, file_path="hello"), content=b"world"
)
assert response.status_code == status.HTTP_204_NO_CONTENT
with open(os.path.join(project.path, "hello")) as f:
assert f.read() == "world"
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="../hello"))
assert response.status_code == status.HTTP_404_NOT_FOUND
async def test_write_file_forbidden_location(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
file_path = "%2e%2e/hello"
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path=file_path),
content=b"world")
response = await client.post(
app.url_path_for("write_file", project_id=project.id, file_path=file_path), content=b"world"
)
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_write_and_get_file_with_leading_slashes_in_filename(
self,
app: FastAPI,
client: AsyncClient,
project: Project) -> None:
response = await client.post(app.url_path_for("write_file", project_id=project.id, file_path="//hello"),
content=b"world")
self, app: FastAPI, client: AsyncClient, project: Project
) -> None:
response = await client.post(
app.url_path_for("write_file", project_id=project.id, file_path="//hello"), content=b"world"
)
assert response.status_code == status.HTTP_403_FORBIDDEN
response = await client.get(app.url_path_for("get_file", project_id=project.id, file_path="//hello"))
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_import(self, app: FastAPI, client: AsyncClient, tmpdir, controller: Controller) -> None:
with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip:
myzip.writestr("project.gns3", b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}')
with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), "w") as myzip:
myzip.writestr(
"project.gns3",
b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}',
)
myzip.writestr("demo", b"hello")
project_id = str(uuid.uuid4())
with open(str(tmpdir / "test.zip"), "rb") as f:
response = await client.post(app.url_path_for("import_project", project_id=project_id), content=f.read())
assert response.status_code == status.HTTP_201_CREATED
project = controller.get_project(project_id)
with open(os.path.join(project.path, "demo")) as f:
content = f.read()
assert content == "hello"
async def test_import_with_project_name(
self,
app: FastAPI,
client: AsyncClient,
tmpdir,
controller: Controller
self, app: FastAPI, client: AsyncClient, tmpdir, controller: Controller
) -> None:
with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), 'w') as myzip:
myzip.writestr("project.gns3", b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}')
with zipfile_zstd.ZipFile(str(tmpdir / "test.zip"), "w") as myzip:
myzip.writestr(
"project.gns3",
b'{"project_id": "c6992992-ac72-47dc-833b-54aa334bcd05", "version": "2.0.0", "name": "test"}',
)
myzip.writestr("demo", b"hello")
project_id = str(uuid.uuid4())
with open(str(tmpdir / "test.zip"), "rb") as f:
response = await client.post(
app.url_path_for("import_project", project_id=project_id),
content=f.read(),
params={"name": "my-imported-project-name"}
params={"name": "my-imported-project-name"},
)
assert response.status_code == status.HTTP_201_CREATED
project = controller.get_project(project_id)
assert project.name == "my-imported-project-name"
async def test_duplicate(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
response = await client.post(app.url_path_for("duplicate_project", project_id=project.id), json={"name": "hello"})
response = await client.post(
app.url_path_for("duplicate_project", project_id=project.id), json={"name": "hello"}
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == "hello"
async def test_lock_unlock(self, app: FastAPI, client: AsyncClient, project: Project, compute: Compute) -> None:
# add a drawing and node to the project
params = {
"svg": '<svg height="210" width="500"><line x1="0" y1="0" x2="200" y2="200" style="stroke:rgb(255,0,0);stroke-width:2" /></svg>',
"x": 10,
"y": 20,
"z": 0
"z": 0,
}
response = await client.post(app.url_path_for("create_drawing", project_id=project.id), json=params)
assert response.status_code == status.HTTP_201_CREATED
response = MagicMock()
response.json = {"console": 2048}
compute.post = AsyncioMagicMock(return_value=response)
response = await client.post(app.url_path_for("create_node", project_id=project.id), json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {
"startup_script": "echo test"
}
})
response = await client.post(
app.url_path_for("create_node", project_id=project.id),
json={
"name": "test",
"node_type": "vpcs",
"compute_id": "example.com",
"properties": {"startup_script": "echo test"},
},
)
assert response.status_code == status.HTTP_201_CREATED
response = await client.post(app.url_path_for("lock_project", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
for drawing in project.drawings.values():
assert drawing.locked is True
for node in project.nodes.values():
assert node.locked is True
response = await client.get(app.url_path_for("locked_project", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert response.json() is True
response = await client.post(app.url_path_for("unlock_project", project_id=project.id))
assert response.status_code == status.HTTP_204_NO_CONTENT
for drawing in project.drawings.values():
assert drawing.locked is False
for node in project.nodes.values():
+11 -51
View File
@@ -27,7 +27,6 @@ pytestmark = pytest.mark.asyncio
class TestRolesRoutes:
async def test_create_role(self, app: FastAPI, client: AsyncClient) -> None:
new_role = {"name": "role1"}
@@ -54,36 +53,22 @@ class TestRolesRoutes:
role_in_db = await rbac_repo.get_role_by_name("role1")
update_role = {"name": "role42"}
response = await client.put(
app.url_path_for("update_role", role_id=role_in_db.role_id),
json=update_role
)
response = await client.put(app.url_path_for("update_role", role_id=role_in_db.role_id), json=update_role)
assert response.status_code == status.HTTP_200_OK
updated_role_in_db = await rbac_repo.get_role(role_in_db.role_id)
assert updated_role_in_db.name == "role42"
async def test_cannot_update_builtin_user_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
update_role = {"name": "Hackers"}
response = await client.put(
app.url_path_for("update_role", role_id=role_in_db.role_id),
json=update_role
)
response = await client.put(app.url_path_for("update_role", role_id=role_in_db.role_id), json=update_role)
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_delete_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_delete_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("role42")
@@ -91,10 +76,7 @@ class TestRolesRoutes:
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_cannot_delete_builtin_administrator_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
@@ -104,13 +86,7 @@ class TestRolesRoutes:
class TestRolesPrivilegesRoutes:
async def test_add_privilege_to_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_add_privilege_to_role(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
@@ -118,38 +94,24 @@ class TestRolesPrivilegesRoutes:
response = await client.put(
app.url_path_for(
"add_privilege_to_role",
role_id=role_in_db.role_id,
privilege_id=str(privilege.privilege_id)
"add_privilege_to_role", role_id=role_in_db.role_id, privilege_id=str(privilege.privilege_id)
)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
privileges = await rbac_repo.get_role_privileges(role_in_db.role_id)
assert len(privileges) == 27 # 25 default privileges + 2 LLMConfig privileges
async def test_get_role_privileges(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_get_role_privileges(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
rbac_repo = RbacRepository(db_session)
role_in_db = await rbac_repo.get_role_by_name("User")
response = await client.get(
app.url_path_for(
"get_role_privileges",
role_id=role_in_db.role_id)
)
response = await client.get(app.url_path_for("get_role_privileges", role_id=role_in_db.role_id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 27 # 25 default privileges + 2 LLMConfig privileges
async def test_remove_privilege_from_role(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession
) -> None:
rbac_repo = RbacRepository(db_session)
@@ -158,9 +120,7 @@ class TestRolesPrivilegesRoutes:
response = await client.delete(
app.url_path_for(
"remove_privilege_from_role",
role_id=role_in_db.role_id,
privilege_id=str(privilege.privilege_id)
"remove_privilege_from_role", role_id=role_in_db.role_id, privilege_id=str(privilege.privilege_id)
),
)
assert response.status_code == status.HTTP_204_NO_CONTENT
+53 -35
View File
@@ -46,15 +46,12 @@ def stable_jwt_secret(config):
class TestSettingsRoutes:
async def test_get_settings(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_server_settings"))
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert sorted(body.keys()) == [
"Controller", "Dynamips", "IOU", "Qemu", "Server", "VPCS", "WebWireshark"
]
assert sorted(body.keys()) == ["Controller", "Dynamips", "IOU", "Qemu", "Server", "VPCS", "WebWireshark"]
# deprecated sections are not exposed
assert "VirtualBox" not in body
assert "VMware" not in body
@@ -71,7 +68,8 @@ class TestSettingsRoutes:
# underlying httpx client and its default headers depend on the fixture
# instantiation order
response = await client.get(
app.url_path_for("get_server_settings"), headers={"Authorization": "Bearer invalid_token"})
app.url_path_for("get_server_settings"), headers={"Authorization": "Bearer invalid_token"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_get_settings_forbidden(self, app: FastAPI, client: AsyncClient, test_user) -> None:
@@ -79,23 +77,28 @@ class TestSettingsRoutes:
# the "User" role has no Server.Audit privilege
token = auth_service.create_access_token(test_user.username, secret_key=DEFAULT_JWT_SECRET_KEY)
response = await client.get(
app.url_path_for("get_server_settings"), headers={"Authorization": f"Bearer {token}"})
app.url_path_for("get_server_settings"), headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_put_settings(self, app: FastAPI, client: AsyncClient, config: Config,
stable_jwt_secret: str) -> None:
async def test_put_settings(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
) -> None:
response = await client.put(app.url_path_for("update_server_settings"), json={
"Server": {
"port": 3081,
"allowed_interfaces": ["eth0"],
"default_symbol_theme": "Classic",
"report_errors": False,
response = await client.put(
app.url_path_for("update_server_settings"),
json={
"Server": {
"port": 3081,
"allowed_interfaces": ["eth0"],
"default_symbol_theme": "Classic",
"report_errors": False,
},
"Qemu": {
"enable_monitor": False,
},
},
"Qemu": {
"enable_monitor": False,
},
})
)
assert response.status_code == status.HTTP_200_OK
body = response.json()
assert body["Server"]["port"] == 3081
@@ -116,7 +119,8 @@ class TestSettingsRoutes:
assert parsed["Qemu"]["enable_monitor"] == "False"
async def test_put_settings_preserves_unknown_options(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
) -> None:
with open(config._main_config_file, "w") as f:
f.write("[Server]\nhost = 127.0.0.1\nfrobnicate = 42\n")
@@ -130,11 +134,13 @@ class TestSettingsRoutes:
assert parsed["Server"]["host"] == "127.0.0.1"
async def test_put_settings_secrets(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
) -> None:
# masked secret means "unchanged": nothing is written
response = await client.put(
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": SECRET_MASK}})
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": SECRET_MASK}}
)
assert response.status_code == status.HTTP_200_OK
parsed = configparser.ConfigParser()
parsed.read(config._main_config_file)
@@ -142,7 +148,8 @@ class TestSettingsRoutes:
# empty string means "unchanged" too
response = await client.put(
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": ""}})
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": ""}}
)
assert response.status_code == status.HTTP_200_OK
parsed = configparser.ConfigParser()
parsed.read(config._main_config_file)
@@ -150,7 +157,8 @@ class TestSettingsRoutes:
# an explicit new value is written in clear text (like a hand-edited file)
response = await client.put(
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": "secret123"}})
app.url_path_for("update_server_settings"), json={"Server": {"compute_password": "secret123"}}
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["Server"]["compute_password"] == SECRET_MASK # masked in the response
parsed = configparser.ConfigParser()
@@ -158,7 +166,8 @@ class TestSettingsRoutes:
assert parsed["Server"]["compute_password"] == "secret123"
async def test_put_settings_null_removes_option(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
) -> None:
with open(config._main_config_file, "w") as f:
f.write("[Server]\nhost = 127.0.0.1\n")
@@ -172,7 +181,8 @@ class TestSettingsRoutes:
assert response.json()["Server"]["host"] == "0.0.0.0" # default restored
async def test_put_settings_validation_failure(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str
) -> None:
with open(config._main_config_file, "w") as f:
f.write("[Server]\nhost = 127.0.0.1\n")
@@ -180,22 +190,25 @@ class TestSettingsRoutes:
content_before = f.read()
# cross-field violation: console_end_port_range must be > console_start_port_range
response = await client.put(app.url_path_for("update_server_settings"), json={
"Server": {"console_start_port_range": 10000, "console_end_port_range": 5000}})
response = await client.put(
app.url_path_for("update_server_settings"),
json={"Server": {"console_start_port_range": 10000, "console_end_port_range": 5000}},
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
with open(config._main_config_file) as f:
assert f.read() == content_before
async def test_put_settings_unknown_option_rejected(
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str
) -> None:
response = await client.put(
app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}})
response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"prot": "http"}})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
async def test_put_settings_deprecated_sections_rejected(
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str
) -> None:
response = await client.put(app.url_path_for("update_server_settings"), json={"VirtualBox": {}})
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
@@ -203,14 +216,17 @@ class TestSettingsRoutes:
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
async def test_put_settings_jwt_secret_key_rejected(
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str) -> None:
self, app: FastAPI, client: AsyncClient, stable_jwt_secret: str
) -> None:
response = await client.put(
app.url_path_for("update_server_settings"), json={"Controller": {"jwt_secret_key": "nope"}})
app.url_path_for("update_server_settings"), json={"Controller": {"jwt_secret_key": "nope"}}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT
async def test_put_settings_conflict(
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str, tmpdir) -> None:
self, app: FastAPI, client: AsyncClient, config: Config, stable_jwt_secret: str, tmpdir
) -> None:
override_path = str(tmpdir / "override.conf")
with open(override_path, "w") as f:
@@ -218,7 +234,9 @@ class TestSettingsRoutes:
# a later configuration file takes precedence over the main one
Config.instance()._files.append(override_path)
response = await client.put(app.url_path_for("update_server_settings"), json={"Server": {"host": "192.168.1.1"}})
response = await client.put(
app.url_path_for("update_server_settings"), json={"Server": {"host": "192.168.1.1"}}
)
assert response.status_code == status.HTTP_409_CONFLICT
async def test_put_settings_empty_body(self, app: FastAPI, client: AsyncClient, config: Config) -> None:
+26 -22
View File
@@ -31,48 +31,52 @@ pytestmark = pytest.mark.asyncio
class TestSnapshotRoutes:
@pytest_asyncio.fixture
async def project(self, app: FastAPI, client: AsyncClient, controller: Controller) -> Project:
u = str(uuid.uuid4())
params = {"name": "test", "project_id": u}
await client.post(app.url_path_for("create_project"), json=params)
controller_project = controller.get_project(u)
return controller_project
@pytest_asyncio.fixture
async def snapshot(self, project: Project):
controller_snapshot = await project.snapshot("test")
return controller_snapshot
async def test_list_snapshots(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None:
async def test_list_snapshots(
self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot
) -> None:
assert snapshot.name == "test"
response = await client.get(app.url_path_for("get_snapshots", project_id=project.id))
assert response.status_code == status.HTTP_200_OK
assert len(response.json()) == 1
async def test_delete_snapshot(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None:
response = await client.delete(app.url_path_for("delete_snapshot", project_id=project.id, snapshot_id=snapshot.id))
async def test_delete_snapshot(
self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot
) -> None:
response = await client.delete(
app.url_path_for("delete_snapshot", project_id=project.id, snapshot_id=snapshot.id)
)
assert response.status_code == status.HTTP_204_NO_CONTENT
assert not os.path.exists(snapshot.path)
async def test_restore_snapshot(self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot) -> None:
response = await client.post(app.url_path_for("restore_snapshot", project_id=project.id, snapshot_id=snapshot.id))
async def test_restore_snapshot(
self, app: FastAPI, client: AsyncClient, project: Project, snapshot: Snapshot
) -> None:
response = await client.post(
app.url_path_for("restore_snapshot", project_id=project.id, snapshot_id=snapshot.id)
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["name"] == project.name
async def test_create_snapshot(self, app: FastAPI, client: AsyncClient, project: Project) -> None:
response = await client.post(app.url_path_for("create_snapshot", project_id=project.id), json={"name": "snap1"})
assert response.status_code == status.HTTP_201_CREATED
assert len(os.listdir(os.path.join(project.path, "snapshots"))) == 2
+17 -20
View File
@@ -29,42 +29,39 @@ pytestmark = pytest.mark.asyncio
class TestSymbolRoutes:
async def test_symbols(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_symbols"))
assert response.status_code == status.HTTP_200_OK
assert {
'symbol_id': ':/symbols/classic/firewall.svg',
'filename': 'firewall.svg',
'builtin': True,
'theme': 'Classic'
"symbol_id": ":/symbols/classic/firewall.svg",
"filename": "firewall.svg",
"builtin": True,
"theme": "Classic",
} in response.json()
async def test_get(self, app: FastAPI, client: AsyncClient, controller: Controller) -> None:
controller.symbols.theme = "Classic"
url = app.url_path_for("get_symbol", symbol_id=urllib.parse.quote(':/symbols/classic/firewall.svg'))
url = app.url_path_for("get_symbol", symbol_id=urllib.parse.quote(":/symbols/classic/firewall.svg"))
response = await client.get(url)
assert response.status_code == status.HTTP_200_OK
assert response.headers['CONTENT-TYPE'] == 'image/svg+xml'
assert response.headers['CONTENT-LENGTH'] == '9381'
assert '</svg>' in response.text
assert response.headers["CONTENT-TYPE"] == "image/svg+xml"
assert response.headers["CONTENT-LENGTH"] == "9381"
assert "</svg>" in response.text
# Reply with the default symbol
response = await client.get(app.url_path_for("get_symbol", symbol_id="404.png"))
assert response.status_code == status.HTTP_200_OK
async def test_upload(self, app: FastAPI, client: AsyncClient, symbols_dir: str) -> None:
response = await client.post(app.url_path_for("upload_symbol", symbol_id="test2"), content=b"TEST")
assert response.status_code == status.HTTP_204_NO_CONTENT
with open(os.path.join(symbols_dir, "test2")) as f:
assert f.read() == "TEST"
response = await client.get(app.url_path_for("get_symbol", symbol_id="test2"))
assert response.status_code == status.HTTP_200_OK
File diff suppressed because it is too large. Load diff
+142 -205
View File
@@ -35,7 +35,6 @@ pytestmark = pytest.mark.asyncio
class TestUserRoutes:
async def test_route_exist(self, app: FastAPI, client: AsyncClient) -> None:
new_user = {"username": "user1", "email": "user1@email.com", "password": "test_password"}
@@ -43,10 +42,7 @@ class TestUserRoutes:
assert response.status_code == status.HTTP_201_CREATED
async def test_users_can_register_successfully(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession
) -> None:
user_repo = UsersRepository(db_session)
@@ -73,21 +69,21 @@ class TestUserRoutes:
@pytest.mark.parametrize(
"attr, value, status_code",
(
("email", "user2@email.com", status.HTTP_400_BAD_REQUEST),
("username", "user2", status.HTTP_400_BAD_REQUEST),
("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT),
("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT),
)
("email", "user2@email.com", status.HTTP_400_BAD_REQUEST),
("username", "user2", status.HTTP_400_BAD_REQUEST),
("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT),
("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT),
),
)
async def test_user_registration_fails_when_credentials_are_taken(
self,
app: FastAPI,
client: AsyncClient,
attr: str,
value: str,
status_code: int,
self,
app: FastAPI,
client: AsyncClient,
attr: str,
value: str,
status_code: int,
) -> None:
new_user = {"email": "not_taken@email.com", "username": "not_taken_username", "password": "test_password"}
@@ -98,26 +94,26 @@ class TestUserRoutes:
@pytest.mark.parametrize(
"attr, value, status_code",
(
("email", "user@email.com", status.HTTP_200_OK),
("email", "user@email.com", status.HTTP_400_BAD_REQUEST),
("username", "user2", status.HTTP_400_BAD_REQUEST),
("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT),
("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT),
("full_name", "John Doe", status.HTTP_200_OK),
("password", "password123", status.HTTP_200_OK),
("is_active", True, status.HTTP_200_OK),
)
("email", "user@email.com", status.HTTP_200_OK),
("email", "user@email.com", status.HTTP_400_BAD_REQUEST),
("username", "user2", status.HTTP_400_BAD_REQUEST),
("email", "invalid_email@one@two.io", status.HTTP_422_UNPROCESSABLE_CONTENT),
("password", "short", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "user2@#$%^<>", status.HTTP_422_UNPROCESSABLE_CONTENT),
("username", "ab", status.HTTP_422_UNPROCESSABLE_CONTENT),
("full_name", "John Doe", status.HTTP_200_OK),
("password", "password123", status.HTTP_200_OK),
("is_active", True, status.HTTP_200_OK),
),
)
async def test_update_user(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
attr: str,
value: str,
status_code: int,
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession,
attr: str,
value: str,
status_code: int,
) -> None:
user_repo = UsersRepository(db_session)
@@ -128,10 +124,7 @@ class TestUserRoutes:
assert response.status_code == status_code
async def test_users_saved_password_is_hashed(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, client: AsyncClient, db_session: AsyncSession
) -> None:
user_repo = UsersRepository(db_session)
@@ -156,13 +149,8 @@ class TestUserRoutes:
class TestAuthTokens:
async def test_can_create_token_successfully(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
config: Config
self, app: FastAPI, client: AsyncClient, test_user: User, config: Config
) -> None:
jwt_secret = config.settings.Controller.jwt_secret_key
@@ -173,11 +161,7 @@ class TestAuthTokens:
assert username == test_user.username
async def test_decode_token_with_wrong_algorithm(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
config: Config
self, app: FastAPI, client: AsyncClient, test_user: User, config: Config
) -> None:
jwt_secret = config.settings.Controller.jwt_secret_key
@@ -185,12 +169,7 @@ class TestAuthTokens:
with pytest.raises(ValueError):
jwt.decode(token, jwt_secret, algorithms=["HS256"])
async def test_can_retrieve_username_from_token(
self,
app: FastAPI,
client: AsyncClient,
test_user: User
) -> None:
async def test_can_retrieve_username_from_token(self, app: FastAPI, client: AsyncClient, test_user: User) -> None:
token = auth_service.create_access_token(test_user.username)
username = auth_service.get_username_from_token(token)
@@ -199,19 +178,19 @@ class TestAuthTokens:
@pytest.mark.parametrize(
"wrong_secret, wrong_token",
(
("use correct secret", "asdf"), # use wrong token
("use correct secret", ""), # use wrong token
(OctKey.generate_key().as_dict()['k'], "use correct token"), # use wrong secret
("use correct secret", "asdf"), # use wrong token
("use correct secret", ""), # use wrong token
(OctKey.generate_key().as_dict()["k"], "use correct token"), # use wrong secret
),
)
async def test_error_when_token_or_secret_is_wrong(
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
wrong_secret: str,
wrong_token: Optional[str],
config,
self,
app: FastAPI,
client: AsyncClient,
test_user: User,
wrong_secret: str,
wrong_token: Optional[str],
config,
) -> None:
token = auth_service.create_access_token(test_user.username)
@@ -224,13 +203,8 @@ class TestAuthTokens:
class TestUserLogin:
async def test_user_can_login_successfully_and_receives_valid_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
config: Config
self, app: FastAPI, unauthorized_client: AsyncClient, test_user: User, config: Config
) -> None:
jwt_secret = config.settings.Controller.jwt_secret_key
@@ -290,22 +264,18 @@ class TestUserLogin:
class TestUnauthorizedUser:
async def test_user_cannot_access_own_data_if_not_authenticated(
self, app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
response = await unauthorized_client.get(app.url_path_for("get_logged_in_user"))
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_user_can_authenticate_using_json(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
config: Config
self, app: FastAPI, unauthorized_client: AsyncClient, test_user: User, config: Config
) -> None:
credentials = {
@@ -321,13 +291,13 @@ class TestUnauthorizedUser:
response = await unauthorized_client.get(app.url_path_for("statistics"), params={"token": token})
assert response.status_code == status.HTTP_200_OK
class TestUserMe:
class TestUserMe:
async def test_authenticated_user_can_retrieve_own_data(
self,
app: FastAPI,
authorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
authorized_client: AsyncClient,
test_user: User,
) -> None:
response = await authorized_client.get(app.url_path_for("get_logged_in_user"))
@@ -338,10 +308,10 @@ class TestUserMe:
assert user.user_id == test_user.user_id
async def test_authenticated_user_can_update_own_data(
self,
app: FastAPI,
authorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
authorized_client: AsyncClient,
test_user: User,
) -> None:
response = await authorized_client.get(app.url_path_for("get_logged_in_user"))
@@ -355,37 +325,33 @@ class TestUserMe:
@pytest.mark.parametrize(
"attr, value, status_code",
(
("email", "user42@email.com", status.HTTP_200_OK),
("email", "user42@email.com", status.HTTP_400_BAD_REQUEST),
("full_name", "John Doe", status.HTTP_200_OK),
("password", "password123", status.HTTP_200_OK),
)
("email", "user42@email.com", status.HTTP_200_OK),
("email", "user42@email.com", status.HTTP_400_BAD_REQUEST),
("full_name", "John Doe", status.HTTP_200_OK),
("password", "password123", status.HTTP_200_OK),
),
)
async def test_authenticated_user_can_update_own_data(
self,
app: FastAPI,
authorized_client: AsyncClient,
attr: str,
value: str,
status_code: int,
self,
app: FastAPI,
authorized_client: AsyncClient,
attr: str,
value: str,
status_code: int,
) -> None:
update_user = {}
update_user[attr] = value
response = await authorized_client.put(
app.url_path_for("update_logged_in_user"),
json=update_user
)
response = await authorized_client.put(app.url_path_for("update_logged_in_user"), json=update_user)
assert response.status_code == status_code
class TestLogout:
async def test_logout_returns_no_content(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# login to get a fresh token that includes token_version
@@ -395,17 +361,16 @@ class TestLogout:
token = response.json()["access_token"]
response = await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {token}"}
app.url_path_for("logout"), headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_204_NO_CONTENT
async def test_token_is_rejected_after_logout(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
) -> None:
# login and get a token
@@ -415,24 +380,20 @@ class TestLogout:
token = response.json()["access_token"]
# logout — increments token_version
await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {token}"}
)
await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {token}"})
# old token must now be rejected
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {token}"}
app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {token}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
assert response.json()["message"] == f"Token has been revoked for '{test_user.username}'"
async def test_new_token_works_after_logout_and_relogin(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
credentials = {"username": test_user.username, "password": "user1_password"}
@@ -440,10 +401,7 @@ class TestLogout:
# login, then logout
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
old_token = response.json()["access_token"]
await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {old_token}"}
)
await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {old_token}"})
# login again to get a fresh token
response = await unauthorized_client.post(app.url_path_for("authenticate"), json=credentials)
@@ -452,18 +410,17 @@ class TestLogout:
# new token must work
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {new_token}"}
app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {new_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert response.json()["username"] == test_user.username
async def test_stale_version_token_is_rejected(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
db_session: AsyncSession,
) -> None:
# craft a token with ver=0 while the user's token_version is already higher
@@ -475,15 +432,14 @@ class TestLogout:
stale_token = auth_service.create_access_token(test_user.username, token_version=0)
response = await unauthorized_client.get(
app.url_path_for("get_logged_in_user"),
headers={"Authorization": f"Bearer {stale_token}"}
app.url_path_for("get_logged_in_user"), headers={"Authorization": f"Bearer {stale_token}"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_logout_without_token_returns_unauthorized(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
) -> None:
response = await unauthorized_client.post(app.url_path_for("logout"))
@@ -491,27 +447,26 @@ class TestLogout:
class TestRefreshToken:
async def test_login_returns_refresh_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
credentials = {"username": test_user.username, "password": "user1_password"}
response = await unauthorized_client.post(app.url_path_for("login"),
data=credentials,
headers={"content-type": "application/x-www-form-urlencoded"})
response = await unauthorized_client.post(
app.url_path_for("login"), data=credentials, headers={"content-type": "application/x-www-form-urlencoded"}
)
assert response.status_code == status.HTTP_200_OK
assert "refresh_token" in response.json()
assert response.json().get("refresh_token") is not None
async def test_authenticate_returns_refresh_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
credentials = {"username": test_user.username, "password": "user1_password"}
@@ -521,10 +476,10 @@ class TestRefreshToken:
assert response.json().get("refresh_token") is not None
async def test_refresh_endpoint_returns_new_tokens(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# authenticate to get a refresh token
@@ -544,10 +499,10 @@ class TestRefreshToken:
assert response.json().get("refresh_token") is not None
async def test_new_access_token_from_refresh_works_on_protected_route(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
credentials = {"username": test_user.username, "password": "user1_password"}
@@ -570,10 +525,10 @@ class TestRefreshToken:
assert response.json()["username"] == test_user.username
async def test_refresh_with_stale_token_after_logout(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# authenticate and get a refresh token
@@ -583,10 +538,7 @@ class TestRefreshToken:
# logout — bumps token_version, invalidating the refresh token
access_token = auth_response.json()["access_token"]
await unauthorized_client.post(
app.url_path_for("logout"),
headers={"Authorization": f"Bearer {access_token}"}
)
await unauthorized_client.post(app.url_path_for("logout"), headers={"Authorization": f"Bearer {access_token}"})
# /refresh must now reject the stale refresh token
response = await unauthorized_client.post(
@@ -596,10 +548,10 @@ class TestRefreshToken:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_refresh_rejects_access_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# an access token presented at /refresh must be rejected
@@ -614,10 +566,10 @@ class TestRefreshToken:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_refresh_rejects_expired_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# a refresh token with an already-expired timestamp
@@ -630,9 +582,9 @@ class TestRefreshToken:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_refresh_rejects_invalid_token(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
) -> None:
response = await unauthorized_client.post(
@@ -642,10 +594,10 @@ class TestRefreshToken:
assert response.status_code == status.HTTP_401_UNAUTHORIZED
async def test_refresh_token_rejected_as_bearer(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
self,
app: FastAPI,
unauthorized_client: AsyncClient,
test_user: User,
) -> None:
# a refresh token used as a bearer access token must be rejected
@@ -661,25 +613,14 @@ class TestRefreshToken:
class TestSuperAdmin:
async def test_super_admin_exists(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_super_admin_exists(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
admin_in_db = await user_repo.get_user_by_username("admin")
assert admin_in_db is not None
assert auth_service.verify_password("admin", admin_in_db.hashed_password)
async def test_cannot_delete_super_admin(
self,
app: FastAPI,
client: AsyncClient,
db_session: AsyncSession
) -> None:
async def test_cannot_delete_super_admin(self, app: FastAPI, client: AsyncClient, db_session: AsyncSession) -> None:
user_repo = UsersRepository(db_session)
admin_in_db = await user_repo.get_user_by_username("admin")
@@ -687,10 +628,7 @@ class TestSuperAdmin:
assert response.status_code == status.HTTP_403_FORBIDDEN
async def test_admin_can_login_after_password_recovery(
self,
app: FastAPI,
unauthorized_client: AsyncClient,
db_session: AsyncSession
self, app: FastAPI, unauthorized_client: AsyncClient, db_session: AsyncSession
) -> None:
# set the admin password to null in the database
@@ -705,4 +643,3 @@ class TestSuperAdmin:
}
response = await unauthorized_client.post(app.url_path_for("login"), data=login_data)
assert response.status_code == status.HTTP_200_OK
+16 -20
View File
@@ -26,40 +26,36 @@ pytestmark = pytest.mark.asyncio
class TestVersionRoutes:
async def test_version_output(self, app: FastAPI, client: AsyncClient) -> None:
response = await client.get(app.url_path_for("get_version"))
assert response.status_code == status.HTTP_200_OK
assert response.json() == {'controller_host': '127.0.0.1', 'local': False, 'version': __version__}
assert response.json() == {"controller_host": "127.0.0.1", "local": False, "version": __version__}
async def test_version_input(self, app: FastAPI, client: AsyncClient) -> None:
params = {'version': __version__}
params = {"version": __version__}
response = await client.post(app.url_path_for("check_version"), json=params)
assert response.status_code == status.HTTP_200_OK
assert response.json() == {'version': __version__}
assert response.json() == {"version": __version__}
async def test_version_invalid_input(self, app: FastAPI, client: AsyncClient) -> None:
params = {'version': "0.4.2"}
params = {"version": "0.4.2"}
response = await client.post(app.url_path_for("check_version"), json=params)
assert response.status_code == status.HTTP_409_CONFLICT
assert response.json() == {'message': 'Client version 0.4.2 is not the same as server version {}'.format(__version__)}
assert response.json() == {
"message": "Client version 0.4.2 is not the same as server version {}".format(__version__)
}
async def test_version_invalid_input_schema(self, app: FastAPI, client: AsyncClient) -> None:
params = {'version': "0.4.2", "bla": "blu"}
params = {"version": "0.4.2", "bla": "blu"}
response = await client.post(app.url_path_for("check_version"), json=params)
assert response.status_code == status.HTTP_409_CONFLICT
async def test_version_invalid_json(self, app: FastAPI, client: AsyncClient) -> None:
params = "BOUM"
response = await client.post(app.url_path_for("check_version"), json=params)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_CONTENT