Files
ProxMenux/oci/tests/test_modify_latency.py
T

104 lines
5.9 KiB
Python
Raw Normal View History

"""Opening Modify avoids backup probes while preserving operation safeguards."""
from pathlib import Path
import sys
import unittest
from unittest.mock import Mock,patch
ROOT=Path(__file__).resolve().parents[1]
sys.path[:0]=[str(ROOT/'src'),str(ROOT/'remote')]
from proxmenux_oci import management as menu,recreation
import oci_carried_record as carried
import oci_instances as instances
import oci_instance_reconcile as reconcile
from proxmenux_oci.ui import UserCancelled
def record():
return {'vmid': 115, 'installation_id': 'test-installation', 'status': 'installed',
'observed': {'config_sha256': 'native-config-fingerprint'},
'template': {'id': 'example-app', 'catalog_ui': {'title': 'Example'},
'container_contract': {'image': {'reference': 'example/app:latest'}},
'proxmox': {'installer_profile': {}}},
'deployment': {'vmid': 115, 'hostname': 'example', 'resources': {'cores': 2, 'memory_mb': 512},
'network': {'bridge': 'vmbr0', 'ipv4': 'dhcp', 'gateway': None},
'rootfs': {'storage': 'local-zfs', 'size_gb': 8}, 'mounts': [
{'container_path': '/data', 'source': 'local-zfs',
'type': 'managed-volume', 'size_gb': 8, 'backup': True}],
'devices': [], 'security': {'unprivileged': True},
'environment': [{'name': 'PASSWORD', 'value': 'private-test-secret',
'sensitive': True}]}}
class SelectedConfigTests(unittest.TestCase):
def test_valid_source_keyed_cache_avoids_another_pct_process(self):
with patch.object(carried,'configuration',create=True,return_value='description: current\n') as reader, \
patch.object(instances,'command') as command:
self.assertEqual(menu._selected_configuration(115),b'description: current\n')
reader.assert_called_once_with(instances.ROOT,115)
command.assert_not_called()
def test_old_or_unavailable_cache_falls_back_to_pct(self):
for reader in (None,Mock(return_value=None),Mock(return_value={}),Mock(side_effect=OSError('cache unavailable'))):
with self.subTest(reader=reader),patch.object(carried,'configuration',reader,create=True), \
patch.object(instances,'command',return_value=b'live config') as command:
self.assertEqual(menu._selected_configuration(115),b'live config')
command.assert_called_once_with('pct','config','115')
def test_identity_check_is_still_performed_on_the_selected_config(self):
r=record();r['installation_id']='11111111-1111-4111-8111-111111111111'
import contextlib
with patch.object(instances,'locked',return_value=contextlib.nullcontext()), \
patch.object(instances,'read',return_value=r), \
patch.object(menu,'_selected_configuration',return_value=b'description: another container\n'):
self.assertEqual(menu.check_selected(ROOT,{'vmid':115})['reason'],'identity-unconfirmed')
class EditorLatencyTests(unittest.TestCase):
def run_editor(self,proposal=None,backup_ready=True,stale=False,adoption_error=False,approved=True,declined=False):
events=[];saved=record()
row={'vmid':115,'reason':'matched','stack':False,'pending':False,'status':'installed'}
def edit(*a,**kw):
events.append('editor')
if declined:return None
if proposal is None:raise UserCancelled()
return proposal
def backup(*a,**kw):events.append('backup');return backup_ready
ui=Mock();ui.unattended=True;ui.review.return_value=approved
with patch.object(menu,'carry_records',return_value={115:'stale'} if stale else {}) as verify, \
patch.object(menu,'check_selected',return_value=row), \
patch.object(menu,'_selected_configuration',return_value=b'current config'), \
patch.object(instances,'read',return_value=saved), \
patch.object(reconcile,'propose',side_effect=ValueError('external changes') if adoption_error else None,return_value=None), \
patch.object(recreation,'edit_recreation',side_effect=edit), \
patch.object(menu,'_work_backup_ready',side_effect=backup), \
patch.object(menu,'_run_lifecycle',side_effect=lambda *a:events.append('lifecycle') or True):
try:result=menu.manage_instance(ROOT,ui,row,action='modify')
except UserCancelled:result=False
verify.assert_called_once_with(ROOT,vmids=[115],verify=True)
return result,events
def test_open_and_cancel_has_no_backup_or_storage_probes(self):
result,events=self.run_editor()
self.assertFalse(result);self.assertEqual(events,['editor'])
def test_declining_the_review_has_no_backup_or_storage_probes(self):
# The review is part of the Modify menu: declining it and leaving the
# menu returns no proposal, so no probe or operation follows.
result,events=self.run_editor(declined=True)
self.assertFalse(result);self.assertEqual(events,['editor'])
def test_confirmed_edit_keeps_backup_check_before_the_lifecycle(self):
result,events=self.run_editor({'operation':'recreate','candidate':record()})
self.assertTrue(result);self.assertEqual(events,['editor','backup','lifecycle'])
def test_missing_backup_space_still_prevents_the_lifecycle(self):
result,events=self.run_editor({'operation':'recreate','candidate':record()},backup_ready=False)
self.assertFalse(result);self.assertEqual(events,['editor','backup'])
def test_restored_or_conflicting_instance_cannot_enter_editor(self):
for options in ({'stale':True},{'adoption_error':True}):
result,events=self.run_editor(**options)
self.assertFalse(result);self.assertEqual(events,[])
if __name__=='__main__':unittest.main()