feat: make DDNS domain_name the effective domain across all services
Unit Tests / test (push) Successful in 11m35s

- ConfigManager.get_effective_domain(): returns domain_name when DDNS
  active (pic_ngo/cloudflare/duckdns), domain otherwise. Used by all
  public-facing services so they use the real registered FQDN.
- ConfigManager.get_internal_domain(): always returns _identity.domain
  (CoreDNS zone name, dnsmasq, cell-link invites — stays internal).
- Silent migration: if domain_mode != lan and domain is generic "cell",
  auto-set to {cell_name}.local for unique CoreDNS zone naming.
- caddy_manager: fix custom_domain bug — cloudflare/http01 modes were
  reading identity.get('custom_domain') which never exists; now reads
  domain_name correctly.
- routes/config, app: expose effective_domain in GET /api/config and
  /api/status responses.
- email_manager, routes/email: use get_effective_domain() for
  OVERRIDE_HOSTNAME, POSTMASTER_ADDRESS, and new-user email defaults.
- ServiceBus.IDENTITY_CHANGED event: emitted from PUT /api/config and
  POST /api/ddns/register after identity writes; caddy_manager and
  email_manager subscribe to regenerate config automatically.
- Settings.jsx: hide Local Domain input in non-LAN modes; show
  read-only effective_domain with "managed by DDNS" badge and an
  Advanced toggle for the internal CoreDNS zone name.
- 11 new test classes covering all new helpers, event subscriptions,
  caddy/email handlers, and the custom_domain fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-28 02:48:47 -04:00
parent 393d56d4ca
commit 1f016de855
13 changed files with 403 additions and 25 deletions
+86
View File
@@ -260,6 +260,92 @@ class TestConfigManager(unittest.TestCase):
"import must not inject zero-filled entries for absent services")
class TestGetEffectiveDomain(unittest.TestCase):
"""Tests for ConfigManager.get_effective_domain and get_internal_domain."""
def setUp(self):
self.temp_dir = tempfile.mkdtemp()
self.config_file = os.path.join(self.temp_dir, 'cell_config.json')
self.data_dir = os.path.join(self.temp_dir, 'data')
os.makedirs(self.data_dir, exist_ok=True)
def tearDown(self):
shutil.rmtree(self.temp_dir)
def _make_cm(self, identity):
cm = ConfigManager(self.config_file, self.data_dir)
cm.configs['_identity'] = identity
return cm
def test_get_effective_domain_lan_mode(self):
cm = self._make_cm({'domain': 'home.local', 'domain_mode': 'lan'})
self.assertEqual(cm.get_effective_domain(), 'home.local')
def test_get_effective_domain_pic_ngo_uses_domain_name(self):
cm = self._make_cm({
'domain': 'home.local',
'domain_mode': 'pic_ngo',
'domain_name': 'home.pic.ngo',
})
self.assertEqual(cm.get_effective_domain(), 'home.pic.ngo')
def test_get_effective_domain_pic_ngo_fallback(self):
cm = self._make_cm({'domain': 'home.local', 'domain_mode': 'pic_ngo'})
self.assertEqual(cm.get_effective_domain(), 'home.local')
def test_get_internal_domain_always_returns_domain(self):
cm = self._make_cm({
'domain': 'home.local',
'domain_mode': 'pic_ngo',
'domain_name': 'home.pic.ngo',
})
self.assertEqual(cm.get_internal_domain(), 'home.local')
def test_get_internal_domain_ignores_domain_name(self):
cm = self._make_cm({
'domain': 'myzone.local',
'domain_mode': 'cloudflare',
'domain_name': 'example.com',
})
self.assertEqual(cm.get_internal_domain(), 'myzone.local')
def test_get_effective_domain_cloudflare_uses_domain_name(self):
cm = self._make_cm({
'domain': 'home.local',
'domain_mode': 'cloudflare',
'domain_name': 'example.com',
})
self.assertEqual(cm.get_effective_domain(), 'example.com')
def test_silent_migration_sets_unique_internal_domain(self):
"""When DDNS is active and domain is the generic 'cell', migration sets cell_name.local."""
config_file2 = os.path.join(self.temp_dir, 'cell_config2.json')
with open(config_file2, 'w') as f:
json.dump({
'_identity': {
'cell_name': 'alpha',
'domain': 'cell',
'domain_mode': 'pic_ngo',
}
}, f)
cm = ConfigManager(config_file2, self.data_dir)
self.assertEqual(cm.get_internal_domain(), 'alpha.local')
def test_silent_migration_does_not_touch_lan_mode(self):
"""Migration must leave domain unchanged when domain_mode is 'lan'."""
config_file2 = os.path.join(self.temp_dir, 'cell_config3.json')
with open(config_file2, 'w') as f:
json.dump({
'_identity': {
'cell_name': 'beta',
'domain': 'cell',
'domain_mode': 'lan',
}
}, f)
cm = ConfigManager(config_file2, self.data_dir)
self.assertEqual(cm.get_internal_domain(), 'cell')
class TestNetworkManagerApply(unittest.TestCase):
"""Test apply_config / apply_domain actually write real config files."""