import json 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 = ROOT / 'app' sys.path.insert(0, str(APP)) import host_agent_reconcile class _Database: def __init__(self, operation_id): self.operation_id = operation_id self.envelopes = [] def pending_runtime_agent_operations(self, limit): return [{'operation_id': self.operation_id}] def reconcile_runtime_operation_result(self, envelope): self.envelopes.append(envelope) return {'status': json.loads(envelope)['result']} @unittest.skipIf(os.name == 'nt', 'POSIX ownership and mode checks required') class HostAgentReconcileTests(unittest.TestCase): def setUp(self): self.temp = tempfile.TemporaryDirectory() self.directory = Path(self.temp.name) / 'results' self.directory.mkdir() os.chmod(self.directory, 0o750) details = os.stat(self.directory) self.patches = ( mock.patch.object(host_agent_reconcile, 'HOST_RESULT_DIRECTORY', self.directory), mock.patch.object(host_agent_reconcile, 'HOST_ROOT_UID', details.st_uid), mock.patch.object(host_agent_reconcile, 'HOST_RUNTIME_GID', details.st_gid), ) for patch in self.patches: patch.start() def tearDown(self): for patch in reversed(self.patches): patch.stop() self.temp.cleanup() def write_result(self, operation_id, value): payload = json.dumps( value, sort_keys=True, separators=(',', ':'), ensure_ascii=True, ).encode('ascii') path = self.directory / f'{operation_id}.json' path.write_bytes(payload) os.chmod(path, 0o640) return path def test_reconciles_only_db_selected_canonical_result_and_retains_evidence(self): operation_id = str(uuid.uuid4()) envelope = { 'schema': 1, 'operation_id': operation_id, 'action': 'restart', 'result': 'succeeded', 'safe_category': None, 'safe_detail': None, 'resulting_identity': { 'active_config_sha256': 'a' * 64, 'active_secrets_sha256': 'b' * 64, }, } path = self.write_result(operation_id, envelope) self.write_result(str(uuid.uuid4()), dict(envelope, operation_id=str(uuid.uuid4()))) database = _Database(operation_id) with mock.patch.object( host_agent_reconcile.os, 'listdir', side_effect=AssertionError('must not enumerate'), ): self.assertEqual( host_agent_reconcile.reconcile_pending_host_results(database, limit=7), 1, ) self.assertEqual( database.envelopes, [json.dumps( envelope, sort_keys=True, separators=(',', ':'), ensure_ascii=True, ).encode('ascii')], ) self.assertTrue(path.exists()) def test_missing_result_is_ignored_and_noncanonical_or_unsafe_evidence_fails(self): operation_id = str(uuid.uuid4()) database = _Database(operation_id) self.assertEqual(host_agent_reconcile.reconcile_pending_host_results(database), 0) path = self.directory / f'{operation_id}.json' path.write_text('{"schema": 1}', encoding='ascii') os.chmod(path, 0o640) with self.assertRaises(host_agent_reconcile.HostResultError): host_agent_reconcile.reconcile_pending_host_results(database) os.chmod(self.directory, 0o700) self.assertEqual(host_agent_reconcile.reconcile_pending_host_results(database), 0) if __name__ == '__main__': unittest.main()