Corp/ops/security/tests/test_redis_persistence.py
Игорь Надеин 4f6244ab1f
Some checks failed
Deploy production / deploy (push) Failing after 2s
Fix CORP runtime errors and harden deployment and media access
2026-09-10 16:40:08 +03:00

125 lines
5.9 KiB
Python

"""Redis configuration state-machine tests with no live Redis/server access."""
import contextlib
import importlib.util
import io
import json
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
from unittest.mock import patch
SOURCE = Path(__file__).resolve().parents[1]/'install-redis-persistence.py'
OLD = {'appendonly': 'no', 'appendfsync': 'always', 'maxmemory': '0', 'maxmemory-policy': 'allkeys-lru'}
WANTED = {'appendonly': 'yes', 'appendfsync': 'everysec', 'maxmemory': '268435456', 'maxmemory-policy': 'noeviction'}
@unittest.skipUnless(os.name == 'posix' and getattr(os, 'geteuid', lambda: -1)() == 0,
'Disposable root Linux container required')
class RedisPersistenceTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory(prefix='redis-persistence-test-')
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
script = SOURCE.read_text()
for prefix in ('/etc/', '/var/', '/run/'):
script = script.replace(prefix, str(self.root)+prefix)
self.module = importlib.util.module_from_spec(importlib.util.spec_from_loader('redis_persistence', loader=None))
exec(compile(script, str(SOURCE), 'exec'), self.module.__dict__)
for directory in ('etc/redis', 'var/lib/eusrr-deploy', 'var/lib/redis', 'run/lock'):
(self.root/directory).mkdir(parents=True, exist_ok=True)
(self.root/'etc/redis/redis.conf').write_text('original configuration\n')
self.state = dict(OLD)
self.calls = []
self.rewrites = 0
self.fail_stage = None
self.verification_failed = False
def fake_redis(self, *args):
self.calls.append(args)
if args == ('INFO', 'memory'):
return 'used_memory:' + ('220000000' if self.fail_stage == 'memory' else '4194304')
if args == ('INFO', 'persistence'):
status = 'err' if self.fail_stage in ('aof', 'rollback') else 'ok'
return f'aof_enabled:1\naof_rewrite_in_progress:0\naof_rewrite_scheduled:0\naof_last_bgrewrite_status:{status}\n'
if args[:2] == ('CONFIG', 'GET'):
if args[2] == 'bind':
return {'bind': '127.0.0.1 -::1'}
if args[2] == 'dir':
return {'dir': str(self.root/'var/lib/redis')}
if self.fail_stage == 'verification' and self.rewrites == 1 and not self.verification_failed:
self.verification_failed = True
return dict(self.state, appendfsync='unexpected')
return dict(self.state)
if args[:2] == ('CONFIG', 'SET'):
key, value = args[2:]
if self.fail_stage == 'set' and key == 'maxmemory' and value == WANTED[key]:
raise RuntimeError('Injected CONFIG SET failure')
if self.fail_stage == 'rollback' and key == 'appendonly' and value == OLD[key]:
raise RuntimeError('Injected rollback failure')
self.state[key] = value
return 'OK'
if args == ('CONFIG', 'REWRITE'):
self.rewrites += 1
return 'OK'
raise AssertionError(args)
def run_installer(self):
with patch.object(self.module, 'redis', side_effect=self.fake_redis), \
patch.object(self.module.time, 'sleep'), contextlib.redirect_stdout(io.StringIO()):
self.module.main()
def test_success_checks_aof_then_persists_and_verifies(self):
self.run_installer()
self.assertEqual(self.state, WANTED)
self.assertEqual(self.rewrites, 1)
self.assertLess(self.calls.index(('INFO', 'persistence')), self.calls.index(('CONFIG', 'REWRITE')))
self.assertFalse(any(args[0] in ('FLUSHALL', 'FLUSHDB', 'DEL', 'SHUTDOWN') for args in self.calls))
def test_set_aof_and_post_rewrite_verification_failures_all_restore(self):
for stage in ('set', 'aof', 'verification'):
with self.subTest(stage=stage):
self.fail_stage = stage
self.state = dict(OLD)
self.rewrites = 0
self.verification_failed = False
for directory in (self.root/'var/lib/eusrr-deploy').iterdir():
shutil.rmtree(directory)
with self.assertRaises(RuntimeError):
self.run_installer()
self.assertEqual(self.state, OLD)
self.assertGreaterEqual(self.rewrites, 1)
def test_rollback_attempts_remaining_settings_and_reports_incomplete(self):
self.fail_stage = 'rollback'
with self.assertRaisesRegex(RuntimeError, 'Redis recovery incomplete'):
self.run_installer()
for key in ('appendfsync', 'maxmemory', 'maxmemory-policy'):
self.assertEqual(self.state[key], OLD[key])
self.assertIn(('CONFIG', 'REWRITE'), self.calls)
def test_high_memory_fails_before_mutation(self):
self.fail_stage = 'memory'
with self.assertRaisesRegex(SystemExit, 'too close'):
self.run_installer()
self.assertFalse(any(args[:2] == ('CONFIG', 'SET') for args in self.calls))
def test_low_disk_fails_before_mutation(self):
with patch.object(self.module.shutil, 'disk_usage', return_value=shutil._ntuple_diskusage(10000000, 9000000, 1000000)):
with self.assertRaisesRegex(SystemExit, 'Insufficient free space'):
self.run_installer()
self.assertFalse(any(args[:2] == ('CONFIG', 'SET') for args in self.calls))
def test_cli_raw_and_json_info_formats(self):
for output in ('used_memory:123\r\n', json.dumps('used_memory:123\r\n')):
with self.subTest(output=output), patch.object(self.module.subprocess, 'run',
return_value=subprocess.CompletedProcess(['redis-cli'], 0, stdout=output)):
self.assertEqual(self.module.redis('INFO', 'memory'), 'used_memory:123\r\n')
if __name__ == '__main__':
unittest.main()