1#!/usr/bin/env python3
2import importlib
3import json
4from pathlib import Path
5import sqlite3
6import tempfile
7
8
9def main():
10 data = importlib.import_module('data')
11 with tempfile.TemporaryDirectory(prefix='dashboard-backup-') as temporary:
12 data.STATE = Path(temporary)
13 data.BACKUPS = data.STATE / 'backups'
14 live = data.STATE / 'dashboard'
15 live.mkdir()
16 connections = []
17 for name in ['accounts.sqlite', 'connections.sqlite']:
18 db = sqlite3.connect(live / name)
19 db.execute('PRAGMA journal_mode=WAL')
20 db.execute('CREATE TABLE durable (value TEXT)')
21 db.execute('INSERT INTO durable VALUES (?)', (name,))
22 db.commit()
23 connections.append(db)
24 (live / 'pictures').mkdir()
25 (live / 'pictures' / 'avatar').write_bytes(b'original picture')
26 backup_id = '20261005T000000Z-abcdef'
27 directory = data.BACKUPS / backup_id
28 directory.mkdir(parents=True)
29 files = data.backup_dashboard(directory / 'dashboard')
30 assert set(files) == {'accounts.sqlite', 'connections.sqlite', 'pictures/avatar'}
31 for name in ['accounts.sqlite', 'connections.sqlite']:
32 with sqlite3.connect(directory / 'dashboard' / name) as db:
33 assert db.execute('SELECT value FROM durable').fetchall() == [(name,)]
34 revision = 'a' * 16
35 (directory / 'manifest.json').write_text(json.dumps({'id': backup_id, 'fromRelease': revision, 'services': {'dashboard': {'files': files}}}))
36 data.current_release = lambda: Path('/releases') / revision
37 lifecycle = []
38 def run(*args, **kwargs):
39 lifecycle.append(args)
40 if args == ('systemctl', 'stop', 'studio-dashboard'):
41 for db in connections:
42 db.close()
43 data.run = run
44 for db in connections:
45 db.execute('DELETE FROM durable')
46 db.execute("INSERT INTO durable VALUES ('later change')")
47 db.commit()
48 (live / 'pictures' / 'avatar').write_bytes(b'later picture')
49 data.restore(backup_id, 'dashboard')
50 assert lifecycle == [('systemctl', 'stop', 'studio-dashboard'), ('systemctl', 'start', 'studio-dashboard'), ('systemctl', 'is-active', '--quiet', 'studio-dashboard')]
51 assert (live / 'pictures' / 'avatar').read_bytes() == b'original picture'
52 for name in ['accounts.sqlite', 'connections.sqlite']:
53 with sqlite3.connect(live / name) as db:
54 assert db.execute('SELECT value FROM durable').fetchall() == [(name,)]
55 safety = next(data.BACKUPS.glob('before-restore-*/dashboard/' + name))
56 with sqlite3.connect(safety) as db:
57 assert db.execute('SELECT value FROM durable').fetchall() == [('later change',)]
58 lifecycle.clear()
59 (directory / 'dashboard' / 'pictures/avatar').write_bytes(b'corrupted backup')
60 try:
61 data.restore(backup_id, 'dashboard')
62 except ValueError:
63 pass
64 else:
65 raise AssertionError('corrupt backup accepted')
66 assert not lifecycle
67 print(json.dumps({'live_wal_backup': 'passed', 'account_and_connection_restore': 'passed', 'safety_copy': 'passed', 'corrupt_backup_refused_before_stop': 'passed'}))
68
69
70if __name__ == '__main__':
71 main()