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:
"""Register the cell's subdomain with the configured provider.
Stores the returned token in the identity config under
identity['domain']['ddns']['token'] and records the subdomain.
Fetches the public IP via ipify when ip is empty.
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().
"""
provider = self.get_provider()
if provider is None:
raise DDNSError("No DDNS provider configured")
if not ip:
ip = _get_public_ip() or ''
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:
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
return result
+35 -7
View File
@@ -307,22 +307,50 @@ class TestUpdateIp(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'})
mgr = DDNSManager(config_manager=cm)
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)
result = mgr.register('alpha', '1.2.3.4')
self.assertEqual(result['token'], 'new_tok')
# set_identity_field('domain', ...) should have been called
cm.set_identity_field.assert_called_once()
field_name, field_value = cm.set_identity_field.call_args[0]
self.assertEqual(field_name, 'domain')
self.assertEqual(field_value['ddns']['token'], 'new_tok')
# Token saved to top-level ddns config so update_ip() can find it
cm.set_ddns_config.assert_called_once()
saved_ddns = cm.set_ddns_config.call_args[0][0]
self.assertEqual(saved_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):
cm = _make_config_manager()