mirror of
https://github.com/CyberMind-FR/secubox-deb.git
synced 2026-07-29 09:14:33 +00:00
fix(profiles): standalone rollback clears the active pointer (state no longer matches a named profile) (ref #893)
Co-Authored-By: Gerald KERMA <devel@cybermind.fr>
This commit is contained in:
parent
3edc32cd4b
commit
b5503a12d6
|
|
@ -503,8 +503,24 @@ def create_app() -> FastAPI:
|
|||
|
||||
@app.post("/api/v1/profiles/rollback")
|
||||
async def rollback_active(_claims=Depends(require_jwt)):
|
||||
# A rollback restores an ARBITRARY point-in-time snapshot, which has no
|
||||
# associated profile name — unlike /apply, there is no "prior profile"
|
||||
# to revert `active` to. On a SUCCESSFUL rollback the honest state
|
||||
# afterward is "no named profile / custom": clear the pointer rather
|
||||
# than leave it naming whatever profile the last /apply set, which no
|
||||
# longer matches the (rolled-back) board state. Success predicate
|
||||
# mirrors the CLI's own (see api/cli.py _cmd_rollback: rc 0 iff
|
||||
# status in ("applied", "planned")) — "planned" is not structurally
|
||||
# reachable here (the ctl is always invoked with --yes) but matching
|
||||
# the CLI's exact predicate beats an ad-hoc single-value check.
|
||||
root = _root()
|
||||
_mod, _prof_dir, _pins, active_file = _cli._paths(root)
|
||||
active_path = Path(active_file)
|
||||
async with _apply_lock:
|
||||
return await _run_ctl_json("rollback")
|
||||
report = await _run_ctl_json("rollback")
|
||||
if isinstance(report, dict) and report.get("status") in ("applied", "planned"):
|
||||
active_path.unlink(missing_ok=True)
|
||||
return report
|
||||
|
||||
return app
|
||||
|
||||
|
|
|
|||
|
|
@ -502,3 +502,47 @@ def test_apply_applied_keeps_active_at_target(client, root, monkeypatch):
|
|||
r = client.post("/api/v1/profiles/apply", json={"profile": "lite"})
|
||||
assert r.status_code == 200
|
||||
assert (root / "profiles" / "active").read_text().strip() == "lite" # kept
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# rollback clears `active` — a standalone rollback restores an ARBITRARY
|
||||
# point-in-time snapshot that has no associated profile name; leaving `active`
|
||||
# unchanged would have it keep naming whatever profile the last /apply set,
|
||||
# which no longer matches the (rolled-back) board state. Mirrors the
|
||||
# apply_active revert-on-rolled_back fix above, but for the separate
|
||||
# /rollback route: on a successful rollback (CLI status "applied"/"planned",
|
||||
# see api/cli.py _cmd_rollback's own success predicate) the honest state is
|
||||
# "no named profile" -> clear the pointer.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def test_rollback_clears_active_pointer(client, root, monkeypatch):
|
||||
assert (root / "profiles" / "active").read_text().strip() == "media"
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
class P:
|
||||
returncode = 0
|
||||
stdout = ('{"status":"applied","changed":["x"],"failed":[],'
|
||||
'"rolled_back":["x"],"target":"R1"}')
|
||||
stderr = ""
|
||||
return P()
|
||||
monkeypatch.setattr(web, "_ctl_run", fake_run)
|
||||
|
||||
r = client.post("/api/v1/profiles/rollback", json={})
|
||||
assert r.status_code == 200
|
||||
assert not (root / "profiles" / "active").exists()
|
||||
|
||||
|
||||
def test_rollback_failure_leaves_active_untouched(client, root, monkeypatch):
|
||||
assert (root / "profiles" / "active").read_text().strip() == "media"
|
||||
|
||||
def fake_run(argv, **kw):
|
||||
class P:
|
||||
returncode = 2
|
||||
stdout = ""
|
||||
stderr = "boum"
|
||||
return P()
|
||||
monkeypatch.setattr(web, "_ctl_run", fake_run)
|
||||
|
||||
r = client.post("/api/v1/profiles/rollback", json={})
|
||||
assert r.status_code == 500
|
||||
assert (root / "profiles" / "active").read_text().strip() == "media"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user