Edit backup destinations and schedules in Settings (0.64.1)
Both had update endpoints, but Settings only offered add, delete and, for schedules, enable/disable, so a changed NAS address or backup time meant deleting and recreating the entry. Each row now has an Edit button that swaps it for the prefilled form, as for private registries. - Destinations: name and connection fields. Stored secrets come back masked, so they start empty with "Stored - leave empty to keep it"; the server keeps the stored value for an empty one. The type is fixed. - Schedules: destination, timing, retention, volumes, stop-first and enabled. The stack is fixed, since retention matches backups by stack id. The last status shows warning/retrying in amber instead of red. Tests pin that an empty secret keeps the stored one, a typed one replaces it, and a timing change moves next_run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8f0a01c4f5
commit
3c187e2e80
4 files changed
+236
-33
No files matched your search
@@ -0,0 +1,99 @@
|
||||
"""Editing backup destinations and schedules from Settings.
|
||||
|
||||
The UI only ever sees stored secrets masked, so it sends them back empty; the
|
||||
server must keep the stored value then, and apply every other change. A
|
||||
schedule whose timing changes gets a new next run.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def destination(as_admin):
|
||||
r = as_admin.post(
|
||||
"/api/backups/destinations",
|
||||
json={
|
||||
"name": f"nas-{uuid.uuid4().hex[:6]}",
|
||||
"type": "sftp",
|
||||
"config": {"host": "nas", "username": "backup", "password": "s3cret", "path": "/b"},
|
||||
},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
return r.json()
|
||||
|
||||
|
||||
def _stored_config(dest_id):
|
||||
from sqlmodel import Session
|
||||
|
||||
from database import engine
|
||||
from models.backup_destination import BackupDestination
|
||||
from services import backup_destination_service as dest_service
|
||||
|
||||
with Session(engine) as session:
|
||||
return dest_service.parse_config(session.get(BackupDestination, dest_id))
|
||||
|
||||
|
||||
def test_edit_keeps_a_secret_sent_back_empty(as_admin, destination):
|
||||
assert destination["config"]["password"] == "••••••"
|
||||
|
||||
r = as_admin.put(
|
||||
f"/api/backups/destinations/{destination['id']}",
|
||||
json={
|
||||
"name": "renamed",
|
||||
"config": {"host": "nas2", "username": "backup", "password": "", "path": "/new"},
|
||||
},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
assert r.json()["name"] == "renamed"
|
||||
stored = _stored_config(destination["id"])
|
||||
assert stored == {"host": "nas2", "username": "backup", "password": "s3cret", "path": "/new"}
|
||||
|
||||
|
||||
def test_edit_replaces_a_secret_that_was_typed(as_admin, destination):
|
||||
as_admin.put(
|
||||
f"/api/backups/destinations/{destination['id']}",
|
||||
json={"config": {"password": "n3w"}},
|
||||
)
|
||||
assert _stored_config(destination["id"])["password"] == "n3w"
|
||||
|
||||
|
||||
def test_edit_schedule_moves_its_next_run(as_admin, destination):
|
||||
from sqlmodel import Session
|
||||
|
||||
from database import engine
|
||||
from models.stack import Stack
|
||||
|
||||
stack_id = f"s{uuid.uuid4().hex[:8]}"
|
||||
with Session(engine) as session:
|
||||
session.add(Stack(id=stack_id, name=stack_id))
|
||||
session.commit()
|
||||
r = as_admin.post(
|
||||
"/api/backups/schedules",
|
||||
json={"stack_id": stack_id, "destination_id": destination["id"],
|
||||
"frequency": "daily", "hour": 3, "minute": 0},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
created = r.json()
|
||||
|
||||
r = as_admin.put(
|
||||
f"/api/backups/schedules/{created['id']}",
|
||||
json={"frequency": "weekly", "weekday": 6, "hour": 4, "minute": 30, "keep": 3,
|
||||
"include_volumes": False, "stop_first": False, "enabled": True,
|
||||
"destination_id": destination["id"]},
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
updated = r.json()
|
||||
assert updated["stack_id"] == stack_id
|
||||
assert (updated["frequency"], updated["weekday"], updated["hour"], updated["minute"]) == (
|
||||
"weekly", 6, 4, 30,
|
||||
)
|
||||
assert (updated["keep"], updated["include_volumes"], updated["stop_first"]) == (3, False, False)
|
||||
next_run = datetime.fromisoformat(updated["next_run"])
|
||||
assert (next_run.weekday(), next_run.hour, next_run.minute) == (6, 4, 30)
|
||||
assert updated["next_run"] != created["next_run"]
|
||||
+1
-1
@@ -1,3 +1,3 @@
|
||||
"""Single source of truth for the StackPilot release version."""
|
||||
|
||||
APP_VERSION = "0.64.0"
|
||||
APP_VERSION = "0.64.1"
|
||||
Reference in new issue
Block a user