test: add .env write verification for port changes

TestEnvFileWrittenOnPortChange (7 tests) confirms that PUT /api/config with
a port change actually writes the new variable to the .env file consumed by
docker compose — the critical link between 'config saved' and 'docker binding
changes on next restart'. Tests cover calendar, webdav, filegator, wireguard,
email; also verifies changing one port does not reset unrelated ports, and
WG_PORT appears exactly once with the new value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 14:06:20 -04:00
parent 255f9e2576
commit 7c94d934e1
+85
View File
@@ -219,11 +219,14 @@ class TestPortChangeDetection(unittest.TestCase):
# Remove any stored service configs so we start clean # Remove any stored service configs so we start clean
for key in ('calendar', 'files', 'wireguard', 'network', 'email'): for key in ('calendar', 'files', 'wireguard', 'network', 'email'):
config_manager.configs.pop(key, None) config_manager.configs.pop(key, None)
self.tmp = tempfile.mkdtemp()
self.env_path = os.path.join(self.tmp, '.env')
def tearDown(self): def tearDown(self):
_clear_pending_restart() _clear_pending_restart()
for key in ('calendar', 'files', 'wireguard', 'network', 'email'): for key in ('calendar', 'files', 'wireguard', 'network', 'email'):
config_manager.configs.pop(key, None) config_manager.configs.pop(key, None)
shutil.rmtree(self.tmp)
def _put_config(self, payload): def _put_config(self, payload):
return self.client.put('/api/config', return self.client.put('/api/config',
@@ -271,5 +274,87 @@ class TestPortChangeDetection(unittest.TestCase):
self.assertFalse(p.get('needs_restart', False)) self.assertFalse(p.get('needs_restart', False))
class TestEnvFileWrittenOnPortChange(unittest.TestCase):
"""Verify that PUT /api/config with a port change actually writes the new
port variable to the .env file consumed by docker compose.
This is the critical link between 'port saved in config' and 'docker binding
changes on next restart'. Without this the container would still bind the old
port even after apply is clicked.
"""
def setUp(self):
app.config['TESTING'] = True
self.client = app.test_client()
_clear_pending_restart()
for key in ('calendar', 'files', 'wireguard', 'network', 'email'):
config_manager.configs.pop(key, None)
self.tmp = tempfile.mkdtemp()
self.env_path = os.path.join(self.tmp, '.env')
# Pre-create .env so write_env_file can overwrite it
import ip_utils
ip_utils.write_env_file('172.20.0.0/16', self.env_path)
def tearDown(self):
_clear_pending_restart()
for key in ('calendar', 'files', 'wireguard', 'network', 'email'):
config_manager.configs.pop(key, None)
shutil.rmtree(self.tmp)
def _put_config(self, payload):
with patch.dict(os.environ, {'COMPOSE_ENV_FILE': self.env_path}):
return self.client.put('/api/config',
data=json.dumps(payload),
content_type='application/json')
def _env_content(self):
return open(self.env_path).read()
def test_calendar_port_written_to_env(self):
self._put_config({'calendar': {'port': 5299}})
self.assertIn('RADICALE_PORT=5299', self._env_content())
def test_webdav_port_written_to_env(self):
self._put_config({'files': {'port': 8181}})
self.assertIn('WEBDAV_PORT=8181', self._env_content())
def test_filegator_port_written_to_env(self):
self._put_config({'files': {'manager_port': 9090}})
self.assertIn('FILEGATOR_PORT=9090', self._env_content())
def test_wireguard_port_written_to_env(self):
self._put_config({'wireguard': {'port': 51999}})
self.assertIn('WG_PORT=51999', self._env_content())
def test_email_smtp_port_written_to_env(self):
self._put_config({'email': {'smtp_port': 2525}})
self.assertIn('MAIL_SMTP_PORT=2525', self._env_content())
def test_other_ports_unchanged_when_one_port_changes(self):
"""Changing calendar port must not reset unrelated ports to defaults."""
# First set webdav to a non-default
self._put_config({'files': {'port': 8181}})
# Then change calendar port
self._put_config({'calendar': {'port': 5299}})
content = self._env_content()
self.assertIn('RADICALE_PORT=5299', content)
self.assertIn('WEBDAV_PORT=8181', content) # must stay at 8181, not revert to 8080
def test_env_uses_symmetric_wg_port_for_docker_binding(self):
"""WG_PORT must be the same value on both sides of the docker port mapping.
docker-compose.yml uses ${WG_PORT:-51820}:${WG_PORT:-51820}/udp so the
host port and container port are always the same. This test verifies
a port change writes a single consistent value to .env so the daemon's
ListenPort matches the Docker binding.
"""
self._put_config({'wireguard': {'port': 51999}})
content = self._env_content()
self.assertIn('WG_PORT=51999', content)
# There must be only one WG_PORT line (no duplicate with old value)
wg_lines = [l for l in content.splitlines() if l.startswith('WG_PORT=')]
self.assertEqual(len(wg_lines), 1, f'Expected exactly one WG_PORT line, got: {wg_lines}')
if __name__ == '__main__': if __name__ == '__main__':
unittest.main() unittest.main()