import os from pathlib import Path import sys import tempfile import unittest import uuid from unittest import mock ROOT = Path(__file__).resolve().parents[1] APP_DIR = ROOT / 'app' sys.path.insert(0, str(APP_DIR)) import result_spool from result_spool import ResultSpool, SpoolCorruptionError from runtime_security import harden_private_file, private_file_ready def write_private_orphan(path, payload=b'orphaned-after-fsync'): flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL if hasattr(os, 'O_BINARY'): flags |= os.O_BINARY descriptor = os.open(path, flags, 0o600) try: harden_private_file(path) with os.fdopen(descriptor, 'wb') as handle: descriptor = None handle.write(payload) handle.flush() os.fsync(handle.fileno()) finally: if descriptor is not None: os.close(descriptor) class ResultSpoolTempRecoveryTests(unittest.TestCase): def make_spool(self, directory): return ResultSpool(directory, min_free_bytes=0) def test_root_and_quarantine_orphans_are_durably_recovered_before_usage(self): with tempfile.TemporaryDirectory() as temp_dir: spool = self.make_spool(os.path.join(temp_dir, 'spool')) event_id = str(uuid.uuid4()) root_orphan = f'{spool._event_path(event_id)}.101.202.303.tmp' quarantine_final = os.path.join( spool.quarantine_directory, f'{event_id}.404.505.conflict.json', ) quarantine_orphan = f'{quarantine_final}.606.707.808.tmp' write_private_orphan(root_orphan) write_private_orphan(quarantine_orphan) self.assertTrue(private_file_ready(root_orphan)) self.assertTrue(private_file_ready(quarantine_orphan)) with mock.patch.object( result_spool, 'durable_unlink', wraps=result_spool.durable_unlink, ) as durable_unlink: self.assertEqual(spool.pending_events(), []) removed = {os.path.normcase(call.args[0]) for call in durable_unlink.call_args_list} self.assertEqual( removed, {os.path.normcase(root_orphan), os.path.normcase(quarantine_orphan)}, ) self.assertFalse(os.path.lexists(root_orphan)) self.assertFalse(os.path.lexists(quarantine_orphan)) def test_reservation_orphan_is_recovered_before_reservation_load(self): with tempfile.TemporaryDirectory() as temp_dir: spool = self.make_spool(os.path.join(temp_dir, 'spool')) reservation_id = spool.reserve_claims('owner', 1, 60) reservation_path = spool._reservation_path(reservation_id) orphan = f'{reservation_path}.101.202.303.tmp' write_private_orphan(orphan) with mock.patch.object( result_spool, 'durable_unlink', wraps=result_spool.durable_unlink, ) as durable_unlink: self.assertTrue(spool.assert_claims_allowed()) durable_unlink.assert_called_once_with(orphan) self.assertFalse(os.path.lexists(orphan)) self.assertTrue(os.path.exists(reservation_path)) def test_full_event_capacity_still_has_recovery_headroom(self): with tempfile.TemporaryDirectory() as temp_dir: spool = ResultSpool( os.path.join(temp_dir, 'spool'), max_events=1, max_event_bytes=1024 * 1024, max_total_bytes=1024 * 1024, min_free_bytes=0, ) event_id = str(uuid.uuid4()) spool.write_event({'version': 1, 'scan_event_id': event_id, 'result': {}}) orphan = f'{spool._event_path(str(uuid.uuid4()))}.101.202.303.tmp' write_private_orphan(orphan) events = spool.pending_events() self.assertEqual([event.event_id for event in events], [event_id]) self.assertFalse(os.path.lexists(orphan)) def test_invalid_prospective_final_name_is_left_and_blocks(self): with tempfile.TemporaryDirectory() as temp_dir: spool = self.make_spool(os.path.join(temp_dir, 'spool')) unrelated = os.path.join( spool.reservation_directory, 'not-an-internal-reservation.json.101.202.303.tmp', ) write_private_orphan(unrelated) with mock.patch.object( result_spool, 'durable_unlink', wraps=result_spool.durable_unlink, ) as durable_unlink: with self.assertRaises(SpoolCorruptionError): spool.assert_claims_allowed() durable_unlink.assert_not_called() self.assertTrue(os.path.exists(unrelated)) def test_matching_symlink_is_left_and_blocks_when_supported(self): with tempfile.TemporaryDirectory() as temp_dir: spool = self.make_spool(os.path.join(temp_dir, 'spool')) external = os.path.join(temp_dir, 'external.txt') Path(external).write_text('external-data', encoding='ascii') link = f'{spool._event_path(str(uuid.uuid4()))}.101.202.303.tmp' try: os.symlink(external, link) except (OSError, NotImplementedError): self.skipTest('symlink creation is unavailable on this platform') with mock.patch.object( result_spool, 'durable_unlink', wraps=result_spool.durable_unlink, ) as durable_unlink: with self.assertRaises(SpoolCorruptionError): spool.pending_events() durable_unlink.assert_not_called() self.assertTrue(os.path.lexists(link)) self.assertEqual(Path(external).read_text(encoding='ascii'), 'external-data') if __name__ == '__main__': unittest.main()