mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
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:
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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
)
|
||||
@@ -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
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
Reference in new issue
Block a user