a69ca1e402
Unit Tests / test (push) Successful in 11m20s
Email, calendar, files, webmail (rainloop), and the file manager (filegator) are removed from the main docker-compose stack. They install as independent per-service compose projects via ServiceComposer. On startup, _cleanup_legacy_builtin_containers() stops and removes any of the 5 legacy containers still running from the old main stack (guarded by a one-shot sentinel in _meta.legacy_builtins_cleaned so it never runs twice). Per-service installs (com.docker.compose.project != 'pic') are left untouched. Changes: - docker-compose.yml: remove mail, radicale, webdav, rainloop, filegator blocks; fix dhcp + ntp to profiles: ["core","full"] so they start with --profile core - Makefile: replace all --profile full with --profile core (6 occurrences); remove mailserver.env conditional from update: target - api/legacy_cleanup.py: new module with cleanup_legacy_builtin_containers() - api/app.py: import and call cleanup at startup before reapply_on_startup() - tests/test_legacy_cleanup.py: 7 tests covering sentinel, absent containers, per-service project skip, main-stack removal, exception safety Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
100 lines
4.2 KiB
Python
100 lines
4.2 KiB
Python
"""Tests for cleanup_legacy_builtin_containers in legacy_cleanup.py."""
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / 'api'))
|
|
|
|
from legacy_cleanup import cleanup_legacy_builtin_containers
|
|
|
|
|
|
def _make_cm(already_cleaned=False):
|
|
cm = MagicMock()
|
|
cm.configs = {'_meta': {'legacy_builtins_cleaned': already_cleaned}} if already_cleaned else {}
|
|
return cm
|
|
|
|
|
|
class TestCleanupLegacyBuiltinContainers(unittest.TestCase):
|
|
|
|
def test_sentinel_true_skips_all_docker_calls(self):
|
|
cm = _make_cm(already_cleaned=True)
|
|
with patch('legacy_cleanup.subprocess.run') as mock_run:
|
|
cleanup_legacy_builtin_containers(cm)
|
|
mock_run.assert_not_called()
|
|
|
|
def test_container_not_found_skipped(self):
|
|
"""docker inspect returns non-zero -> container absent -> no stop/rm."""
|
|
cm = _make_cm()
|
|
inspect_result = MagicMock(returncode=1, stdout='', stderr='')
|
|
with patch('legacy_cleanup.subprocess.run', return_value=inspect_result) as mock_run:
|
|
cleanup_legacy_builtin_containers(cm)
|
|
# Only inspect calls, no stop/rm
|
|
for c in mock_run.call_args_list:
|
|
self.assertNotIn('stop', c.args[0])
|
|
self.assertNotIn('rm', c.args[0])
|
|
|
|
def test_container_from_per_service_project_not_removed(self):
|
|
"""Project label 'pic-email' -> skip (per-service install)."""
|
|
cm = _make_cm()
|
|
inspect_result = MagicMock(returncode=0, stdout='pic-email\n')
|
|
with patch('legacy_cleanup.subprocess.run', return_value=inspect_result) as mock_run:
|
|
cleanup_legacy_builtin_containers(cm)
|
|
for c in mock_run.call_args_list:
|
|
self.assertNotIn('stop', c.args[0])
|
|
|
|
def test_container_from_main_stack_removed(self):
|
|
"""Project label 'pic' -> stop + rm called."""
|
|
cm = _make_cm()
|
|
def side_effect(cmd, **kwargs):
|
|
r = MagicMock(returncode=0)
|
|
if 'inspect' in cmd:
|
|
r.stdout = 'pic\n'
|
|
else:
|
|
r.stdout = ''
|
|
return r
|
|
with patch('legacy_cleanup.subprocess.run', side_effect=side_effect) as mock_run:
|
|
cleanup_legacy_builtin_containers(cm)
|
|
cmds = [c.args[0] for c in mock_run.call_args_list]
|
|
stop_cmds = [c for c in cmds if 'stop' in c]
|
|
self.assertGreater(len(stop_cmds), 0)
|
|
|
|
def test_sentinel_set_after_run(self):
|
|
"""_meta.legacy_builtins_cleaned is set to True after cleanup."""
|
|
cm = _make_cm()
|
|
inspect_result = MagicMock(returncode=1, stdout='')
|
|
with patch('legacy_cleanup.subprocess.run', return_value=inspect_result):
|
|
cleanup_legacy_builtin_containers(cm)
|
|
self.assertTrue(cm.configs.get('_meta', {}).get('legacy_builtins_cleaned', False))
|
|
cm._save_all_configs.assert_called()
|
|
|
|
def test_exception_in_inspect_does_not_crash(self):
|
|
"""If docker inspect throws, the function continues and sets sentinel."""
|
|
cm = _make_cm()
|
|
with patch('legacy_cleanup.subprocess.run', side_effect=OSError('docker not found')):
|
|
cleanup_legacy_builtin_containers(cm) # must not raise
|
|
self.assertTrue(cm.configs.get('_meta', {}).get('legacy_builtins_cleaned', False))
|
|
|
|
def test_mixed_containers_only_pic_project_removed(self):
|
|
"""Some containers are per-service installs, only 'pic'-labelled ones removed."""
|
|
cm = _make_cm()
|
|
call_count = [0]
|
|
def side_effect(cmd, **kwargs):
|
|
r = MagicMock(returncode=0)
|
|
if 'inspect' in cmd:
|
|
call_count[0] += 1
|
|
# First container is per-service, rest are main stack
|
|
r.stdout = 'pic-email\n' if call_count[0] == 1 else 'pic\n'
|
|
else:
|
|
r.stdout = ''
|
|
return r
|
|
with patch('legacy_cleanup.subprocess.run', side_effect=side_effect) as mock_run:
|
|
cleanup_legacy_builtin_containers(cm)
|
|
stop_cmds = [c.args[0] for c in mock_run.call_args_list if 'stop' in c.args[0]]
|
|
# 4 of 5 containers should be stopped (first was per-service)
|
|
self.assertEqual(len(stop_cmds), 4)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|