fix: DDNS register() always sends public IP and saves token to correct location
Unit Tests / test (push) Successful in 15m27s

Two bugs that prevented registration from working after wizard completion:
1. register(name, '') sent empty IP; server stored blank A record. Now calls
   _get_public_ip() when ip is empty so the A record is always set correctly.
2. Token was saved to _identity.domain.ddns.token (TypeError when domain is a
   string) instead of the top-level ddns config where update_ip() reads it.
   Subdomain also now correctly written to _identity.domain_name.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-26 16:05:55 -04:00
parent 0b31d02f10
commit de43f4a9a0
2 changed files with 49 additions and 19 deletions
+14 -12
View File
@@ -386,27 +386,29 @@ class DDNSManager(BaseServiceManager):
def register(self, name: str, ip: str) -> dict: def register(self, name: str, ip: str) -> dict:
"""Register the cell's subdomain with the configured provider. """Register the cell's subdomain with the configured provider.
Stores the returned token in the identity config under Fetches the public IP via ipify when ip is empty.
identity['domain']['ddns']['token'] and records the subdomain. Stores the returned token in the top-level ddns config (where
update_ip reads it) and updates _identity.domain_name.
Returns the dict from provider.register(). Returns the dict from provider.register().
""" """
provider = self.get_provider() provider = self.get_provider()
if provider is None: if provider is None:
raise DDNSError("No DDNS provider configured") raise DDNSError("No DDNS provider configured")
if not ip:
ip = _get_public_ip() or ''
result = provider.register(name, ip) result = provider.register(name, ip)
# Persist token + subdomain back into identity
identity = self._identity()
domain_cfg = dict(identity.get('domain', {}))
ddns_cfg = dict(domain_cfg.get('ddns', {}))
if 'token' in result:
ddns_cfg['token'] = result['token']
if 'subdomain' in result:
ddns_cfg['subdomain'] = result['subdomain']
domain_cfg['ddns'] = ddns_cfg
if self.config_manager is not None: if self.config_manager is not None:
self.config_manager.set_identity_field('domain', domain_cfg) # Token lives in the top-level ddns config so update_ip() can find it
if 'token' in result:
ddns_cfg = dict(self.config_manager.configs.get('ddns', {}))
ddns_cfg['token'] = result['token']
self.config_manager.set_ddns_config(ddns_cfg)
# Keep domain_name in identity up to date
if 'subdomain' in result:
self.config_manager.set_identity_field('domain_name', result['subdomain'])
self._last_ip = ip self._last_ip = ip
return result return result
+35 -7
View File
@@ -307,22 +307,50 @@ class TestUpdateIp(unittest.TestCase):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
class TestRegister(unittest.TestCase): class TestRegister(unittest.TestCase):
def test_register_stores_token_in_config(self): def test_register_stores_token_in_ddns_config(self):
cm = _make_config_manager(ddns_cfg={'provider': 'pic_ngo'}) cm = _make_config_manager(ddns_cfg={'provider': 'pic_ngo'})
mgr = DDNSManager(config_manager=cm) mgr = DDNSManager(config_manager=cm)
mock_provider = MagicMock() mock_provider = MagicMock()
mock_provider.register.return_value = {'token': 'new_tok', 'subdomain': 'alpha'} mock_provider.register.return_value = {'token': 'new_tok', 'subdomain': 'alpha.pic.ngo'}
mgr.get_provider = MagicMock(return_value=mock_provider) mgr.get_provider = MagicMock(return_value=mock_provider)
result = mgr.register('alpha', '1.2.3.4') result = mgr.register('alpha', '1.2.3.4')
self.assertEqual(result['token'], 'new_tok') self.assertEqual(result['token'], 'new_tok')
# set_identity_field('domain', ...) should have been called # Token saved to top-level ddns config so update_ip() can find it
cm.set_identity_field.assert_called_once() cm.set_ddns_config.assert_called_once()
field_name, field_value = cm.set_identity_field.call_args[0] saved_ddns = cm.set_ddns_config.call_args[0][0]
self.assertEqual(field_name, 'domain') self.assertEqual(saved_ddns['token'], 'new_tok')
self.assertEqual(field_value['ddns']['token'], 'new_tok')
# Subdomain saved to _identity.domain_name
cm.set_identity_field.assert_called_once_with('domain_name', 'alpha.pic.ngo')
def test_register_fetches_public_ip_when_empty(self):
cm = _make_config_manager(ddns_cfg={'provider': 'pic_ngo'})
mgr = DDNSManager(config_manager=cm)
mock_provider = MagicMock()
mock_provider.register.return_value = {'token': 't', 'subdomain': 'alpha.pic.ngo'}
mgr.get_provider = MagicMock(return_value=mock_provider)
with patch('ddns_manager._get_public_ip', return_value='5.6.7.8') as mock_ip:
mgr.register('alpha', '')
mock_ip.assert_called_once()
mock_provider.register.assert_called_once_with('alpha', '5.6.7.8')
def test_register_uses_provided_ip_without_fetching(self):
cm = _make_config_manager(ddns_cfg={'provider': 'pic_ngo'})
mgr = DDNSManager(config_manager=cm)
mock_provider = MagicMock()
mock_provider.register.return_value = {'token': 't', 'subdomain': 'alpha.pic.ngo'}
mgr.get_provider = MagicMock(return_value=mock_provider)
with patch('ddns_manager._get_public_ip') as mock_ip:
mgr.register('alpha', '1.2.3.4')
mock_ip.assert_not_called()
mock_provider.register.assert_called_once_with('alpha', '1.2.3.4')
def test_register_raises_when_no_provider(self): def test_register_raises_when_no_provider(self):
cm = _make_config_manager() cm = _make_config_manager()