import json from pathlib import Path import sys import tempfile import unittest from unittest.mock import MagicMock, patch sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts')) import cleanup_nextcloud_recovery_fixture as cleanup class RecoveryFixtureCleanup(unittest.TestCase): def proof(self, directory, status='restored'): path = Path(directory) / 'proof.json' path.write_text(json.dumps({'status': status, 'cleanup': True, 'offsite_artifact': 'wp0029-recovery-forgejo-dump-20260905-verified.zip.age'})) return path def test_no_credentials_before_successful_recovery(self): with tempfile.TemporaryDirectory() as directory, patch.object(cleanup, 'bao') as bao: with self.assertRaises(Exception): cleanup.run(self.proof(directory, 'failed'), {}) bao.assert_not_called() def exercise(self, size): opener = MagicMock() head = MagicMock(status=200, headers={'Content-Length': str(size), 'ETag': '"fixture-version"'}) delete = MagicMock(status=204) opener.open.side_effect = [MagicMock(__enter__=lambda _: head, __exit__=lambda *a: False), MagicMock(__enter__=lambda _: delete, __exit__=lambda *a: False)] with tempfile.TemporaryDirectory() as directory, \ patch.object(cleanup, 'bao'), \ patch.object(cleanup, 'data', return_value={'data': {'data': {'BACKUP_USERNAME': 'Backup', 'BACKUP_PASSWORD': 'fixture'}}}), \ patch.object(cleanup, 'quota', return_value={}), \ patch.object(cleanup.urllib.request, 'build_opener', return_value=opener): receipt = {} if size != cleanup.SIZE: with self.assertRaises(Exception): cleanup.run(self.proof(directory), receipt) self.assertEqual(opener.open.call_count, 1) else: cleanup.run(self.proof(directory), receipt) request = opener.open.call_args_list[1].args[0] self.assertEqual(request.method, 'DELETE') self.assertTrue(request.full_url.endswith('/forgejo/' + cleanup.ARTIFACT)) self.assertEqual(request.get_header('If-match'), '"fixture-version"') self.assertEqual(receipt['status'], 'removed') def test_mismatched_object_is_preserved(self): self.exercise(1) def test_exact_object_has_conditional_delete(self): self.exercise(cleanup.SIZE)