"""The database backup against the local stack: dump, encrypt, upload, restore. docker compose -f compose.local.yaml exec -T backup python -m tests.backup_test Uses a throwaway age key made here, so nothing secret is kept in the repository. """ import os import subprocess import tempfile from datetime import datetime, timezone from pathlib import Path from botocore.exceptions import ClientError from app.bootstrap import admin_connect from ops import db_backup def check(condition, message): if not condition: raise AssertionError(message) print('PASS:', message) def main(): with tempfile.TemporaryDirectory() as work: identity = Path(work, 'identity.txt') subprocess.run(['age-keygen', '-o', str(identity)], check=True, capture_output=True) public = next(line.split(': ', 1)[1] for line in identity.read_text().splitlines() if line.startswith('# public key: ')) os.environ['BACKUP_AGE_RECIPIENT'] = public check(db_backup.configured(), 'the local backup service is configured once a recipient is set') live = db_backup.counts(None) key = db_backup.run_once() with admin_connect() as c: row = c.execute('SELECT status,bytes FROM dtf_local.backups WHERE object_key=%s', (key,)).fetchone() check(row is not None and row[0] == 'ok' and row[1] > 0, 'the run is recorded for the Kanban') client, bucket = db_backup.bucket() head = client.get_object(Bucket=bucket, Key=key, Range='bytes=0-20')['Body'].read() check(head.startswith(b'age-encryption.org/v1'), 'what leaves the server is encrypted') check(key in [k for k, _, _ in db_backup.listing()], 'the backup is listed in its bucket') try: client.delete_object(Bucket=bucket, Key=key) deleted = True except ClientError: deleted = False check(not deleted and key in [k for k, _, _ in db_backup.listing()], 'the backup credential cannot delete backups') restored = db_backup.verify(str(identity), key) check(restored == live, f'the restore has the same rows as the live database ({restored})') live_name = db_backup.admin_params()['dbname'] try: db_backup.restore(key, str(identity), live_name) refused = False except SystemExit: refused = True check(refused, 'a restore over the live database is refused') other = Path(work, 'other.txt') subprocess.run(['age-keygen', '-o', str(other)], check=True, capture_output=True) try: db_backup.verify(str(other), key) opened = True except RuntimeError: opened = False check(not opened, 'another key cannot open the backup') brt = lambda h, m=0: datetime(2026, 9, 29, h + 3, m, tzinfo=timezone.utc) check(db_backup.next_run(brt(2), 3) == brt(3), 'before 03:00 in Brasília the run is that day') check(db_backup.next_run(brt(3), 3) == datetime(2026, 9, 30, 6, tzinfo=timezone.utc), 'at or after 03:00 the run is the next day') if __name__ == '__main__': main()