fix: clean-install bugs — Tor false-installed, WG port-check honesty, encrypted backup upload
Unit Tests / test (push) Successful in 13m7s
Unit Tests / test (push) Successful in 13m7s
Three independent bugs surfaced during pic1 clean-install testing: 1. Tor _exit_status hardcoded configured=True regardless of whether Tor was actually installed. Status now flows through the same store-installed / container-running bridge used by every other optional service, so Tor only reports installed when the container is present and running. 2. check_port_open compared the port from wg0.conf against the kernel-reported listening port, causing false "port closed" results whenever the conf and the running container were momentarily out of sync. The function is now an honest liveness check: any wg0 interface that is up and has a "listening port:" line in `wg show` is considered open. The check-port API endpoint now also returns the actual kernel listening_port and a port_mismatch flag so the UI can inform the user when a container recreate is needed. (The recreate machinery already exists via the port-change pending-restart path; this fix makes the mismatch visible rather than silently lying about reachability.) 3. upload_backup only handled .zip archives; encrypted .age blobs were rejected with a generic error. The endpoint now calls backup_crypto.is_encrypted() to detect Age-encrypted blobs and stores them verbatim as <id>.tar.gz.age with mode 0600 so they can be uploaded and then restored with a passphrase. The plaintext zip path is unchanged. Tests added/updated: test_connectivity_manager.py (Tor status bridge), test_wireguard_manager.py + test_wireguard_endpoints.py (port-check liveness and mismatch flag), test_config_backup_restore_http.py (encrypted upload round-trip). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,8 @@ api_dir = Path(__file__).parent.parent / 'api'
|
||||
sys.path.insert(0, str(api_dir))
|
||||
|
||||
from app import app
|
||||
import backup_crypto
|
||||
import tarfile
|
||||
|
||||
|
||||
class TestCreateConfigBackup(unittest.TestCase):
|
||||
@@ -345,6 +347,63 @@ class TestUploadBackup(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(r.status_code, 400)
|
||||
|
||||
@patch('app.config_manager')
|
||||
def test_upload_stores_encrypted_blob_verbatim(self, mock_cm):
|
||||
backup_dir = Path(self.tmp)
|
||||
mock_cm.backup_dir = backup_dir
|
||||
blob = backup_crypto.encrypt_bytes(b'payload-bytes', 'secret')
|
||||
self.assertTrue(blob.startswith(backup_crypto.MAGIC))
|
||||
r = self.client.post(
|
||||
'/api/config/backup/upload',
|
||||
data={'file': (io.BytesIO(blob), 'backup_20260101_010101.tar.gz.age')},
|
||||
content_type='multipart/form-data',
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
data = json.loads(r.data)
|
||||
self.assertTrue(data['encrypted'])
|
||||
self.assertEqual(data['backup_id'], 'backup_20260101_010101')
|
||||
archive = backup_dir / 'backup_20260101_010101.tar.gz.age'
|
||||
self.assertTrue(archive.exists())
|
||||
self.assertEqual(archive.read_bytes(), blob)
|
||||
|
||||
@patch('app.config_manager')
|
||||
def test_upload_encrypted_then_restore_round_trip(self, mock_cm):
|
||||
# Build a real encrypted backup archive (tar.gz of a manifest, then
|
||||
# encrypted), upload it, then restore it through the real ConfigManager
|
||||
# decrypt/resolve path with the correct and an incorrect passphrase.
|
||||
from config_manager import ConfigManager
|
||||
|
||||
backup_dir = Path(self.tmp) / 'backups'
|
||||
backup_dir.mkdir(parents=True, exist_ok=True)
|
||||
mock_cm.backup_dir = backup_dir
|
||||
|
||||
tar_buf = io.BytesIO()
|
||||
with tarfile.open(fileobj=tar_buf, mode='w:gz') as tar:
|
||||
inner = json.dumps({'backup_id': 'rt', 'services': []}).encode()
|
||||
info = tarfile.TarInfo('manifest.json')
|
||||
info.size = len(inner)
|
||||
tar.addfile(info, io.BytesIO(inner))
|
||||
blob = backup_crypto.encrypt_bytes(tar_buf.getvalue(), 'pw123')
|
||||
|
||||
r = self.client.post(
|
||||
'/api/config/backup/upload',
|
||||
data={'file': (io.BytesIO(blob), 'rt.tar.gz.age')},
|
||||
content_type='multipart/form-data',
|
||||
)
|
||||
self.assertEqual(r.status_code, 200)
|
||||
backup_id = json.loads(r.data)['backup_id']
|
||||
|
||||
# Resolve+decrypt with the correct passphrase succeeds.
|
||||
real_cm = ConfigManager.__new__(ConfigManager)
|
||||
real_cm.backup_dir = backup_dir
|
||||
path, cleanup = real_cm._resolve_backup_dir(f'{backup_id}.tar.gz.age', 'pw123')
|
||||
self.assertTrue((path / 'manifest.json').exists())
|
||||
shutil.rmtree(cleanup, ignore_errors=True)
|
||||
|
||||
# Wrong passphrase raises PermissionError → route returns 400.
|
||||
with self.assertRaises(PermissionError):
|
||||
real_cm._resolve_backup_dir(f'{backup_id}.tar.gz.age', 'wrong')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user