0b31d02f10
Unit Tests / test (push) Successful in 15m26s
- DDNSTokenExpired exception triggers auto re-register in update_ip() so cells recover silently after a DDNS DB reset - POST /api/ddns/register lets the user force re-registration from Settings - Re-register button in Settings → External Domain & DDNS (pic_ngo only) - 3 new tests covering register endpoint: wrong provider, missing name, success Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
174 lines
7.1 KiB
Python
174 lines
7.1 KiB
Python
"""Tests for GET /api/ddns/check/<name> and PUT /api/ddns."""
|
|
import json
|
|
import sys
|
|
import os
|
|
import unittest
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'api'))
|
|
|
|
|
|
def _make_client():
|
|
from app import app
|
|
app.config['TESTING'] = True
|
|
return app.test_client()
|
|
|
|
|
|
class TestDdnsCheckName(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.client = _make_client()
|
|
|
|
def _get(self, name):
|
|
return self.client.get(f'/api/ddns/check/{name}')
|
|
|
|
@patch('routes.config.DDNS_API_BASE', 'http://ddns.test', create=True)
|
|
def test_available_name_returns_true(self):
|
|
with patch('routes.config._ureq', create=True):
|
|
import io
|
|
resp_mock = MagicMock()
|
|
resp_mock.read.return_value = b'{"available": true}'
|
|
resp_mock.__enter__ = lambda s: resp_mock
|
|
resp_mock.__exit__ = MagicMock(return_value=False)
|
|
with patch('urllib.request.urlopen', return_value=resp_mock):
|
|
r = self._get('testname')
|
|
self.assertEqual(r.status_code, 200)
|
|
body = json.loads(r.data)
|
|
self.assertTrue(body['available'])
|
|
|
|
@patch('routes.config.DDNS_API_BASE', 'http://ddns.test', create=True)
|
|
def test_taken_name_returns_false(self):
|
|
resp_mock = MagicMock()
|
|
resp_mock.read.return_value = b'{"available": false}'
|
|
resp_mock.__enter__ = lambda s: resp_mock
|
|
resp_mock.__exit__ = MagicMock(return_value=False)
|
|
with patch('urllib.request.urlopen', return_value=resp_mock):
|
|
r = self._get('taken')
|
|
self.assertEqual(r.status_code, 200)
|
|
body = json.loads(r.data)
|
|
self.assertFalse(body['available'])
|
|
|
|
def test_unreachable_returns_503(self):
|
|
import urllib.error
|
|
with patch('urllib.request.urlopen', side_effect=OSError('conn refused')):
|
|
r = self._get('anything')
|
|
self.assertEqual(r.status_code, 503)
|
|
body = json.loads(r.data)
|
|
self.assertIsNone(body['available'])
|
|
|
|
|
|
class TestUpdateDdnsConfig(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.client = _make_client()
|
|
|
|
def _put(self, payload):
|
|
return self.client.put(
|
|
'/api/ddns',
|
|
data=json.dumps(payload),
|
|
content_type='application/json',
|
|
)
|
|
|
|
def test_invalid_domain_mode_returns_400(self):
|
|
r = self._put({'domain_mode': 'invalid_mode'})
|
|
self.assertEqual(r.status_code, 400)
|
|
self.assertIn('domain_mode', json.loads(r.data)['error'])
|
|
|
|
def test_cloudflare_requires_domain_name(self):
|
|
r = self._put({'domain_mode': 'cloudflare', 'cloudflare_api_token': 'tok'})
|
|
self.assertEqual(r.status_code, 400)
|
|
self.assertIn('domain_name', json.loads(r.data)['error'])
|
|
|
|
def test_cloudflare_invalid_token_returns_422(self):
|
|
import urllib.error
|
|
with patch('urllib.request.urlopen', side_effect=urllib.error.HTTPError(
|
|
None, 403, 'Forbidden', {}, None
|
|
)):
|
|
r = self._put({'domain_mode': 'cloudflare', 'domain_name': 'home.example.com',
|
|
'cloudflare_api_token': 'bad-token'})
|
|
self.assertEqual(r.status_code, 422)
|
|
|
|
def test_cloudflare_valid_token_saves_config(self):
|
|
from app import config_manager
|
|
resp_mock = MagicMock()
|
|
resp_mock.read.return_value = b'{"success": true}'
|
|
resp_mock.__enter__ = lambda s: resp_mock
|
|
resp_mock.__exit__ = MagicMock(return_value=False)
|
|
with patch('urllib.request.urlopen', return_value=resp_mock):
|
|
with patch.object(config_manager, 'set_ddns_config') as mock_set_ddns, \
|
|
patch.object(config_manager, 'set_identity_field') as mock_set_id:
|
|
r = self._put({'domain_mode': 'cloudflare', 'domain_name': 'home.example.com',
|
|
'cloudflare_api_token': 'valid-token'})
|
|
self.assertEqual(r.status_code, 200)
|
|
self.assertTrue(json.loads(r.data)['updated'])
|
|
mock_set_ddns.assert_called_once()
|
|
mock_set_id.assert_any_call('domain_mode', 'cloudflare')
|
|
|
|
def test_duckdns_requires_domain_name(self):
|
|
r = self._put({'domain_mode': 'duckdns', 'duckdns_token': 'tok'})
|
|
self.assertEqual(r.status_code, 400)
|
|
|
|
def test_duckdns_invalid_token_returns_422(self):
|
|
resp_mock = MagicMock()
|
|
resp_mock.read.return_value = b'KO'
|
|
resp_mock.__enter__ = lambda s: resp_mock
|
|
resp_mock.__exit__ = MagicMock(return_value=False)
|
|
with patch('urllib.request.urlopen', return_value=resp_mock):
|
|
r = self._put({'domain_mode': 'duckdns', 'domain_name': 'myname.duckdns.org',
|
|
'duckdns_token': 'bad'})
|
|
self.assertEqual(r.status_code, 422)
|
|
|
|
def test_lan_mode_saves_without_validation(self):
|
|
from app import config_manager
|
|
with patch.object(config_manager, 'set_ddns_config') as mock_ddns, \
|
|
patch.object(config_manager, 'set_identity_field') as mock_id:
|
|
r = self._put({'domain_mode': 'lan'})
|
|
self.assertEqual(r.status_code, 200)
|
|
mock_ddns.assert_called_once()
|
|
mock_id.assert_any_call('domain_mode', 'lan')
|
|
|
|
def test_http01_mode_saves_with_domain(self):
|
|
from app import config_manager
|
|
with patch.object(config_manager, 'set_ddns_config') as mock_ddns, \
|
|
patch.object(config_manager, 'set_identity_field') as mock_id:
|
|
r = self._put({'domain_mode': 'http01', 'domain_name': 'home.example.com'})
|
|
self.assertEqual(r.status_code, 200)
|
|
mock_id.assert_any_call('domain_name', 'home.example.com')
|
|
|
|
|
|
class TestDdnsRegister(unittest.TestCase):
|
|
|
|
def setUp(self):
|
|
self.client = _make_client()
|
|
|
|
def test_non_pic_ngo_provider_returns_400(self):
|
|
from app import config_manager
|
|
with patch.object(config_manager, 'configs', {'ddns': {'provider': 'cloudflare'}, '_identity': {}}):
|
|
r = self.client.post('/api/ddns/register')
|
|
self.assertEqual(r.status_code, 400)
|
|
|
|
def test_missing_cell_name_returns_400(self):
|
|
from app import config_manager
|
|
with patch.object(config_manager, 'configs', {'ddns': {'provider': 'pic_ngo'}, '_identity': {}}):
|
|
r = self.client.post('/api/ddns/register')
|
|
self.assertEqual(r.status_code, 400)
|
|
self.assertIn('cell_name', json.loads(r.data)['error'])
|
|
|
|
def test_register_success(self):
|
|
from app import config_manager
|
|
from ddns_manager import DDNSManager
|
|
with patch.object(config_manager, 'configs', {
|
|
'ddns': {'provider': 'pic_ngo'},
|
|
'_identity': {'cell_name': 'mypic'}
|
|
}):
|
|
with patch.object(DDNSManager, 'register', return_value={'subdomain': 'mypic.pic.ngo', 'token': 'tok'}) as mock_reg, \
|
|
patch.object(config_manager, 'set_identity_field') as mock_id:
|
|
r = self.client.post('/api/ddns/register')
|
|
self.assertEqual(r.status_code, 200)
|
|
body = json.loads(r.data)
|
|
self.assertTrue(body['registered'])
|
|
self.assertEqual(body['subdomain'], 'mypic.pic.ngo')
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|