From b74ebafb8feb948534c48574717e1283c3a55065 Mon Sep 17 00:00:00 2001 From: Volobue Date: Mon, 18 May 2026 21:49:01 +0300 Subject: [PATCH 1/5] api endpoint implemented --- .../controller_get_templateidbaseconfig.txt | 17 ++++ .../controller_get_templatesbaseconfigs.txt | 44 +++++++++ .../controller_put_templateidbaseconfig.txt | 17 ++++ .../template/templatesbaseconfigs.rst | 38 ++++++++ .../templatestemplateidbaseconfigfilename.rst | 90 +++++++++++++++++++ gns3server/api/routes/controller/templates.py | 56 +++++++++++- gns3server/db/repositories/templates.py | 3 + gns3server/services/templates.py | 45 ++++++++++ tests/api/routes/controller/test_templates.py | 74 +++++++++++++++ 9 files changed, 383 insertions(+), 1 deletion(-) create mode 100644 docs/api/examples/controller_get_templateidbaseconfig.txt create mode 100644 docs/api/examples/controller_get_templatesbaseconfigs.txt create mode 100644 docs/api/examples/controller_put_templateidbaseconfig.txt create mode 100644 docs/api/v2/controller/template/templatesbaseconfigs.rst create mode 100644 docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst diff --git a/docs/api/examples/controller_get_templateidbaseconfig.txt b/docs/api/examples/controller_get_templateidbaseconfig.txt new file mode 100644 index 000000000..9e3d9c25c --- /dev/null +++ b/docs/api/examples/controller_get_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +GET /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:34:10 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/examples/controller_get_templatesbaseconfigs.txt b/docs/api/examples/controller_get_templatesbaseconfigs.txt new file mode 100644 index 000000000..263cbfa1f --- /dev/null +++ b/docs/api/examples/controller_get_templatesbaseconfigs.txt @@ -0,0 +1,44 @@ +curl -i -X 'http://localhost:3080/v2/templates/base-configs' + +GET /v2/templates/base-configs HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/base-configs +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 576 +Date: Mon, 18 May 2026 18:28:53 GMT + +[ + { + "filename": "config1.txt" + }, + { + "filename": "config2.txt" + }, + { + "filename": "ios_base_startup-config.txt" + }, + { + "filename": "ios_etherswitch_startup-config.txt" + }, + { + "filename": "iou_l2_base_startup-config.txt" + }, + { + "filename": "iou_l3_base_startup-config.txt" + }, + { + "filename": "test_config.txt" + }, + { + "filename": "update_test.txt" + }, + { + "filename": "vpcs_base_config.txt" + }, + { + "filename": "vpcs_base_config2.txt" + } +] \ No newline at end of file diff --git a/docs/api/examples/controller_put_templateidbaseconfig.txt b/docs/api/examples/controller_put_templateidbaseconfig.txt new file mode 100644 index 000000000..c70e315b8 --- /dev/null +++ b/docs/api/examples/controller_put_templateidbaseconfig.txt @@ -0,0 +1,17 @@ +curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' + +PUT /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 + +HTTP/1.1 200 OK +Connection: close +X-Route: /v2/templates/{template_id}/base-config/{filename} +Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f +Content-Type: application/json +Content-Length: 144 +Date: Mon, 18 May 2026 18:37:57 GMT + +{ + "content": "# test config\n\ndhcp\n", + "filename": "vpcs_base_config.txt", + "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" +} \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatesbaseconfigs.rst b/docs/api/v2/controller/template/templatesbaseconfigs.rst new file mode 100644 index 000000000..dc4de3e8c --- /dev/null +++ b/docs/api/v2/controller/template/templatesbaseconfigs.rst @@ -0,0 +1,38 @@ +GET /v2/templates/base-configs +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/base-configs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst new file mode 100644 index 000000000..98b356270 --- /dev/null +++ b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst @@ -0,0 +1,90 @@ +/v2/templates/{template_id}/base-config/{filename} +------------------------------------------------------------------------------------------------------------------------------------------ + +.. contents:: + +GET /v2/templates/**{template_id}**/base-configs/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +List all available base configuration files + +Response status codes +********************** +- **200**: List of base configuration files returned + +Output +******* + +.. raw:: html + + + + + + + + + + + + + + +
NameTypeDescription
filenamestringName of the base configuration file
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_get_templateidbaseconfig.txt + + +PUT /v2/templates/**{template_id}**/base-config/**{filename}** +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Update base configuration file content + +Response status codes +********************** +- **200**: File updated +- **404**: File not found + +Input +******* + +.. raw:: html + + + + + + + + + + + +
NameMandatoryTypeDescription
contentstringNew file content
+ +Output +******* + +.. raw:: html + + + + + + + + + + + + + +
NameMandatoryTypeDescription
template_idstringTemplate UUID
filenamestringBase configuration filename
contentstringUpdated file content
+ +Sample session +*************** + +.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt \ No newline at end of file diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index a8d0be8a3..d991ac086 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -35,7 +35,7 @@ from gns3server.db.repositories.templates import TemplatesRepository from gns3server.services.templates import TemplatesService from gns3server.db.repositories.rbac import RbacRepository from gns3server.db.repositories.images import ImagesRepository -from gns3server.controller.controller_error import ControllerError +from gns3server.controller.controller_error import ControllerError, ControllerBadRequestError from gns3server.utils.images import get_builtin_disks from .dependencies.authentication import get_current_active_user @@ -230,3 +230,57 @@ async def duplicate_template( template = await TemplatesService(templates_repo).duplicate_template(template_id) return template + +@router.get("/{template_id}/base-config/{filename}") +async def get_base_config( + template_id: UUID, + filename: str, + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), +): + service = TemplatesService(templates_repo) + + await service.get_template(template_id) + + content = service.get_file(str(template_id), filename) + + return { + "template_id": str(template_id), + "filename": os.path.basename(filename), + "content": content + } + + +@router.put("/{template_id}/base-config/{filename}") +async def update_base_config( + template_id: UUID, + filename: str, + body: dict, + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), +): + if not body or "content" not in body: + raise ControllerBadRequestError("Missing 'content' field") + + service = TemplatesService(templates_repo) + + await service.get_template(template_id) + + service.update_file(str(template_id), filename, body["content"]) + + return { + "template_id": str(template_id), + "filename": os.path.basename(filename), + "content": body["content"] + } + + +@router.get("/{template_id}/base-configs") +async def list_base_configs( + template_id: UUID, + templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), +): + service = TemplatesService(templates_repo) + + await service.get_template(template_id) + + return service.list_files(str(template_id)) + diff --git a/gns3server/db/repositories/templates.py b/gns3server/db/repositories/templates.py index ec8215af8..ed835a37a 100644 --- a/gns3server/db/repositories/templates.py +++ b/gns3server/db/repositories/templates.py @@ -49,6 +49,9 @@ class TemplatesRepository(BaseRepository): super().__init__(db_session) + def configs_path(self) -> str: + return os.path.join(os.getcwd(), "configs") + async def get_template(self, template_id: UUID) -> Union[None, models.Template]: query = select(models.Template).\ diff --git a/gns3server/services/templates.py b/gns3server/services/templates.py index 92b78633e..5284e2324 100644 --- a/gns3server/services/templates.py +++ b/gns3server/services/templates.py @@ -174,6 +174,9 @@ class TemplatesService: if builtin_template["template_id"] == template_id: return jsonable_encoder(builtin_template) + def _base_path(self): + return self._templates_repo.configs_path() + async def get_templates(self) -> List[dict]: templates = [] @@ -336,3 +339,45 @@ class TemplatesService: self._controller.notification.controller_emit("template.deleted", {"template_id": str(template_id)}) else: raise ControllerNotFoundError(f"Template '{template_id}' not found") + + def _template_path(self, template_id: str) -> str: + return os.path.join(self._base_path(), str(template_id)) + + def list_files(self, template_id: str): + path = self._template_path(template_id) + + if not os.path.exists(path): + return [] + + return [ + {"filename": f} + for f in sorted(os.listdir(path)) + if os.path.isfile(os.path.join(path, f)) + ] + + def get_file(self, template_id: str, filename: str): + safe_filename = os.path.basename(filename) + path = os.path.join(self._template_path(template_id), safe_filename) + + if not os.path.isfile(path): + raise ControllerNotFoundError(f"File '{safe_filename}' not found") + + try: + with open(path, encoding="utf-8", errors="ignore") as f: + return f.read() + except OSError as e: + raise ControllerError(str(e)) + + def update_file(self, template_id: str, filename: str, content: str): + safe_filename = os.path.basename(filename) + + dir_path = self._template_path(template_id) + path = os.path.join(dir_path, safe_filename) + + os.makedirs(dir_path, exist_ok=True) + + try: + with open(path, "w", encoding="utf-8") as f: + f.write(content) + except OSError as e: + raise ControllerError(str(e)) \ No newline at end of file diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py index 3ae7e9383..0c61b3ab0 100644 --- a/tests/api/routes/controller/test_templates.py +++ b/tests/api/routes/controller/test_templates.py @@ -239,6 +239,80 @@ class TestTemplateRoutes: # mock.assert_called_with(id, x=42, y=12, compute_id=None) # assert response.status_code == status.HTTP_201_CREATED + async def test_get_base_config(self, app: FastAPI, client: AsyncClient): + + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": "TEST", + "compute_id": "local", + "template_type": "vpcs" + }) + + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] + + #создаём файл перед чтением + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), + json={"content": "hello"} + ) + + response = await client.get( + app.url_path_for( + "get_base_config", + template_id=template_id, + filename="test.txt" + ) + ) + + assert response.status_code == 200 + assert response.json()["content"] == "hello" + + async def test_update_base_config(self, app: FastAPI, client: AsyncClient): + template_id = str(uuid.uuid4()) + filename = "test.txt" + + await client.post(app.url_path_for("create_template"), json={ + "template_id": template_id, + "name": "TEST", + "compute_id": "local", + "template_type": "vpcs" + }) + + payload = {"content": "hello world"} + + response = await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename=filename), + json=payload + ) + + assert response.status_code == 200 + assert response.json()["content"] == "hello world" + + async def test_update_base_config_missing_content(self, app: FastAPI, client: AsyncClient): + template_id = str(uuid.uuid4()) + filename = "test.txt" + + await client.post(app.url_path_for("create_template"), json={ + "template_id": template_id, + "name": "TEST", + "compute_id": "local", + "template_type": "vpcs" + }) + + response = await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename=filename), + json={} + ) + + assert response.status_code in (400, 422) + + async def test_base_config_template_not_found(self, app: FastAPI, client: AsyncClient): + response = await client.get( + app.url_path_for("get_base_config", template_id=str(uuid.uuid4()), filename="x.txt") + ) + + assert response.status_code == 404 + class TestDuplicateTemplates: From 01511f3aef3736d83ca4d6cd0ccb62eefc1f5aa8 Mon Sep 17 00:00:00 2001 From: Volobue Date: Mon, 25 May 2026 21:37:04 +0300 Subject: [PATCH 2/5] added endpoint to v3 branch --- .../controller_get_templateidbaseconfig.txt | 17 ---- .../controller_get_templatesbaseconfigs.txt | 44 --------- .../controller_put_templateidbaseconfig.txt | 17 ---- .../template/templatesbaseconfigs.rst | 38 -------- .../templatestemplateidbaseconfigfilename.rst | 90 ------------------ tests/api/routes/controller/test_templates.py | 93 ++++++++++++++++--- 6 files changed, 79 insertions(+), 220 deletions(-) delete mode 100644 docs/api/examples/controller_get_templateidbaseconfig.txt delete mode 100644 docs/api/examples/controller_get_templatesbaseconfigs.txt delete mode 100644 docs/api/examples/controller_put_templateidbaseconfig.txt delete mode 100644 docs/api/v2/controller/template/templatesbaseconfigs.rst delete mode 100644 docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst diff --git a/docs/api/examples/controller_get_templateidbaseconfig.txt b/docs/api/examples/controller_get_templateidbaseconfig.txt deleted file mode 100644 index 9e3d9c25c..000000000 --- a/docs/api/examples/controller_get_templateidbaseconfig.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' - -GET /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 - -HTTP/1.1 200 OK -Connection: close -X-Route: /v2/templates/{template_id}/base-config/{filename} -Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f -Content-Type: application/json -Content-Length: 144 -Date: Mon, 18 May 2026 18:34:10 GMT - -{ - "content": "# test config\n\ndhcp\n", - "filename": "vpcs_base_config.txt", - "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" -} \ No newline at end of file diff --git a/docs/api/examples/controller_get_templatesbaseconfigs.txt b/docs/api/examples/controller_get_templatesbaseconfigs.txt deleted file mode 100644 index 263cbfa1f..000000000 --- a/docs/api/examples/controller_get_templatesbaseconfigs.txt +++ /dev/null @@ -1,44 +0,0 @@ -curl -i -X 'http://localhost:3080/v2/templates/base-configs' - -GET /v2/templates/base-configs HTTP/1.1 - -HTTP/1.1 200 OK -Connection: close -X-Route: /v2/templates/base-configs -Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f -Content-Type: application/json -Content-Length: 576 -Date: Mon, 18 May 2026 18:28:53 GMT - -[ - { - "filename": "config1.txt" - }, - { - "filename": "config2.txt" - }, - { - "filename": "ios_base_startup-config.txt" - }, - { - "filename": "ios_etherswitch_startup-config.txt" - }, - { - "filename": "iou_l2_base_startup-config.txt" - }, - { - "filename": "iou_l3_base_startup-config.txt" - }, - { - "filename": "test_config.txt" - }, - { - "filename": "update_test.txt" - }, - { - "filename": "vpcs_base_config.txt" - }, - { - "filename": "vpcs_base_config2.txt" - } -] \ No newline at end of file diff --git a/docs/api/examples/controller_put_templateidbaseconfig.txt b/docs/api/examples/controller_put_templateidbaseconfig.txt deleted file mode 100644 index c70e315b8..000000000 --- a/docs/api/examples/controller_put_templateidbaseconfig.txt +++ /dev/null @@ -1,17 +0,0 @@ -curl -i -X 'http://localhost:3080/v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt' - -PUT /v2/templates/19021f99-e36f-394d-b4a1-8aaa902ab9cc/base-config/vpcs_base_config.txt HTTP/1.1 - -HTTP/1.1 200 OK -Connection: close -X-Route: /v2/templates/{template_id}/base-config/{filename} -Server: Python/3.14 GNS3/2.2.60.dev1+713dbb7f -Content-Type: application/json -Content-Length: 144 -Date: Mon, 18 May 2026 18:37:57 GMT - -{ - "content": "# test config\n\ndhcp\n", - "filename": "vpcs_base_config.txt", - "template_id": "19021f99-e36f-394d-b4a1-8aaa902ab9cc" -} \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatesbaseconfigs.rst b/docs/api/v2/controller/template/templatesbaseconfigs.rst deleted file mode 100644 index dc4de3e8c..000000000 --- a/docs/api/v2/controller/template/templatesbaseconfigs.rst +++ /dev/null @@ -1,38 +0,0 @@ -GET /v2/templates/base-configs ------------------------------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -GET /v2/templates/base-configs -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -List all available base configuration files - -Response status codes -********************** -- **200**: List of base configuration files returned - -Output -******* - -.. raw:: html - - - - - - - - - - - - - - -
NameTypeDescription
filenamestringName of the base configuration file
- -Sample session -*************** - -.. literalinclude:: ../../../examples/controller_get_templatesbaseconfigs.txt \ No newline at end of file diff --git a/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst b/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst deleted file mode 100644 index 98b356270..000000000 --- a/docs/api/v2/controller/template/templatestemplateidbaseconfigfilename.rst +++ /dev/null @@ -1,90 +0,0 @@ -/v2/templates/{template_id}/base-config/{filename} ------------------------------------------------------------------------------------------------------------------------------------------- - -.. contents:: - -GET /v2/templates/**{template_id}**/base-configs/**{filename}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -List all available base configuration files - -Response status codes -********************** -- **200**: List of base configuration files returned - -Output -******* - -.. raw:: html - - - - - - - - - - - - - - -
NameTypeDescription
filenamestringName of the base configuration file
- -Sample session -*************** - -.. literalinclude:: ../../../examples/controller_get_templateidbaseconfig.txt - - -PUT /v2/templates/**{template_id}**/base-config/**{filename}** -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Update base configuration file content - -Response status codes -********************** -- **200**: File updated -- **404**: File not found - -Input -******* - -.. raw:: html - - - - - - - - - - - -
NameMandatoryTypeDescription
contentstringNew file content
- -Output -******* - -.. raw:: html - - - - - - - - - - - - - -
NameMandatoryTypeDescription
template_idstringTemplate UUID
filenamestringBase configuration filename
contentstringUpdated file content
- -Sample session -*************** - -.. literalinclude:: ../../../examples/controller_put_templateidbaseconfig.txt \ No newline at end of file diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py index 0c61b3ab0..e26c394bf 100644 --- a/tests/api/routes/controller/test_templates.py +++ b/tests/api/routes/controller/test_templates.py @@ -33,6 +33,8 @@ from gns3server.db.repositories.templates import TemplatesRepository from gns3server.controller import Controller from gns3server.controller import Config from gns3server.services.templates import BUILTIN_TEMPLATES +from gns3server.api.routes.controller.dependencies.authentication import get_current_active_user +from gns3server import schemas pytestmark = pytest.mark.asyncio @@ -241,6 +243,15 @@ class TestTemplateRoutes: async def test_get_base_config(self, app: FastAPI, client: AsyncClient): + async def mock_get_current_active_user(): + return schemas.User( + username="admin", + user_id=uuid.uuid4(), + is_superadmin=True, + is_active=True + ) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + create_resp = await client.post(app.url_path_for("create_template"), json={ "name": "TEST", "compute_id": "local", @@ -250,7 +261,6 @@ class TestTemplateRoutes: assert create_resp.status_code == 201 template_id = create_resp.json()["template_id"] - #создаём файл перед чтением await client.put( app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), json={"content": "hello"} @@ -268,20 +278,28 @@ class TestTemplateRoutes: assert response.json()["content"] == "hello" async def test_update_base_config(self, app: FastAPI, client: AsyncClient): - template_id = str(uuid.uuid4()) - filename = "test.txt" - await client.post(app.url_path_for("create_template"), json={ - "template_id": template_id, - "name": "TEST", + async def mock_get_current_active_user(): + return schemas.User( + username="admin", + user_id=uuid.uuid4(), + is_superadmin=True, + is_active=True + ) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + + template_name = f"TEST_UPDATE_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, "compute_id": "local", "template_type": "vpcs" }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] payload = {"content": "hello world"} - response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename=filename), + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), json=payload ) @@ -289,18 +307,27 @@ class TestTemplateRoutes: assert response.json()["content"] == "hello world" async def test_update_base_config_missing_content(self, app: FastAPI, client: AsyncClient): - template_id = str(uuid.uuid4()) - filename = "test.txt" - await client.post(app.url_path_for("create_template"), json={ - "template_id": template_id, - "name": "TEST", + async def mock_get_current_active_user(): + return schemas.User( + username="admin", + user_id=uuid.uuid4(), + is_superadmin=True, + is_active=True + ) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + + template_name = f"TEST_MISSING_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, "compute_id": "local", "template_type": "vpcs" }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename=filename), + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), json={} ) @@ -313,6 +340,44 @@ class TestTemplateRoutes: assert response.status_code == 404 + async def test_list_base_configs(self, app: FastAPI, client: AsyncClient): + + async def mock_get_current_active_user(): + return schemas.User( + username="admin", + user_id=uuid.uuid4(), + is_superadmin=True, + is_active=True + ) + app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + + template_name = f"TEST_LIST_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, + "compute_id": "local", + "template_type": "vpcs" + }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] + + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="config1.txt"), + json={"content": "file1"} + ) + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="config2.txt"), + json={"content": "file2"} + ) + + response = await client.get( + app.url_path_for("list_base_configs", template_id=template_id) + ) + + assert response.status_code == 200 + filenames = [item["filename"] for item in response.json()] + assert "config1.txt" in filenames + assert "config2.txt" in filenames + class TestDuplicateTemplates: From b5e44b540395275170315354da3a649289fb307b Mon Sep 17 00:00:00 2001 From: Volobue Date: Mon, 25 May 2026 22:17:14 +0300 Subject: [PATCH 3/5] added mock user in api endpoints tests --- .../test.txt | 1 + .../test.txt | 1 + .../test.txt | 1 + .../test.txt | 1 + .../config1.txt | 1 + .../config2.txt | 1 + .../config1.txt | 1 + .../config2.txt | 1 + tests/api/routes/controller/test_templates.py | 158 +++++++++--------- 9 files changed, 91 insertions(+), 75 deletions(-) create mode 100644 configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt create mode 100644 configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt create mode 100644 configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt create mode 100644 configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt create mode 100644 configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt create mode 100644 configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt create mode 100644 configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt create mode 100644 configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt diff --git a/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt b/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt b/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt b/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt b/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt new file mode 100644 index 000000000..b6fc4c620 --- /dev/null +++ b/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt @@ -0,0 +1 @@ +hello \ No newline at end of file diff --git a/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt b/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt new file mode 100644 index 000000000..08219db9b --- /dev/null +++ b/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt @@ -0,0 +1 @@ +file1 \ No newline at end of file diff --git a/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt b/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt new file mode 100644 index 000000000..30d67d467 --- /dev/null +++ b/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt @@ -0,0 +1 @@ +file2 \ No newline at end of file diff --git a/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt b/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt new file mode 100644 index 000000000..08219db9b --- /dev/null +++ b/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt @@ -0,0 +1 @@ +file1 \ No newline at end of file diff --git a/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt b/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt new file mode 100644 index 000000000..30d67d467 --- /dev/null +++ b/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt @@ -0,0 +1 @@ +file2 \ No newline at end of file diff --git a/tests/api/routes/controller/test_templates.py b/tests/api/routes/controller/test_templates.py index e26c394bf..cd1745783 100644 --- a/tests/api/routes/controller/test_templates.py +++ b/tests/api/routes/controller/test_templates.py @@ -251,31 +251,33 @@ class TestTemplateRoutes: is_active=True ) app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + try: + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": "TEST", + "compute_id": "local", + "template_type": "vpcs" + }) - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": "TEST", - "compute_id": "local", - "template_type": "vpcs" - }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] - assert create_resp.status_code == 201 - template_id = create_resp.json()["template_id"] - - await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json={"content": "hello"} - ) - - response = await client.get( - app.url_path_for( - "get_base_config", - template_id=template_id, - filename="test.txt" + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), + json={"content": "hello"} ) - ) - assert response.status_code == 200 - assert response.json()["content"] == "hello" + response = await client.get( + app.url_path_for( + "get_base_config", + template_id=template_id, + filename="test.txt" + ) + ) + + assert response.status_code == 200 + assert response.json()["content"] == "hello" + finally: + app.dependency_overrides.pop(get_current_active_user, None) async def test_update_base_config(self, app: FastAPI, client: AsyncClient): @@ -287,24 +289,26 @@ class TestTemplateRoutes: is_active=True ) app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + try: + template_name = f"TEST_UPDATE_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, + "compute_id": "local", + "template_type": "vpcs" + }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] - template_name = f"TEST_UPDATE_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) - assert create_resp.status_code == 201 - template_id = create_resp.json()["template_id"] + payload = {"content": "hello world"} + response = await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), + json=payload + ) - payload = {"content": "hello world"} - response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json=payload - ) - - assert response.status_code == 200 - assert response.json()["content"] == "hello world" + assert response.status_code == 200 + assert response.json()["content"] == "hello world" + finally: + app.dependency_overrides.pop(get_current_active_user, None) async def test_update_base_config_missing_content(self, app: FastAPI, client: AsyncClient): @@ -316,22 +320,24 @@ class TestTemplateRoutes: is_active=True ) app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + try: + template_name = f"TEST_MISSING_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, + "compute_id": "local", + "template_type": "vpcs" + }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] - template_name = f"TEST_MISSING_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) - assert create_resp.status_code == 201 - template_id = create_resp.json()["template_id"] + response = await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), + json={} + ) - response = await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="test.txt"), - json={} - ) - - assert response.status_code in (400, 422) + assert response.status_code in (400, 422) + finally: + app.dependency_overrides.pop(get_current_active_user, None) async def test_base_config_template_not_found(self, app: FastAPI, client: AsyncClient): response = await client.get( @@ -350,33 +356,35 @@ class TestTemplateRoutes: is_active=True ) app.dependency_overrides[get_current_active_user] = mock_get_current_active_user + try: + template_name = f"TEST_LIST_{uuid.uuid4().hex[:8]}" + create_resp = await client.post(app.url_path_for("create_template"), json={ + "name": template_name, + "compute_id": "local", + "template_type": "vpcs" + }) + assert create_resp.status_code == 201 + template_id = create_resp.json()["template_id"] - template_name = f"TEST_LIST_{uuid.uuid4().hex[:8]}" - create_resp = await client.post(app.url_path_for("create_template"), json={ - "name": template_name, - "compute_id": "local", - "template_type": "vpcs" - }) - assert create_resp.status_code == 201 - template_id = create_resp.json()["template_id"] + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="config1.txt"), + json={"content": "file1"} + ) + await client.put( + app.url_path_for("update_base_config", template_id=template_id, filename="config2.txt"), + json={"content": "file2"} + ) - await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="config1.txt"), - json={"content": "file1"} - ) - await client.put( - app.url_path_for("update_base_config", template_id=template_id, filename="config2.txt"), - json={"content": "file2"} - ) + response = await client.get( + app.url_path_for("list_base_configs", template_id=template_id) + ) - response = await client.get( - app.url_path_for("list_base_configs", template_id=template_id) - ) - - assert response.status_code == 200 - filenames = [item["filename"] for item in response.json()] - assert "config1.txt" in filenames - assert "config2.txt" in filenames + assert response.status_code == 200 + filenames = [item["filename"] for item in response.json()] + assert "config1.txt" in filenames + assert "config2.txt" in filenames + finally: + app.dependency_overrides.pop(get_current_active_user, None) class TestDuplicateTemplates: From 87f5a4560a6c604afe1edaba59f1dc914bc9e561 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 31 May 2026 19:26:12 +0200 Subject: [PATCH 4/5] Remove test config files --- .../configs}/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt | 0 .../configs}/49720c91-645b-4e88-b27a-10e899deba50/test.txt | 0 .../configs}/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt | 0 .../configs}/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt | 0 .../configs}/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt | 0 .../configs}/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt | 0 .../configs}/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt | 0 .../configs}/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt | 0 8 files changed, 0 insertions(+), 0 deletions(-) rename {configs => tests/resources/configs}/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt (100%) rename {configs => tests/resources/configs}/49720c91-645b-4e88-b27a-10e899deba50/test.txt (100%) rename {configs => tests/resources/configs}/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt (100%) rename {configs => tests/resources/configs}/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt (100%) rename {configs => tests/resources/configs}/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt (100%) rename {configs => tests/resources/configs}/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt (100%) rename {configs => tests/resources/configs}/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt (100%) rename {configs => tests/resources/configs}/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt (100%) diff --git a/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt b/tests/resources/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt similarity index 100% rename from configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt rename to tests/resources/configs/3dc89371-56f1-45f8-8ec9-042f35ea6c65/test.txt diff --git a/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt b/tests/resources/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt similarity index 100% rename from configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt rename to tests/resources/configs/49720c91-645b-4e88-b27a-10e899deba50/test.txt diff --git a/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt b/tests/resources/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt similarity index 100% rename from configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt rename to tests/resources/configs/6824cfff-51b2-4c02-9e49-aaece7b6742a/test.txt diff --git a/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt b/tests/resources/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt similarity index 100% rename from configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt rename to tests/resources/configs/7b34ada0-c96e-451d-95d6-be07d26a637a/test.txt diff --git a/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt b/tests/resources/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt similarity index 100% rename from configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt rename to tests/resources/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config1.txt diff --git a/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt b/tests/resources/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt similarity index 100% rename from configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt rename to tests/resources/configs/87e5df97-07dd-4b4f-abad-4427745b89fd/config2.txt diff --git a/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt b/tests/resources/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt similarity index 100% rename from configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt rename to tests/resources/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config1.txt diff --git a/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt b/tests/resources/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt similarity index 100% rename from configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt rename to tests/resources/configs/baa12124-1afb-4650-a08f-25a3452c5cbc/config2.txt From bc15e3331b779bc3e47a451e333497b08f737049 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 31 May 2026 19:26:47 +0200 Subject: [PATCH 5/5] Add privilege dependencies for the new API endpoints --- gns3server/api/routes/controller/templates.py | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/gns3server/api/routes/controller/templates.py b/gns3server/api/routes/controller/templates.py index d991ac086..05fa6482c 100644 --- a/gns3server/api/routes/controller/templates.py +++ b/gns3server/api/routes/controller/templates.py @@ -231,16 +231,18 @@ async def duplicate_template( template = await TemplatesService(templates_repo).duplicate_template(template_id) return template -@router.get("/{template_id}/base-config/{filename}") +@router.get( + "/{template_id}/base-config/{filename}", + dependencies=[Depends(has_privilege("Template.Audit"))] +) async def get_base_config( template_id: UUID, filename: str, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), ): + service = TemplatesService(templates_repo) - await service.get_template(template_id) - content = service.get_file(str(template_id), filename) return { @@ -250,7 +252,10 @@ async def get_base_config( } -@router.put("/{template_id}/base-config/{filename}") +@router.put( + "/{template_id}/base-config/{filename}", + dependencies=[Depends(has_privilege("Template.Modify"))] +) async def update_base_config( template_id: UUID, filename: str, @@ -261,9 +266,7 @@ async def update_base_config( raise ControllerBadRequestError("Missing 'content' field") service = TemplatesService(templates_repo) - await service.get_template(template_id) - service.update_file(str(template_id), filename, body["content"]) return { @@ -273,14 +276,14 @@ async def update_base_config( } -@router.get("/{template_id}/base-configs") +@router.get( + "/{template_id}/base-configs", + dependencies=[Depends(has_privilege("Template.Audit"))] +) async def list_base_configs( template_id: UUID, templates_repo: TemplatesRepository = Depends(get_repository(TemplatesRepository)), ): service = TemplatesService(templates_repo) - await service.get_template(template_id) - return service.list_files(str(template_id)) -