"""Guard semantics and recovery tests; no Kubernetes access.""" from pathlib import Path import copy import importlib.util import json import unittest from unittest.mock import patch HERE = Path(__file__).resolve().parent spec = importlib.util.spec_from_file_location('scanner_rollout', HERE / 'rollout.py') m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m) C = json.loads((HERE / 'inputs.json').read_bytes()) def deployment(): init = [{'name': 'unrelated'}] * (C['guard_index'] + 1) init[C['guard_index']] = {'name': 'verify-secret-scanner', 'command': C['guard_command']} volumes = [{'name': 'unrelated'}] * (C['volume_index'] + 1) volumes[C['volume_index']] = C['volume'] return copy.deepcopy({'metadata': {'uid': C['deployment_uid'], 'resourceVersion': '123'}, 'spec': {'strategy': C['strategy'], 'template': {'spec': { 'initContainers': init, 'volumes': volumes, 'containers': [{'name': 'gitea', 'image': 'existing-image', 'env': [{'name': 'public', 'value': 'preserved'}]}]}}}}) def apply_patch(d, operations): value = copy.deepcopy(d) for op in operations: parts = op['path'].strip('/').split('/'); obj = value for key in parts[:-1]: obj = obj[int(key)] if isinstance(obj, list) else obj[key] key = int(parts[-1]) if isinstance(obj, list) else parts[-1] if op['op'] == 'test': if obj[key] != op['value']: raise ValueError('test-failed') else: obj[key] = op['value'] return value class Tests(unittest.TestCase): def test_forward_and_reverse_change_only_two_selected_fields(self): before = deployment(); ops = m.patch_for(before, C) self.assertEqual(sum(o['op'] == 'replace' for o in ops), 2) after = apply_patch(before, ops) self.assertEqual(after['spec']['template']['spec']['containers'], before['spec']['template']['spec']['containers']) self.assertEqual(apply_patch(after, m.patch_for(after, C, True)), before) def test_identity_guard_volume_strategy_and_resource_version_drift_refused(self): for key in ('uid', 'guard', 'volume', 'strategy'): d = deployment(); s = d['spec']['template']['spec'] if key == 'uid': d['metadata']['uid'] = 'other' elif key == 'guard': s['initContainers'][C['guard_index']]['command'] = ['other'] elif key == 'volume': s['volumes'][C['volume_index']]['configMap']['name'] = 'other' else: d['spec']['strategy'] = {'type': 'RollingUpdate'} with self.subTest(key=key), self.assertRaises(RuntimeError): m.patch_for(d, C) d = deployment(); ops = m.patch_for(d, C); d['metadata']['resourceVersion'] = '124' with self.assertRaises(ValueError): apply_patch(d, ops) def test_inventory_requires_all_scanner_hooks_and_preserves_protected_set(self): repo = '/data/git/gitea-repositories/coulomb/example.git' raw = ('775 1000 1000 ' + C['old_hook_sha256'] + ' ' + repo + '/hooks/' + m.HOOK + '\n' + '755 1000 1000 ' + 'a'*64 + ' ' + repo + '/hooks/pre-receive\n').encode() before = m.parse_inventory(raw, C, {C['old_hook_sha256']}) m.protected_equal(before, copy.deepcopy(before)) changed = copy.deepcopy(before); changed[repo]['pre-receive']['sha256'] = 'b'*64 with self.assertRaises(RuntimeError): m.protected_equal(before, changed) with self.assertRaises(RuntimeError): m.parse_inventory(raw.splitlines(keepends=True)[1], C, {C['old_hook_sha256']}) with self.assertRaises(RuntimeError): m.parse_inventory(raw + raw, C, {C['old_hook_sha256']}) def test_lost_patch_response_attempts_guarded_recovery_and_retains_failure(self): class Simulated(m.Rollout): def __init__(self): super().__init__(C); self.recovered = False def preflight(self): return deployment(), 'pod', {} def event(self, name, **kw): self.receipt['events'].append({'event': name, **kw}) def shell(self, *a, **kw): return b'{}' def probe(self, *a): pass def migrate(self, *a): return {} def deployment(self): return deployment() def k(self, *a, **kw): if 'patch' in a: raise RuntimeError('lost-response-after-patch') return b'' def rollback(self, before=None): self.recovered = True x = Simulated() with self.assertRaisesRegex(RuntimeError, 'lost-response'): x.apply(Path('/tmp/sync-scanner-candidate/receive-secret-scan-candidate')) self.assertTrue(x.recovered) self.assertIn('activation-failed', [e['event'] for e in x.receipt['events']]) self.assertNotIn('activation-verified', [e['event'] for e in x.receipt['events']]) def test_rollback_refuses_unknown_deployment_instead_of_overwriting(self): class Simulated(m.Rollout): def target(self): pass def deployment(self): d = deployment(); d['spec']['template']['spec']['initContainers'][C['guard_index']]['command'] = ['unknown'] return d def k(self, *a, **kw): raise AssertionError('must not mutate') with self.assertRaisesRegex(RuntimeError, 'unknown-guard'): Simulated(C).rollback() if __name__ == '__main__': unittest.main()