Групповое удаление бэкапов, поддержка CHR, строгое сравнение версий ROS
Групповое удаление бэкапов (docs/changes/013): - страница «Резервные копии»: чекбоксы, «выбрать все», панель «Выбрано: N / Удалить / Снять выбор» вместо фильтров, подтверждение, итог удаления; - backups.delete_many: все ключи проверяются до удаления, удаление параллельное; UI POST /backups/delete-many, API POST /api/v1/backups/delete; - выбор строк в app.js обобщён (data-select) для устройств и бэкапов. Поддержка CHR (docs/changes/014): - у CHR нет /system/routerboard (HTTP 400): устройство больше не считается недоступным, версия FW не показывается, обновление FW пропускается; - «есть обновление ROS» определяется строгим сравнением версий (на канале long-term последняя версия может быть старше установленной); - в таблице указана причина недоступности: авторизация / ошибка ответа / нет соединения. Тесты: 11 из 11. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4c1841b61b
commit
0914209bf8
16 files changed
+194
-20
No files matched your search
@@ -229,3 +229,48 @@ async def test_poll_marks_offline_and_recovers(monkeypatch):
|
||||
await ops.poll_device(d.id) # онлайн -> лёгкий опрос обновляет uptime, остальное сохраняется
|
||||
dev = devices.get_device(d.id)
|
||||
assert dev.status["uptime"] == "2m" and dev.status["ros_latest"] == "7.24.4"
|
||||
|
||||
|
||||
def test_bulk_delete_backups(monkeypatch):
|
||||
"""Групповое удаление: чужой ключ отклоняет всю операцию; допустимые — удаляются (UI и API)."""
|
||||
removed = []
|
||||
|
||||
async def fake_delete(key):
|
||||
removed.append(key)
|
||||
|
||||
monkeypatch.setattr(s3, "delete_object", fake_delete)
|
||||
ok = ["backups/a/1.backup", "backups/a/1.rsc"]
|
||||
with TestClient(create_app()) as c:
|
||||
c.post("/login", data={"username": "admin", "password": "pw"})
|
||||
# UI: ключ вне префикса бэкапов -> ошибка, ничего не удалено
|
||||
r = c.post("/backups/delete-many", data={"key": ok + ["other/secret.txt"]}, follow_redirects=False)
|
||||
assert r.status_code == 400 and removed == []
|
||||
# UI: допустимые ключи -> возврат на страницу с итогом
|
||||
r = c.post("/backups/delete-many", data={"key": ok, "next": "/backups?group=1"}, follow_redirects=False)
|
||||
assert r.status_code == 303 and r.headers["location"] == "/backups?group=1&deleted=2&failed=0"
|
||||
assert sorted(removed) == ok
|
||||
# API
|
||||
removed.clear()
|
||||
h = {"Authorization": "Bearer test-token"}
|
||||
assert c.post("/api/v1/backups/delete", headers=h, json={"keys": ok}).json() == {"deleted": 2, "failed": 0}
|
||||
assert c.post("/api/v1/backups/delete", headers=h, json={"keys": ["x/../y"]}).status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chr_status_and_version_compare():
|
||||
"""CHR: раздела system/routerboard нет (HTTP 400) — это не сбой; «последняя» версия старше установленной — не обновление."""
|
||||
def handler(r: httpx.Request) -> httpx.Response:
|
||||
if r.url.path == "/rest/system/routerboard":
|
||||
return httpx.Response(400, json={"detail": "no such command or directory (routerboard)", "error": 400})
|
||||
data = {
|
||||
"/rest/system/resource": {"board-name": "CHR Yandex epyc-9654", "version": "7.24 (stable)", "uptime": "1d"},
|
||||
"/rest/system/package/update": {"channel": "long-term", "installed-version": "7.24", "latest-version": "7.23.7"},
|
||||
}
|
||||
return httpx.Response(200, json=data.get(r.url.path, []))
|
||||
|
||||
async with ros_client(handler) as c:
|
||||
st = await ros.get_status(c)
|
||||
assert await ros.upgrade_firmware(c) == "Устройство без RouterBOARD firmware (например, CHR) — пропущено"
|
||||
assert st["model"].startswith("CHR") and st["fw_current"] is None
|
||||
assert not ros.version_newer(st["ros_latest"], st["ros_installed"]) # 7.23.7 старше 7.24
|
||||
assert ros.version_newer("7.24.1", "7.24") and ros.version_newer("7.25", "7.25rc1") and not ros.version_newer("7.25rc1", "7.25")
|
||||
Reference in new issue
Block a user