Files
pic/tests/test_legacy_cleanup.py
T
roof a69ca1e402
Unit Tests / test (push) Successful in 11m20s
feat: Phase 5 — remove legacy service blocks, one-shot container cleanup
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>
2026-05-29 15:57:45 -04:00

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()