From 7c94d934e11b38491f48b250df7a54107bf2db05 Mon Sep 17 00:00:00 2001 From: Dmitrii Date: Wed, 22 Apr 2026 14:06:20 -0400 Subject: [PATCH] test: add .env write verification for port changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/test_pending_restart.py | 85 +++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/tests/test_pending_restart.py b/tests/test_pending_restart.py index e981c9d..4fbe252 100644 --- a/tests/test_pending_restart.py +++ b/tests/test_pending_restart.py @@ -219,11 +219,14 @@ class TestPortChangeDetection(unittest.TestCase): # Remove any stored service configs so we start clean 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') 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): return self.client.put('/api/config', @@ -271,5 +274,87 @@ class TestPortChangeDetection(unittest.TestCase): 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__': unittest.main()