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>
100 lines
3.3 KiB
Python
100 lines
3.3 KiB
Python
"""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"]
|