108 lines
3.7 KiB
Python
108 lines
3.7 KiB
Python
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()
|