Fix raw action config endpoint shadowed by config router

Rename GET/PUT /api/config/actions/{name} to /actions/{name}/raw in
file_config.py to eliminate the route-shadowing conflict with config.py,
which registers its own GET /actions/{name} returning ActionConfig.

Add configActionRaw endpoint helper in endpoints.ts and update
fetchActionFile/updateActionFile in config.ts to use it. Add
TestGetActionFileRaw and TestUpdateActionFileRaw test classes.
This commit is contained in:
2026-03-15 14:09:37 +01:00
parent 41dcd60225
commit b81e0cdbb4
7 changed files with 219 additions and 40 deletions

View File

@@ -14,8 +14,8 @@ Endpoints:
* ``GET /api/config/filters/{name}/parsed`` — parse a filter file into a structured model
* ``PUT /api/config/filters/{name}/parsed`` — update a filter file from a structured model
* ``GET /api/config/actions`` — list all action files
* ``GET /api/config/actions/{name}`` — get one action file (with content)
* ``PUT /api/config/actions/{name}`` — update an action file
* ``GET /api/config/actions/{name}/raw`` — get one action file (raw content)
* ``PUT /api/config/actions/{name}/raw`` — update an action file (raw content)
* ``POST /api/config/actions`` — create a new action file
* ``GET /api/config/actions/{name}/parsed`` — parse an action file into a structured model
* ``PUT /api/config/actions/{name}/parsed`` — update an action file from a structured model
@@ -460,7 +460,7 @@ async def list_action_files(
@router.get(
"/actions/{name}",
"/actions/{name}/raw",
response_model=ConfFileContent,
summary="Return an action definition file with its content",
)
@@ -496,7 +496,7 @@ async def get_action_file(
@router.put(
"/actions/{name}",
"/actions/{name}/raw",
status_code=status.HTTP_204_NO_CONTENT,
summary="Update an action definition file",
)

View File

@@ -377,6 +377,102 @@ class TestCreateActionFile:
assert resp.json()["name"] == "myaction"
# ---------------------------------------------------------------------------
# GET /api/config/actions/{name}/raw
# ---------------------------------------------------------------------------
class TestGetActionFileRaw:
"""Tests for ``GET /api/config/actions/{name}/raw``."""
async def test_200_returns_content(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.get_action_file",
AsyncMock(return_value=_conf_file_content("iptables")),
):
resp = await file_config_client.get("/api/config/actions/iptables/raw")
assert resp.status_code == 200
assert resp.json()["name"] == "iptables"
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.get_action_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.get("/api/config/actions/missing/raw")
assert resp.status_code == 404
async def test_503_on_config_dir_error(
self, file_config_client: AsyncClient
) -> None:
with patch(
"app.routers.file_config.file_config_service.get_action_file",
AsyncMock(side_effect=ConfigDirError("no dir")),
):
resp = await file_config_client.get("/api/config/actions/iptables/raw")
assert resp.status_code == 503
# ---------------------------------------------------------------------------
# PUT /api/config/actions/{name}/raw
# ---------------------------------------------------------------------------
class TestUpdateActionFileRaw:
"""Tests for ``PUT /api/config/actions/{name}/raw``."""
async def test_204_on_success(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.write_action_file",
AsyncMock(return_value=None),
):
resp = await file_config_client.put(
"/api/config/actions/iptables/raw",
json={"content": "[Definition]\nactionban = iptables -I INPUT -s <ip> -j DROP\n"},
)
assert resp.status_code == 204
async def test_400_write_error(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.write_action_file",
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
):
resp = await file_config_client.put(
"/api/config/actions/iptables/raw",
json={"content": "x"},
)
assert resp.status_code == 400
async def test_404_not_found(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.write_action_file",
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
):
resp = await file_config_client.put(
"/api/config/actions/missing/raw",
json={"content": "x"},
)
assert resp.status_code == 404
async def test_400_invalid_name(self, file_config_client: AsyncClient) -> None:
with patch(
"app.routers.file_config.file_config_service.write_action_file",
AsyncMock(side_effect=ConfigFileNameError("bad/../name")),
):
resp = await file_config_client.put(
"/api/config/actions/escape/raw",
json={"content": "x"},
)
assert resp.status_code == 400
# ---------------------------------------------------------------------------
# POST /api/config/jail-files
# ---------------------------------------------------------------------------