feat: implement API versioning /api/v1/
- All backend routers moved to /api/v1/ prefix
- Frontend BASE_URL updated to /api/v1
- Setup redirect middleware updated to redirect to /api/v1/setup
- Health router path fixed: prefix=/api/v1/health, @router.get('')
- conftest.py: set server_status=online for test fixture
- Created Docs/API_VERSIONING.md with deprecation policy
- Updated Docs/Backend-Development.md with versioning section
- Updated Instructions.md curl examples
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -68,9 +68,9 @@ async def file_config_client(tmp_path: Path) -> AsyncClient: # type: ignore[mis
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as ac:
|
||||
await ac.post("/api/setup", json=_SETUP_PAYLOAD)
|
||||
await ac.post("/api/v1/setup", json=_SETUP_PAYLOAD)
|
||||
login = await ac.post(
|
||||
"/api/auth/login",
|
||||
"/api/v1/auth/login",
|
||||
json={"password": _SETUP_PAYLOAD["master_password"]},
|
||||
)
|
||||
assert login.status_code == 200
|
||||
@@ -115,7 +115,7 @@ class TestListJailConfigFiles:
|
||||
"app.routers.file_config.raw_config_io_service.list_jail_config_files",
|
||||
AsyncMock(return_value=_jail_files_resp()),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/jail-files")
|
||||
resp = await file_config_client.get("/api/v1/config/jail-files")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
@@ -129,7 +129,7 @@ class TestListJailConfigFiles:
|
||||
"app.routers.file_config.raw_config_io_service.list_jail_config_files",
|
||||
AsyncMock(side_effect=ConfigDirError("not found")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/jail-files")
|
||||
resp = await file_config_client.get("/api/v1/config/jail-files")
|
||||
|
||||
assert resp.status_code == 503
|
||||
|
||||
@@ -137,7 +137,7 @@ class TestListJailConfigFiles:
|
||||
resp = await AsyncClient(
|
||||
transport=ASGITransport(app=file_config_client._transport.app), # type: ignore[attr-defined]
|
||||
base_url="http://test",
|
||||
).get("/api/config/jail-files")
|
||||
).get("/api/v1/config/jail-files")
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ class TestGetJailConfigFile:
|
||||
"app.routers.file_config.raw_config_io_service.get_jail_config_file",
|
||||
AsyncMock(return_value=content),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/jail-files/sshd.conf")
|
||||
resp = await file_config_client.get("/api/v1/config/jail-files/sshd.conf")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["content"] == "[sshd]\nenabled = true\n"
|
||||
@@ -170,7 +170,7 @@ class TestGetJailConfigFile:
|
||||
"app.routers.file_config.raw_config_io_service.get_jail_config_file",
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/jail-files/missing.conf")
|
||||
resp = await file_config_client.get("/api/v1/config/jail-files/missing.conf")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
@@ -181,7 +181,7 @@ class TestGetJailConfigFile:
|
||||
"app.routers.file_config.raw_config_io_service.get_jail_config_file",
|
||||
AsyncMock(side_effect=ConfigFileNameError("bad name")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/jail-files/bad.txt")
|
||||
resp = await file_config_client.get("/api/v1/config/jail-files/bad.txt")
|
||||
|
||||
assert resp.status_code == 400
|
||||
|
||||
@@ -198,7 +198,7 @@ class TestSetJailConfigEnabled:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/jail-files/sshd.conf/enabled",
|
||||
"/api/v1/config/jail-files/sshd.conf/enabled",
|
||||
json={"enabled": False},
|
||||
)
|
||||
|
||||
@@ -210,7 +210,7 @@ class TestSetJailConfigEnabled:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/jail-files/missing.conf/enabled",
|
||||
"/api/v1/config/jail-files/missing.conf/enabled",
|
||||
json={"enabled": True},
|
||||
)
|
||||
|
||||
@@ -235,7 +235,7 @@ class TestGetFilterFileRaw:
|
||||
"app.routers.file_config.raw_config_io_service.get_filter_file",
|
||||
AsyncMock(return_value=_conf_file_content("nginx")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/nginx/raw")
|
||||
resp = await file_config_client.get("/api/v1/config/filters/nginx/raw")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "nginx"
|
||||
@@ -245,7 +245,7 @@ class TestGetFilterFileRaw:
|
||||
"app.routers.file_config.raw_config_io_service.get_filter_file",
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/missing/raw")
|
||||
resp = await file_config_client.get("/api/v1/config/filters/missing/raw")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
@@ -262,7 +262,7 @@ class TestUpdateFilterFile:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/filters/nginx/raw",
|
||||
"/api/v1/config/filters/nginx/raw",
|
||||
json={"content": "[Definition]\nfailregex = test\n"},
|
||||
)
|
||||
|
||||
@@ -274,7 +274,7 @@ class TestUpdateFilterFile:
|
||||
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/filters/nginx/raw",
|
||||
"/api/v1/config/filters/nginx/raw",
|
||||
json={"content": "x"},
|
||||
)
|
||||
|
||||
@@ -293,7 +293,7 @@ class TestCreateFilterFile:
|
||||
AsyncMock(return_value="myfilter.conf"),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/filters/raw",
|
||||
"/api/v1/config/filters/raw",
|
||||
json={"name": "myfilter", "content": "[Definition]\n"},
|
||||
)
|
||||
|
||||
@@ -306,7 +306,7 @@ class TestCreateFilterFile:
|
||||
AsyncMock(side_effect=ConfigFileExistsError("myfilter.conf")),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/filters/raw",
|
||||
"/api/v1/config/filters/raw",
|
||||
json={"name": "myfilter", "content": "[Definition]\n"},
|
||||
)
|
||||
|
||||
@@ -318,7 +318,7 @@ class TestCreateFilterFile:
|
||||
AsyncMock(side_effect=ConfigFileNameError("bad/../name")),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/filters/raw",
|
||||
"/api/v1/config/filters/raw",
|
||||
json={"name": "../escape", "content": "[Definition]\n"},
|
||||
)
|
||||
|
||||
@@ -345,7 +345,7 @@ class TestListActionFiles:
|
||||
"app.routers.config.action_config_service.list_actions",
|
||||
AsyncMock(return_value=resp_data),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/actions")
|
||||
resp = await file_config_client.get("/api/v1/config/actions")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["actions"][0]["name"] == "iptables"
|
||||
@@ -369,7 +369,7 @@ class TestCreateActionFile:
|
||||
AsyncMock(return_value=created),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/actions",
|
||||
"/api/v1/config/actions",
|
||||
json={"name": "myaction", "actionban": "echo ban <ip>"},
|
||||
)
|
||||
|
||||
@@ -390,7 +390,7 @@ class TestGetActionFileRaw:
|
||||
"app.routers.file_config.raw_config_io_service.get_action_file",
|
||||
AsyncMock(return_value=_conf_file_content("iptables")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/actions/iptables/raw")
|
||||
resp = await file_config_client.get("/api/v1/config/actions/iptables/raw")
|
||||
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["name"] == "iptables"
|
||||
@@ -400,7 +400,7 @@ class TestGetActionFileRaw:
|
||||
"app.routers.file_config.raw_config_io_service.get_action_file",
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/actions/missing/raw")
|
||||
resp = await file_config_client.get("/api/v1/config/actions/missing/raw")
|
||||
|
||||
assert resp.status_code == 404
|
||||
|
||||
@@ -411,7 +411,7 @@ class TestGetActionFileRaw:
|
||||
"app.routers.file_config.raw_config_io_service.get_action_file",
|
||||
AsyncMock(side_effect=ConfigDirError("no dir")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/actions/iptables/raw")
|
||||
resp = await file_config_client.get("/api/v1/config/actions/iptables/raw")
|
||||
|
||||
assert resp.status_code == 503
|
||||
|
||||
@@ -430,7 +430,7 @@ class TestUpdateActionFileRaw:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/iptables/raw",
|
||||
"/api/v1/config/actions/iptables/raw",
|
||||
json={"content": "[Definition]\nactionban = iptables -I INPUT -s <ip> -j DROP\n"},
|
||||
)
|
||||
|
||||
@@ -442,7 +442,7 @@ class TestUpdateActionFileRaw:
|
||||
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/iptables/raw",
|
||||
"/api/v1/config/actions/iptables/raw",
|
||||
json={"content": "x"},
|
||||
)
|
||||
|
||||
@@ -454,7 +454,7 @@ class TestUpdateActionFileRaw:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/missing/raw",
|
||||
"/api/v1/config/actions/missing/raw",
|
||||
json={"content": "x"},
|
||||
)
|
||||
|
||||
@@ -466,7 +466,7 @@ class TestUpdateActionFileRaw:
|
||||
AsyncMock(side_effect=ConfigFileNameError("bad/../name")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/escape/raw",
|
||||
"/api/v1/config/actions/escape/raw",
|
||||
json={"content": "x"},
|
||||
)
|
||||
|
||||
@@ -485,7 +485,7 @@ class TestCreateJailConfigFile:
|
||||
AsyncMock(return_value="myjail.conf"),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/jail-files",
|
||||
"/api/v1/config/jail-files",
|
||||
json={"name": "myjail", "content": "[myjail]\nenabled = true\n"},
|
||||
)
|
||||
|
||||
@@ -498,7 +498,7 @@ class TestCreateJailConfigFile:
|
||||
AsyncMock(side_effect=ConfigFileExistsError("myjail.conf")),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/jail-files",
|
||||
"/api/v1/config/jail-files",
|
||||
json={"name": "myjail", "content": "[myjail]\nenabled = true\n"},
|
||||
)
|
||||
|
||||
@@ -510,7 +510,7 @@ class TestCreateJailConfigFile:
|
||||
AsyncMock(side_effect=ConfigFileNameError("bad/../name")),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/jail-files",
|
||||
"/api/v1/config/jail-files",
|
||||
json={"name": "../escape", "content": "[Definition]\n"},
|
||||
)
|
||||
|
||||
@@ -524,7 +524,7 @@ class TestCreateJailConfigFile:
|
||||
AsyncMock(side_effect=ConfigDirError("no dir")),
|
||||
):
|
||||
resp = await file_config_client.post(
|
||||
"/api/config/jail-files",
|
||||
"/api/v1/config/jail-files",
|
||||
json={"name": "anyjail", "content": "[anyjail]\nenabled = false\n"},
|
||||
)
|
||||
|
||||
@@ -545,7 +545,7 @@ class TestGetParsedFilter:
|
||||
"app.routers.file_config.raw_config_io_service.get_parsed_filter_file",
|
||||
AsyncMock(return_value=cfg),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/nginx/parsed")
|
||||
resp = await file_config_client.get("/api/v1/config/filters/nginx/parsed")
|
||||
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
@@ -558,7 +558,7 @@ class TestGetParsedFilter:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/filters/missing/parsed"
|
||||
"/api/v1/config/filters/missing/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
@@ -570,7 +570,7 @@ class TestGetParsedFilter:
|
||||
"app.routers.file_config.raw_config_io_service.get_parsed_filter_file",
|
||||
AsyncMock(side_effect=ConfigDirError("no dir")),
|
||||
):
|
||||
resp = await file_config_client.get("/api/config/filters/nginx/parsed")
|
||||
resp = await file_config_client.get("/api/v1/config/filters/nginx/parsed")
|
||||
|
||||
assert resp.status_code == 503
|
||||
|
||||
@@ -587,7 +587,7 @@ class TestUpdateParsedFilter:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/filters/nginx/parsed",
|
||||
"/api/v1/config/filters/nginx/parsed",
|
||||
json={"failregex": ["^<HOST> "]},
|
||||
)
|
||||
|
||||
@@ -599,7 +599,7 @@ class TestUpdateParsedFilter:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/filters/missing/parsed",
|
||||
"/api/v1/config/filters/missing/parsed",
|
||||
json={"failregex": []},
|
||||
)
|
||||
|
||||
@@ -611,7 +611,7 @@ class TestUpdateParsedFilter:
|
||||
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/filters/nginx/parsed",
|
||||
"/api/v1/config/filters/nginx/parsed",
|
||||
json={"failregex": ["^<HOST> "]},
|
||||
)
|
||||
|
||||
@@ -633,7 +633,7 @@ class TestGetParsedAction:
|
||||
AsyncMock(return_value=cfg),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/actions/iptables/parsed"
|
||||
"/api/v1/config/actions/iptables/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
@@ -647,7 +647,7 @@ class TestGetParsedAction:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/actions/missing/parsed"
|
||||
"/api/v1/config/actions/missing/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
@@ -660,7 +660,7 @@ class TestGetParsedAction:
|
||||
AsyncMock(side_effect=ConfigDirError("no dir")),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/actions/iptables/parsed"
|
||||
"/api/v1/config/actions/iptables/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 503
|
||||
@@ -678,7 +678,7 @@ class TestUpdateParsedAction:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/iptables/parsed",
|
||||
"/api/v1/config/actions/iptables/parsed",
|
||||
json={"actionban": "iptables -I INPUT -s <ip> -j DROP"},
|
||||
)
|
||||
|
||||
@@ -690,7 +690,7 @@ class TestUpdateParsedAction:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/missing/parsed",
|
||||
"/api/v1/config/actions/missing/parsed",
|
||||
json={"actionban": ""},
|
||||
)
|
||||
|
||||
@@ -702,7 +702,7 @@ class TestUpdateParsedAction:
|
||||
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/actions/iptables/parsed",
|
||||
"/api/v1/config/actions/iptables/parsed",
|
||||
json={"actionban": "iptables -I INPUT -s <ip> -j DROP"},
|
||||
)
|
||||
|
||||
@@ -725,7 +725,7 @@ class TestGetParsedJailFile:
|
||||
AsyncMock(return_value=cfg),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/jail-files/sshd.conf/parsed"
|
||||
"/api/v1/config/jail-files/sshd.conf/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 200
|
||||
@@ -739,7 +739,7 @@ class TestGetParsedJailFile:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/jail-files/missing.conf/parsed"
|
||||
"/api/v1/config/jail-files/missing.conf/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 404
|
||||
@@ -752,7 +752,7 @@ class TestGetParsedJailFile:
|
||||
AsyncMock(side_effect=ConfigDirError("no dir")),
|
||||
):
|
||||
resp = await file_config_client.get(
|
||||
"/api/config/jail-files/sshd.conf/parsed"
|
||||
"/api/v1/config/jail-files/sshd.conf/parsed"
|
||||
)
|
||||
|
||||
assert resp.status_code == 503
|
||||
@@ -770,7 +770,7 @@ class TestUpdateParsedJailFile:
|
||||
AsyncMock(return_value=None),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/jail-files/sshd.conf/parsed",
|
||||
"/api/v1/config/jail-files/sshd.conf/parsed",
|
||||
json={"jails": {"sshd": {"enabled": False}}},
|
||||
)
|
||||
|
||||
@@ -782,7 +782,7 @@ class TestUpdateParsedJailFile:
|
||||
AsyncMock(side_effect=ConfigFileNotFoundError("missing.conf")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/jail-files/missing.conf/parsed",
|
||||
"/api/v1/config/jail-files/missing.conf/parsed",
|
||||
json={"jails": {}},
|
||||
)
|
||||
|
||||
@@ -794,7 +794,7 @@ class TestUpdateParsedJailFile:
|
||||
AsyncMock(side_effect=ConfigFileWriteError("disk full")),
|
||||
):
|
||||
resp = await file_config_client.put(
|
||||
"/api/config/jail-files/sshd.conf/parsed",
|
||||
"/api/v1/config/jail-files/sshd.conf/parsed",
|
||||
json={"jails": {"sshd": {"enabled": True}}},
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user