Files
stackpilot/backend/tests/test_backup_settings_edit.py
T
menzeljandClaude Opus 5 3c187e2e80
CI / check (push) Successful in 13m18s
CI / build-and-push (push) Successful in 1m53s
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>
2026-09-18 12:10:18 +02:00

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"]