mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-11-03 02:26:43 +09:00
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
"""Plan 23 Phase 5 — edge-triggered restore-proof alerts + support-bundle section."""
|
|
from datetime import datetime
|
|
|
|
from app import db
|
|
from app.services.backup_policy_service import BackupPolicyService
|
|
from app.services.backup_alert_service import BackupAlertService
|
|
|
|
|
|
def _capture_notify(monkeypatch):
|
|
import app.plugins_sdk as sdk
|
|
sent = []
|
|
monkeypatch.setattr(sdk.notify, 'send',
|
|
lambda event, to=None, data=None, **kw: sent.append((event, data)))
|
|
return sent
|
|
|
|
|
|
def _policy(target_id):
|
|
return BackupPolicyService.get_or_create_policy('files', target_id,
|
|
target_meta={'paths': ['/x']})
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Drill alerts — fire once on transition (#17)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_drill_failed_fires_once_then_throttles(app, monkeypatch):
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5501)
|
|
BackupAlertService.on_drill_result(policy, 'failed', 'boom')
|
|
assert [e for e, _ in sent] == ['backup.drill_failed']
|
|
# A second consecutive failure does NOT re-alert.
|
|
BackupAlertService.on_drill_result(policy, 'failed', 'boom again')
|
|
assert [e for e, _ in sent] == ['backup.drill_failed']
|
|
|
|
|
|
def test_drill_recovery_fires_after_failure(app, monkeypatch):
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5502)
|
|
BackupAlertService.on_drill_result(policy, 'failed', 'boom')
|
|
BackupAlertService.on_drill_result(policy, 'success', None)
|
|
assert [e for e, _ in sent] == ['backup.drill_failed', 'backup.drill_recovered']
|
|
# Staying healthy doesn't repeat the recovery notice.
|
|
BackupAlertService.on_drill_result(policy, 'success', None)
|
|
assert [e for e, _ in sent] == ['backup.drill_failed', 'backup.drill_recovered']
|
|
|
|
|
|
def test_first_success_is_silent(app, monkeypatch):
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5503)
|
|
# A healthy-from-the-start policy never had a failure -> no recovery spam.
|
|
BackupAlertService.on_drill_result(policy, 'success', None)
|
|
assert sent == []
|
|
|
|
|
|
def test_skipped_no_space_counts_as_failure(app, monkeypatch):
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5504)
|
|
BackupAlertService.on_drill_result(policy, 'skipped_no_space', 'no space')
|
|
assert [e for e, _ in sent] == ['backup.drill_failed']
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Verify alerts (#17)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_verify_failed_and_recovered(app, monkeypatch):
|
|
from app.models.backup_run import BackupRun
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5505)
|
|
bad = BackupRun(policy_id=policy.id, kind='full', status='success',
|
|
verify_level='listed', verify_error='Checksum mismatch')
|
|
db.session.add(bad)
|
|
db.session.commit()
|
|
BackupAlertService.on_verify_result(policy, bad)
|
|
assert [e for e, _ in sent] == ['backup.verify_failed']
|
|
|
|
good = BackupRun(policy_id=policy.id, kind='full', status='success',
|
|
verify_level='hashed')
|
|
db.session.add(good)
|
|
db.session.commit()
|
|
BackupAlertService.on_verify_result(policy, good)
|
|
assert [e for e, _ in sent] == ['backup.verify_failed', 'backup.verify_recovered']
|
|
|
|
|
|
def test_verify_failed_on_deep_offsite_mismatch(app, monkeypatch):
|
|
from app.models.backup_run import BackupRun
|
|
with app.app_context():
|
|
sent = _capture_notify(monkeypatch)
|
|
policy = _policy(5506)
|
|
run = BackupRun(policy_id=policy.id, kind='full', status='success',
|
|
verify_level='hashed')
|
|
run.set_metadata({'offsite': {'evidence': 'size_only', 'deep_match': False}})
|
|
db.session.add(run)
|
|
db.session.commit()
|
|
BackupAlertService.on_verify_result(policy, run)
|
|
assert [e for e, _ in sent] == ['backup.verify_failed']
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Catalog registration (#17)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_catalog_has_restore_proof_events():
|
|
from app.notifications import catalog
|
|
for key in ('backup.drill_failed', 'backup.drill_recovered',
|
|
'backup.verify_failed', 'backup.verify_recovered'):
|
|
entry = catalog.get(key)
|
|
assert entry is not None
|
|
assert entry['category'] == 'backups'
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Support bundle restore_confidence section (#18)
|
|
# --------------------------------------------------------------------------- #
|
|
|
|
def test_support_bundle_restore_confidence(app):
|
|
from app.services.support_bundle_service import _collect_restore_confidence
|
|
from app.models.backup_run import BackupRun
|
|
with app.app_context():
|
|
policy = _policy(5507)
|
|
policy.drill_cadence = 'weekly'
|
|
policy.last_drill_status = 'success'
|
|
policy.last_drill_at = datetime.utcnow()
|
|
run = BackupRun(policy_id=policy.id, kind='full', status='success',
|
|
verify_level='drilled')
|
|
run.set_metadata({'offsite': {'evidence': 'checksum', 'deep_evidence': 'deep_sha256'}})
|
|
db.session.add(run)
|
|
db.session.commit()
|
|
|
|
section = _collect_restore_confidence()
|
|
assert section['total'] >= 1
|
|
entry = next(p for p in section['policies']
|
|
if p['target_type'] == 'files' and p['target_id'] == 5507)
|
|
assert entry['drill_cadence'] == 'weekly'
|
|
assert entry['latest_verify_level'] == 'drilled'
|
|
assert entry['last_drill_status'] == 'success'
|
|
assert entry['offsite_evidence'] == 'checksum'
|
|
# No paths / secrets leak into the section.
|
|
assert 'storage_path' not in entry and 'remote_key' not in entry
|